From c4380589bcb5e58a8928a30f351c9aed1044debf Mon Sep 17 00:00:00 2001 From: Drewster727 Date: Sat, 16 Apr 2016 12:22:02 -0500 Subject: [PATCH 01/76] set the max json length (fixes large json response errors) --- PlexRequests.UI/Bootstrapper.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/PlexRequests.UI/Bootstrapper.cs b/PlexRequests.UI/Bootstrapper.cs index 6e540dbaf..e39720c26 100644 --- a/PlexRequests.UI/Bootstrapper.cs +++ b/PlexRequests.UI/Bootstrapper.cs @@ -50,6 +50,7 @@ using PlexRequests.Store; using PlexRequests.Store.Models; using PlexRequests.Store.Repository; using PlexRequests.UI.Helpers; +using Nancy.Json; namespace PlexRequests.UI { @@ -104,6 +105,8 @@ namespace PlexRequests.UI // NotificationService container.Register().AsSingleton(); + JsonSettings.MaxJsonLength = int.MaxValue; + SubscribeAllObservers(container); base.ConfigureRequestContainer(container, context); var loc = ServiceLocator.Instance; From 5dc39b9b3ae9cd33cdcf346f8993fd7dd81f8e95 Mon Sep 17 00:00:00 2001 From: Jamie Date: Sat, 16 Apr 2016 20:41:04 +0100 Subject: [PATCH 02/76] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fd915d2d8..d98db0311 100644 --- a/README.md +++ b/README.md @@ -49,4 +49,4 @@ If you feel like donating you can [here!](https://paypal.me/PlexRequestsNet) ## A massive thanks to everyone below for all their help! -[heartisall](https://github.com/heartisall), [Stuke00](https://github.com/Stuke00), [shiitake](https://github.com/shiitake), [Drewster727](https://github.com/Drewster727), Majawat, [EddiYo](https://github.com/EddiYo) +[heartisall](https://github.com/heartisall), [Stuke00](https://github.com/Stuke00), [shiitake](https://github.com/shiitake), [Drewster727](https://github.com/Drewster727), Majawat, [EddiYo](https://github.com/EddiYo), [SaskiFX](https://github.com/SaskiFX) From 84edd6636d7860a81ec740a1ea1637ad6287158c Mon Sep 17 00:00:00 2001 From: Drewster727 Date: Sun, 17 Apr 2016 21:58:34 -0500 Subject: [PATCH 03/76] fix saving the log level --- PlexRequests.Core/PlexRequests.Core.csproj | 1 + .../SettingModels/LogSettings.cs | 36 +++++++++++++++++++ PlexRequests.UI/Bootstrapper.cs | 1 + PlexRequests.UI/Modules/AdminModule.cs | 11 ++++++ PlexRequests.UI/Program.cs | 15 ++++++-- PlexRequests.UI/Views/Admin/Logs.cshtml | 13 ++++--- 6 files changed, 69 insertions(+), 8 deletions(-) create mode 100644 PlexRequests.Core/SettingModels/LogSettings.cs diff --git a/PlexRequests.Core/PlexRequests.Core.csproj b/PlexRequests.Core/PlexRequests.Core.csproj index 773d17e88..453ea859e 100644 --- a/PlexRequests.Core/PlexRequests.Core.csproj +++ b/PlexRequests.Core/PlexRequests.Core.csproj @@ -83,6 +83,7 @@ + diff --git a/PlexRequests.Core/SettingModels/LogSettings.cs b/PlexRequests.Core/SettingModels/LogSettings.cs new file mode 100644 index 000000000..bcea3127d --- /dev/null +++ b/PlexRequests.Core/SettingModels/LogSettings.cs @@ -0,0 +1,36 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: SickRageSettings.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 NLog; + +namespace PlexRequests.Core.SettingModels +{ + public class LogSettings : Settings + { + public int Level { get; set; } + } +} \ No newline at end of file diff --git a/PlexRequests.UI/Bootstrapper.cs b/PlexRequests.UI/Bootstrapper.cs index e39720c26..48aeb8290 100644 --- a/PlexRequests.UI/Bootstrapper.cs +++ b/PlexRequests.UI/Bootstrapper.cs @@ -78,6 +78,7 @@ namespace PlexRequests.UI container.Register, SettingsServiceV2>(); container.Register, SettingsServiceV2>(); container.Register, SettingsServiceV2>(); + container.Register, SettingsServiceV2>(); // Repo's container.Register, GenericRepository>(); diff --git a/PlexRequests.UI/Modules/AdminModule.cs b/PlexRequests.UI/Modules/AdminModule.cs index 1650c4ed4..f30d0cee3 100644 --- a/PlexRequests.UI/Modules/AdminModule.cs +++ b/PlexRequests.UI/Modules/AdminModule.cs @@ -68,6 +68,7 @@ namespace PlexRequests.UI.Modules private ISettingsService PushbulletService { get; } private ISettingsService PushoverService { get; } private ISettingsService HeadphonesService { get; } + private ISettingsService LogService { get; } private IPlexApi PlexApi { get; } private ISonarrApi SonarrApi { get; } private IPushbulletApi PushbulletApi { get; } @@ -95,6 +96,7 @@ namespace PlexRequests.UI.Modules IRepository logsRepo, INotificationService notify, ISettingsService headphones, + ISettingsService logs, ICacheProvider cache) : base("admin") { PrService = prService; @@ -114,6 +116,7 @@ namespace PlexRequests.UI.Modules PushoverApi = pushoverApi; NotificationService = notify; HeadphonesService = headphones; + LogService = logs; Cache = cache; #if !DEBUG @@ -637,8 +640,16 @@ namespace PlexRequests.UI.Modules private Response UpdateLogLevels(int level) { + var settings = LogService.GetSettings(); + + // apply the level var newLevel = LogLevel.FromOrdinal(level); LoggingHelper.ReconfigureLogLevel(newLevel); + + //save the log settings + settings.Level = level; + LogService.SaveSettings(settings); + return Response.AsJson(new JsonResponseModel { Result = true, Message = $"The new log level is now {newLevel}"}); } diff --git a/PlexRequests.UI/Program.cs b/PlexRequests.UI/Program.cs index c16a08c08..a23309721 100644 --- a/PlexRequests.UI/Program.cs +++ b/PlexRequests.UI/Program.cs @@ -25,7 +25,6 @@ // ************************************************************************/ #endregion using System; -using System.Collections.Generic; using Microsoft.Owin.Hosting; @@ -42,8 +41,6 @@ using PlexRequests.Store.Repository; using System.Diagnostics; using FluentScheduler; - -using PlexRequests.Services; using PlexRequests.UI.Jobs; namespace PlexRequests.UI @@ -91,6 +88,7 @@ namespace PlexRequests.UI var cn = s.SetupDb(baseUrl); s.CacheQualityProfiles(); ConfigureTargets(cn); + SetupLogging(); if (port == -1) port = GetStartupPort(); @@ -157,6 +155,17 @@ namespace PlexRequests.UI LoggingHelper.ConfigureLogging(connectionString); } + private static void SetupLogging() + { + var settingsService = new SettingsServiceV2(new SettingsJsonRepository(new DbConfiguration(new SqliteFactory()), new MemoryCacheProvider())); + var logSettings = settingsService.GetSettings(); + + if (logSettings != null) + { + LoggingHelper.ReconfigureLogLevel(LogLevel.FromOrdinal(logSettings.Level)); + } + } + private static void SetupSchedulers() { TaskManager.TaskFactory = new PlexTaskFactory(); diff --git a/PlexRequests.UI/Views/Admin/Logs.cshtml b/PlexRequests.UI/Views/Admin/Logs.cshtml index 50a689d89..ed32d6552 100644 --- a/PlexRequests.UI/Views/Admin/Logs.cshtml +++ b/PlexRequests.UI/Views/Admin/Logs.cshtml @@ -76,11 +76,14 @@ url: logUrl, dataType: "json", success: function (response) { - $("#select > option").each(function (level) { - if (response[0] == level.value) { - $('#' + level.target.id).prop("selected", "selected"); - } - }); + if (response && response.length > 0) { + $("#selected > option").each(function (level) { + var $opt = $(this); + if (response[0].ordinal == level) { + $opt.prop("selected", "selected"); + } + }); + } }, error: function (e) { console.log(e); From ffbb3d9a443f31d97af3ea477e93fc5c2f62483f Mon Sep 17 00:00:00 2001 From: tidusjar Date: Mon, 18 Apr 2016 18:46:09 +0100 Subject: [PATCH 04/76] Fixed #162 --- PlexRequests.UI/Views/Admin/Sonarr.cshtml | 56 +++++++++++++---------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/PlexRequests.UI/Views/Admin/Sonarr.cshtml b/PlexRequests.UI/Views/Admin/Sonarr.cshtml index 6ca64285d..fd50071f5 100644 --- a/PlexRequests.UI/Views/Admin/Sonarr.cshtml +++ b/PlexRequests.UI/Views/Admin/Sonarr.cshtml @@ -130,34 +130,40 @@ @if (!string.IsNullOrEmpty(Model.QualityProfile)) { - - var qualitySelected = @Model.QualityProfile; - if (!qualitySelected) { - return; - } - var $form = $("#mainForm"); - $.ajax({ - type: $form.prop("method"), - data: $form.serialize(), - url: "sonarrprofiles", - dataType: "json", - success: function(response) { - response.forEach(function(result) { - if (result.id == qualitySelected) { - $("#select").append(""); - } else { - $("#select").append(""); - } - }); - }, - error: function(e) { - console.log(e); - generateNotify("Something went wrong!", "danger"); + + + preLoad(); + + function preLoad() { + var qualitySelected = @Model.QualityProfile; + if (!qualitySelected) { + return; } - }); + var $form = $("#mainForm"); + $.ajax({ + type: $form.prop("method"), + data: $form.serialize(), + url: "sonarrprofiles", + dataType: "json", + success: function(response) { + response.forEach(function(result) { + if (result.id == qualitySelected) { + + $("#select").append(""); + } else { + $("#select").append(""); + } + }); + }, + error: function(e) { + console.log(e); + generateNotify("Something went wrong!", "danger"); + } + }); + } - } + } $('#save').click(function(e) { From 8388514931de6fce4e126b3ceee8e25b37c01bb4 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Mon, 18 Apr 2016 20:09:03 +0100 Subject: [PATCH 05/76] Fixed #168 --- PlexRequests.UI/Modules/SearchModule.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PlexRequests.UI/Modules/SearchModule.cs b/PlexRequests.UI/Modules/SearchModule.cs index b7faaa9bc..78bf7540d 100644 --- a/PlexRequests.UI/Modules/SearchModule.cs +++ b/PlexRequests.UI/Modules/SearchModule.cs @@ -668,7 +668,7 @@ namespace PlexRequests.UI.Modules return Response.AsJson(new JsonResponseModel { Result = false, Message = result?.message != null ? "Message From SickRage: " + result.message : "Something went wrong adding the movie to SickRage! Please check your settings." }); } - return Response.AsJson("The request of TV Shows is not correctly set up. Please contact your admin."); + return Response.AsJson(new JsonResponseModel { Result=false, Message = "The request of TV Shows is not correctly set up. Please contact your admin."}); } From 9c61f909decdcfd8ac6e879562b42f7f98418da7 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Tue, 19 Apr 2016 13:37:58 +0100 Subject: [PATCH 06/76] Switched out the schedulers, this seems to be a better implimentation to the previous and is easier to add new "jobs" in. --- PlexRequests.Services.Tests/app.config | 8 +- .../AvailabilityUpdateService.cs | 93 --- PlexRequests.Services/Configuration.cs | 40 -- PlexRequests.Services/ConfigurationReader.cs | 38 -- .../Interfaces/IAvailabilityChecker.cs | 2 +- .../Interfaces/IConfigurationReader.cs | 33 - .../Interfaces/ICouchPotatoCacher.cs | 2 +- .../{ITvCacher.cs => ISickRageCacher.cs} | 16 +- .../Interfaces/ISonarrCacher.cs | 2 +- .../{ => Jobs}/CouchPotatoCacher.cs | 157 ++--- .../{ => Jobs}/PlexAvailabilityChecker.cs | 598 +++++++++--------- .../{ => Jobs}/SickRageCacher.cs | 158 ++--- .../{ => Jobs}/SonarrCacher.cs | 158 ++--- PlexRequests.Services/MediaCacheService.cs | 102 --- .../PlexRequests.Services.csproj | 29 +- PlexRequests.Services/PlexType.cs | 35 - PlexRequests.Services/UpdateInterval.cs | 38 -- PlexRequests.Services/app.config | 8 +- PlexRequests.Services/packages.config | 4 +- PlexRequests.UI/Bootstrapper.cs | 10 +- PlexRequests.UI/Helpers/ServiceLocator.cs | 15 +- PlexRequests.UI/Jobs/CustomJobFactory.cs | 94 +++ PlexRequests.UI/Jobs/MediaCacheRegistry.cs | 41 -- PlexRequests.UI/Jobs/PlexRegistry.cs | 41 -- PlexRequests.UI/Jobs/PlexTaskFactory.cs | 23 - PlexRequests.UI/Jobs/Scheduler.cs | 137 ++++ PlexRequests.UI/PlexRequests.UI.csproj | 24 +- PlexRequests.UI/Program.cs | 14 +- PlexRequests.UI/Startup.cs | 13 +- PlexRequests.UI/app.config | 30 +- PlexRequests.UI/job_scheduling_data_2_0.xsd | 361 +++++++++++ PlexRequests.UI/packages.config | 4 +- 32 files changed, 1237 insertions(+), 1091 deletions(-) delete mode 100644 PlexRequests.Services/AvailabilityUpdateService.cs delete mode 100644 PlexRequests.Services/Configuration.cs delete mode 100644 PlexRequests.Services/ConfigurationReader.cs delete mode 100644 PlexRequests.Services/Interfaces/IConfigurationReader.cs rename PlexRequests.Services/Interfaces/{ITvCacher.cs => ISickRageCacher.cs} (75%) rename PlexRequests.Services/{ => Jobs}/CouchPotatoCacher.cs (87%) rename PlexRequests.Services/{ => Jobs}/PlexAvailabilityChecker.cs (95%) rename PlexRequests.Services/{ => Jobs}/SickRageCacher.cs (90%) rename PlexRequests.Services/{ => Jobs}/SonarrCacher.cs (91%) delete mode 100644 PlexRequests.Services/MediaCacheService.cs delete mode 100644 PlexRequests.Services/PlexType.cs delete mode 100644 PlexRequests.Services/UpdateInterval.cs create mode 100644 PlexRequests.UI/Jobs/CustomJobFactory.cs delete mode 100644 PlexRequests.UI/Jobs/MediaCacheRegistry.cs delete mode 100644 PlexRequests.UI/Jobs/PlexRegistry.cs delete mode 100644 PlexRequests.UI/Jobs/PlexTaskFactory.cs create mode 100644 PlexRequests.UI/Jobs/Scheduler.cs create mode 100644 PlexRequests.UI/job_scheduling_data_2_0.xsd diff --git a/PlexRequests.Services.Tests/app.config b/PlexRequests.Services.Tests/app.config index 0404cdcb4..44b249bff 100644 --- a/PlexRequests.Services.Tests/app.config +++ b/PlexRequests.Services.Tests/app.config @@ -1,11 +1,11 @@ - + - - + + - + diff --git a/PlexRequests.Services/AvailabilityUpdateService.cs b/PlexRequests.Services/AvailabilityUpdateService.cs deleted file mode 100644 index a9268a272..000000000 --- a/PlexRequests.Services/AvailabilityUpdateService.cs +++ /dev/null @@ -1,93 +0,0 @@ -#region Copyright -// /************************************************************************ -// Copyright (c) 2016 Jamie Rees -// File: AvailabilityUpdateService.cs -// Created By: Jamie Rees -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// ************************************************************************/ -#endregion -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Reactive.Linq; -using System.Web.Hosting; - -using FluentScheduler; - -using Mono.Data.Sqlite; - -using NLog; - -using PlexRequests.Api; -using PlexRequests.Core; -using PlexRequests.Core.SettingModels; -using PlexRequests.Helpers; -using PlexRequests.Services.Interfaces; -using PlexRequests.Store; -using PlexRequests.Store.Repository; -using System.Threading.Tasks; - -namespace PlexRequests.Services -{ - public class AvailabilityUpdateService : ITask, IRegisteredObject, IAvailabilityUpdateService - { - - public AvailabilityUpdateService() - { - - var memCache = new MemoryCacheProvider(); - var dbConfig = new DbConfiguration(new SqliteFactory()); - var repo = new SettingsJsonRepository(dbConfig, memCache); - - ConfigurationReader = new ConfigurationReader(); - Checker = new PlexAvailabilityChecker(new SettingsServiceV2(repo), new SettingsServiceV2(repo), new JsonRequestService(new RequestJsonRepository(dbConfig, memCache)), new PlexApi(), memCache); - HostingEnvironment.RegisterObject(this); - } - - private static Logger Log = LogManager.GetCurrentClassLogger(); - - private IConfigurationReader ConfigurationReader { get; } - private IAvailabilityChecker Checker { get; } - private IDisposable UpdateSubscription { get; set; } - - public void Start(Configuration c) - { - UpdateSubscription?.Dispose(); - Task.Factory.StartNew(() => Checker.CheckAndUpdateAll(-1)); // cache the libraries and run the availability checks - UpdateSubscription = Observable.Interval(c.Intervals.Notification).Subscribe(Checker.CheckAndUpdateAll); - } - - public void Execute() - { - Start(ConfigurationReader.Read()); - } - - public void Stop(bool immediate) - { - HostingEnvironment.UnregisterObject(this); - } - } - - public interface IAvailabilityUpdateService - { - void Start(Configuration c); - } -} diff --git a/PlexRequests.Services/Configuration.cs b/PlexRequests.Services/Configuration.cs deleted file mode 100644 index d88789ecb..000000000 --- a/PlexRequests.Services/Configuration.cs +++ /dev/null @@ -1,40 +0,0 @@ -#region Copyright -// /************************************************************************ -// Copyright (c) 2016 Jamie Rees -// File: Congifuration.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 PlexRequests.Services.Interfaces; - -namespace PlexRequests.Services -{ - public class Configuration - { - public Configuration(IIntervals intervals) - { - Intervals = intervals; - } - - public IIntervals Intervals { get; set; } - } -} \ No newline at end of file diff --git a/PlexRequests.Services/ConfigurationReader.cs b/PlexRequests.Services/ConfigurationReader.cs deleted file mode 100644 index 4c35ff2e7..000000000 --- a/PlexRequests.Services/ConfigurationReader.cs +++ /dev/null @@ -1,38 +0,0 @@ -#region Copyright -// /************************************************************************ -// Copyright (c) 2016 Jamie Rees -// File: ConfigurationReader.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 PlexRequests.Services.Interfaces; - -namespace PlexRequests.Services -{ - public class ConfigurationReader : IConfigurationReader - { - public Configuration Read() - { - return new Configuration(new UpdateInterval()); - } - } -} \ No newline at end of file diff --git a/PlexRequests.Services/Interfaces/IAvailabilityChecker.cs b/PlexRequests.Services/Interfaces/IAvailabilityChecker.cs index 3d7a423ac..14eda1731 100644 --- a/PlexRequests.Services/Interfaces/IAvailabilityChecker.cs +++ b/PlexRequests.Services/Interfaces/IAvailabilityChecker.cs @@ -31,7 +31,7 @@ namespace PlexRequests.Services.Interfaces { public interface IAvailabilityChecker { - void CheckAndUpdateAll(long check); + void CheckAndUpdateAll(); List GetPlexMovies(); bool IsMovieAvailable(PlexMovie[] plexMovies, string title, string year); List GetPlexTvShows(); diff --git a/PlexRequests.Services/Interfaces/IConfigurationReader.cs b/PlexRequests.Services/Interfaces/IConfigurationReader.cs deleted file mode 100644 index 179f66e96..000000000 --- a/PlexRequests.Services/Interfaces/IConfigurationReader.cs +++ /dev/null @@ -1,33 +0,0 @@ -#region Copyright -// /************************************************************************ -// Copyright (c) 2016 Jamie Rees -// File: IConfigurationReader.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 PlexRequests.Services.Interfaces -{ - public interface IConfigurationReader - { - Configuration Read(); - } -} \ No newline at end of file diff --git a/PlexRequests.Services/Interfaces/ICouchPotatoCacher.cs b/PlexRequests.Services/Interfaces/ICouchPotatoCacher.cs index 4da6fa852..05fa8da75 100644 --- a/PlexRequests.Services/Interfaces/ICouchPotatoCacher.cs +++ b/PlexRequests.Services/Interfaces/ICouchPotatoCacher.cs @@ -2,7 +2,7 @@ { public interface ICouchPotatoCacher { - void Queued(long check); + void Queued(); int[] QueuedIds(); } } diff --git a/PlexRequests.Services/Interfaces/ITvCacher.cs b/PlexRequests.Services/Interfaces/ISickRageCacher.cs similarity index 75% rename from PlexRequests.Services/Interfaces/ITvCacher.cs rename to PlexRequests.Services/Interfaces/ISickRageCacher.cs index a42d2a1e4..54f1e0573 100644 --- a/PlexRequests.Services/Interfaces/ITvCacher.cs +++ b/PlexRequests.Services/Interfaces/ISickRageCacher.cs @@ -1,8 +1,8 @@ -namespace PlexRequests.Services.Interfaces -{ - public interface ISickRageCacher - { - void Queued(long check); - int[] QueuedIds(); - } -} +namespace PlexRequests.Services.Interfaces +{ + public interface ISickRageCacher + { + void Queued(); + int[] QueuedIds(); + } +} diff --git a/PlexRequests.Services/Interfaces/ISonarrCacher.cs b/PlexRequests.Services/Interfaces/ISonarrCacher.cs index 590666e07..a7cf8f9fa 100644 --- a/PlexRequests.Services/Interfaces/ISonarrCacher.cs +++ b/PlexRequests.Services/Interfaces/ISonarrCacher.cs @@ -2,7 +2,7 @@ { public interface ISonarrCacher { - void Queued(long check); + void Queued(); int[] QueuedIds(); } } diff --git a/PlexRequests.Services/CouchPotatoCacher.cs b/PlexRequests.Services/Jobs/CouchPotatoCacher.cs similarity index 87% rename from PlexRequests.Services/CouchPotatoCacher.cs rename to PlexRequests.Services/Jobs/CouchPotatoCacher.cs index 557a29179..8bc50c5b6 100644 --- a/PlexRequests.Services/CouchPotatoCacher.cs +++ b/PlexRequests.Services/Jobs/CouchPotatoCacher.cs @@ -1,77 +1,82 @@ -#region Copyright -// /************************************************************************ -// Copyright (c) 2016 Jamie Rees -// File: PlexAvailabilityChecker.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 NLog; - -using PlexRequests.Api.Interfaces; -using PlexRequests.Core; -using PlexRequests.Core.SettingModels; -using PlexRequests.Helpers; -using PlexRequests.Services.Interfaces; -using PlexRequests.Api.Models.Movie; -using System.Linq; - -namespace PlexRequests.Services -{ - public class CouchPotatoCacher : ICouchPotatoCacher - { - public CouchPotatoCacher(ISettingsService cpSettings, ICouchPotatoApi cpApi, ICacheProvider cache) - { - CpSettings = cpSettings; - CpApi = cpApi; - Cache = cache; - } - - private ISettingsService CpSettings { get; } - private ICacheProvider Cache { get; } - private ICouchPotatoApi CpApi { get; } - - private static Logger Log = LogManager.GetCurrentClassLogger(); - - public void Queued(long check) - { - Log.Trace("This is check no. {0}", check); - Log.Trace("Getting the settings"); - - var settings = CpSettings.GetSettings(); - if (settings.Enabled) - { - Log.Trace("Getting all movies from CouchPotato"); - var movies = CpApi.GetMovies(settings.FullUri, settings.ApiKey, new[] { "active" }); - Cache.Set(CacheKeys.CouchPotatoQueued, movies, 10); - } - } - - // we do not want to set here... - public int[] QueuedIds() - { - var movies = Cache.Get(CacheKeys.CouchPotatoQueued); - return movies != null ? movies.movies.Select(x => x.info.tmdb_id).ToArray() : new int[] { }; - } - } +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: PlexAvailabilityChecker.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.Linq; + +using NLog; + +using PlexRequests.Api.Interfaces; +using PlexRequests.Api.Models.Movie; +using PlexRequests.Core; +using PlexRequests.Core.SettingModels; +using PlexRequests.Helpers; +using PlexRequests.Services.Interfaces; + +using Quartz; + +namespace PlexRequests.Services.Jobs +{ + public class CouchPotatoCacher : IJob, ICouchPotatoCacher + { + public CouchPotatoCacher(ISettingsService cpSettings, ICouchPotatoApi cpApi, ICacheProvider cache) + { + CpSettings = cpSettings; + CpApi = cpApi; + Cache = cache; + } + + private ISettingsService CpSettings { get; } + private ICacheProvider Cache { get; } + private ICouchPotatoApi CpApi { get; } + + private static Logger Log = LogManager.GetCurrentClassLogger(); + + public void Queued() + { + Log.Trace("Getting the settings"); + + var settings = CpSettings.GetSettings(); + if (settings.Enabled) + { + Log.Trace("Getting all movies from CouchPotato"); + var movies = CpApi.GetMovies(settings.FullUri, settings.ApiKey, new[] { "active" }); + Cache.Set(CacheKeys.CouchPotatoQueued, movies, 10); + } + } + + // we do not want to set here... + public int[] QueuedIds() + { + var movies = Cache.Get(CacheKeys.CouchPotatoQueued); + return movies?.movies.Select(x => x.info.tmdb_id).ToArray() ?? new int[] { }; + } + + public void Execute(IJobExecutionContext context) + { + Queued(); + } + } } \ No newline at end of file diff --git a/PlexRequests.Services/PlexAvailabilityChecker.cs b/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs similarity index 95% rename from PlexRequests.Services/PlexAvailabilityChecker.cs rename to PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs index c4caa18a0..bb8f957a0 100644 --- a/PlexRequests.Services/PlexAvailabilityChecker.cs +++ b/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs @@ -1,297 +1,303 @@ -#region Copyright -// /************************************************************************ -// Copyright (c) 2016 Jamie Rees -// File: PlexAvailabilityChecker.cs -// Created By: Jamie Rees -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// ************************************************************************/ -#endregion -using System; -using System.Collections.Generic; -using System.Linq; - -using NLog; - -using PlexRequests.Api.Interfaces; -using PlexRequests.Api.Models.Plex; -using PlexRequests.Core; -using PlexRequests.Core.SettingModels; -using PlexRequests.Helpers; -using PlexRequests.Services.Interfaces; -using PlexRequests.Store; -using PlexRequests.Services.Models; - -namespace PlexRequests.Services -{ - public class PlexAvailabilityChecker : IAvailabilityChecker - { - public PlexAvailabilityChecker(ISettingsService plexSettings, ISettingsService auth, IRequestService request, IPlexApi plex, ICacheProvider cache) - { - Plex = plexSettings; - Auth = auth; - RequestService = request; - PlexApi = plex; - Cache = cache; - } - - private ISettingsService Plex { get; } - private ISettingsService Auth { get; } - private IRequestService RequestService { get; } - private static Logger Log = LogManager.GetCurrentClassLogger(); - private IPlexApi PlexApi { get; } - private ICacheProvider Cache { get; } - - public void CheckAndUpdateAll(long check) - { - Log.Trace("This is check no. {0}", check); - Log.Trace("Getting the settings"); - var plexSettings = Plex.GetSettings(); - var authSettings = Auth.GetSettings(); - Log.Trace("Getting all the requests"); - - if (!ValidateSettings(plexSettings, authSettings)) - { - Log.Info("Validation of the plex settings failed."); - return; - } - - var libraries = CachedLibraries(authSettings, plexSettings, true); //force setting the cache (10 min intervals via scheduler) - var movies = GetPlexMovies().ToArray(); - var shows = GetPlexTvShows().ToArray(); - var albums = GetPlexAlbums().ToArray(); - - var requests = RequestService.GetAll(); - var requestedModels = requests as RequestedModel[] ?? requests.Where(x => !x.Available).ToArray(); - Log.Trace("Requests Count {0}", requestedModels.Length); - - if (!requestedModels.Any()) - { - Log.Info("There are no requests to check."); - return; - } - - var modifiedModel = new List(); - foreach (var r in requestedModels) - { - Log.Trace("We are going to see if Plex has the following title: {0}", r.Title); - - if (libraries == null) - { - libraries = new List() { PlexApi.SearchContent(authSettings.PlexAuthToken, r.Title, plexSettings.FullUri) }; - if (libraries == null) - { - Log.Trace("Could not find any matching result for this title."); - continue; - } - } - - Log.Trace("Search results from Plex for the following request: {0}", r.Title); - //Log.Trace(results.DumpJson()); - - var releaseDate = r.ReleaseDate == DateTime.MinValue ? string.Empty : r.ReleaseDate.ToString("yyyy"); - - bool matchResult; - switch (r.Type) - { - case RequestType.Movie: - matchResult = IsMovieAvailable(movies, r.Title, releaseDate); - break; - case RequestType.TvShow: - matchResult = IsTvShowAvailable(shows, r.Title, releaseDate); - break; - case RequestType.Album: - matchResult = IsAlbumAvailable(albums, r.Title, r.ReleaseDate.Year.ToString(), r.ArtistName); - break; - default: - throw new ArgumentOutOfRangeException(); - } - - if (matchResult) - { - r.Available = true; - modifiedModel.Add(r); - continue; - } - - Log.Trace("The result from Plex where the title's match was null, so that means the content is not yet in Plex."); - } - - Log.Trace("Updating the requests now"); - Log.Trace("Requests that will be updates:"); - Log.Trace(modifiedModel.SelectMany(x => x.Title).DumpJson()); - - if (modifiedModel.Any()) - { - RequestService.BatchUpdate(modifiedModel); - } - } - - public List GetPlexMovies() - { - var movies = new List(); - var libs = Cache.Get>(CacheKeys.PlexLibaries); - if (libs != null) - { - var movieLibs = libs.Where(x => - x.Video.Any(y => - y.Type.Equals(PlexMediaType.Movie.ToString(), StringComparison.CurrentCultureIgnoreCase) - ) - ).ToArray(); - - foreach (var lib in movieLibs) - { - movies.AddRange(lib.Video.Select(x => new PlexMovie() // movies are in the Video list - { - Title = x.Title, - ReleaseYear = x.Year - })); - } - } - return movies; - } - - public bool IsMovieAvailable(PlexMovie[] plexMovies, string title, string year) - { - return plexMovies.Any(x => x.Title.Equals(title, StringComparison.CurrentCultureIgnoreCase) && x.ReleaseYear.Equals(year, StringComparison.CurrentCultureIgnoreCase)); - } - - public List GetPlexTvShows() - { - var shows = new List(); - var libs = Cache.Get>(CacheKeys.PlexLibaries); - if (libs != null) - { - var tvLibs = libs.Where(x => - x.Directory.Any(y => - y.Type.Equals(PlexMediaType.Show.ToString(), StringComparison.CurrentCultureIgnoreCase) - ) - ).ToArray(); - - foreach (var lib in tvLibs) - { - shows.AddRange(lib.Directory.Select(x => new PlexTvShow() // shows are in the directory list - { - Title = x.Title, - ReleaseYear = x.Year - })); - } - } - return shows; - } - - public bool IsTvShowAvailable(PlexTvShow[] plexShows, string title, string year) - { - return plexShows.Any(x => - (x.Title.Equals(title, StringComparison.CurrentCultureIgnoreCase) || x.Title.StartsWith(title, StringComparison.CurrentCultureIgnoreCase)) && - x.ReleaseYear.Equals(year, StringComparison.CurrentCultureIgnoreCase)); - } - - public List GetPlexAlbums() - { - var albums = new List(); - var libs = Cache.Get>(CacheKeys.PlexLibaries); - if (libs != null) - { - var albumLibs = libs.Where(x => - x.Directory.Any(y => - y.Type.Equals(PlexMediaType.Show.ToString(), StringComparison.CurrentCultureIgnoreCase) - ) - ).ToArray(); - - foreach (var lib in albumLibs) - { - albums.AddRange(lib.Directory.Select(x => new PlexAlbum() - { - Title = x.Title, - ReleaseYear = x.Year, - Artist = x.ParentTitle - })); - } - } - return albums; - } - - public bool IsAlbumAvailable(PlexAlbum[] plexAlbums, string title, string year, string artist) - { - return plexAlbums.Any(x => - x.Title.Contains(title) && - //x.ReleaseYear.Equals(year, StringComparison.CurrentCultureIgnoreCase) && - x.Artist.Equals(artist, StringComparison.CurrentCultureIgnoreCase)); - } - - private List CachedLibraries(AuthenticationSettings authSettings, PlexSettings plexSettings, bool setCache) - { - Log.Trace("Obtaining library sections from Plex for the following request"); - - List results = new List(); - - if (!ValidateSettings(plexSettings, authSettings)) - { - Log.Warn("The settings are not configured"); - return results; // don't error out here, just let it go! - } - - if (setCache) - { - results = GetLibraries(authSettings, plexSettings); - Cache.Set(CacheKeys.PlexLibaries, results, 10); - } - else - { - results = Cache.GetOrSet(CacheKeys.PlexLibaries, () => { - return GetLibraries(authSettings, plexSettings); - }, 10); - } - return results; - } - - private List GetLibraries(AuthenticationSettings authSettings, PlexSettings plexSettings) - { - var sections = PlexApi.GetLibrarySections(authSettings.PlexAuthToken, plexSettings.FullUri); - - List libs = new List(); - if (sections != null) - { - foreach (var dir in sections.Directories) - { - Log.Trace("Obtaining results from Plex for the following library section: {0}", dir.Title); - var lib = PlexApi.GetLibrary(authSettings.PlexAuthToken, plexSettings.FullUri, dir.Key); - if (lib != null) - { - libs.Add(lib); - } - } - } - - return libs; - } - - private bool ValidateSettings(PlexSettings plex, AuthenticationSettings auth) - { - if (plex?.Ip == null || auth?.PlexAuthToken == null) - { - Log.Warn("A setting is null, Ensure Plex is configured correctly, and we have a Plex Auth token."); - return false; - } - return true; - } - } +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: PlexAvailabilityChecker.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion +using System; +using System.Collections.Generic; +using System.Linq; + +using NLog; + +using PlexRequests.Api.Interfaces; +using PlexRequests.Api.Models.Plex; +using PlexRequests.Core; +using PlexRequests.Core.SettingModels; +using PlexRequests.Helpers; +using PlexRequests.Services.Interfaces; +using PlexRequests.Services.Models; +using PlexRequests.Store; + +using Quartz; + +namespace PlexRequests.Services.Jobs +{ + public class PlexAvailabilityChecker : IJob, IAvailabilityChecker + { + public PlexAvailabilityChecker(ISettingsService plexSettings, ISettingsService auth, IRequestService request, IPlexApi plex, ICacheProvider cache) + { + Plex = plexSettings; + Auth = auth; + RequestService = request; + PlexApi = plex; + Cache = cache; + } + + private ISettingsService Plex { get; } + private ISettingsService Auth { get; } + private IRequestService RequestService { get; } + private static Logger Log = LogManager.GetCurrentClassLogger(); + private IPlexApi PlexApi { get; } + private ICacheProvider Cache { get; } + + public void CheckAndUpdateAll() + { + Log.Trace("Getting the settings"); + var plexSettings = Plex.GetSettings(); + var authSettings = Auth.GetSettings(); + Log.Trace("Getting all the requests"); + + if (!ValidateSettings(plexSettings, authSettings)) + { + Log.Info("Validation of the plex settings failed."); + return; + } + + var libraries = CachedLibraries(authSettings, plexSettings, true); //force setting the cache (10 min intervals via scheduler) + var movies = GetPlexMovies().ToArray(); + var shows = GetPlexTvShows().ToArray(); + var albums = GetPlexAlbums().ToArray(); + + var requests = RequestService.GetAll(); + var requestedModels = requests as RequestedModel[] ?? requests.Where(x => !x.Available).ToArray(); + Log.Trace("Requests Count {0}", requestedModels.Length); + + if (!requestedModels.Any()) + { + Log.Info("There are no requests to check."); + return; + } + + var modifiedModel = new List(); + foreach (var r in requestedModels) + { + Log.Trace("We are going to see if Plex has the following title: {0}", r.Title); + + if (libraries == null) + { + libraries = new List() { PlexApi.SearchContent(authSettings.PlexAuthToken, r.Title, plexSettings.FullUri) }; + if (libraries == null) + { + Log.Trace("Could not find any matching result for this title."); + continue; + } + } + + Log.Trace("Search results from Plex for the following request: {0}", r.Title); + //Log.Trace(results.DumpJson()); + + var releaseDate = r.ReleaseDate == DateTime.MinValue ? string.Empty : r.ReleaseDate.ToString("yyyy"); + + bool matchResult; + switch (r.Type) + { + case RequestType.Movie: + matchResult = IsMovieAvailable(movies, r.Title, releaseDate); + break; + case RequestType.TvShow: + matchResult = IsTvShowAvailable(shows, r.Title, releaseDate); + break; + case RequestType.Album: + matchResult = IsAlbumAvailable(albums, r.Title, r.ReleaseDate.Year.ToString(), r.ArtistName); + break; + default: + throw new ArgumentOutOfRangeException(); + } + + if (matchResult) + { + r.Available = true; + modifiedModel.Add(r); + continue; + } + + Log.Trace("The result from Plex where the title's match was null, so that means the content is not yet in Plex."); + } + + Log.Trace("Updating the requests now"); + Log.Trace("Requests that will be updates:"); + Log.Trace(modifiedModel.SelectMany(x => x.Title).DumpJson()); + + if (modifiedModel.Any()) + { + RequestService.BatchUpdate(modifiedModel); + } + } + + public List GetPlexMovies() + { + var movies = new List(); + var libs = Cache.Get>(CacheKeys.PlexLibaries); + if (libs != null) + { + var movieLibs = libs.Where(x => + x.Video.Any(y => + y.Type.Equals(PlexMediaType.Movie.ToString(), StringComparison.CurrentCultureIgnoreCase) + ) + ).ToArray(); + + foreach (var lib in movieLibs) + { + movies.AddRange(lib.Video.Select(x => new PlexMovie() // movies are in the Video list + { + Title = x.Title, + ReleaseYear = x.Year + })); + } + } + return movies; + } + + public bool IsMovieAvailable(PlexMovie[] plexMovies, string title, string year) + { + return plexMovies.Any(x => x.Title.Equals(title, StringComparison.CurrentCultureIgnoreCase) && x.ReleaseYear.Equals(year, StringComparison.CurrentCultureIgnoreCase)); + } + + public List GetPlexTvShows() + { + var shows = new List(); + var libs = Cache.Get>(CacheKeys.PlexLibaries); + if (libs != null) + { + var tvLibs = libs.Where(x => + x.Directory.Any(y => + y.Type.Equals(PlexMediaType.Show.ToString(), StringComparison.CurrentCultureIgnoreCase) + ) + ).ToArray(); + + foreach (var lib in tvLibs) + { + shows.AddRange(lib.Directory.Select(x => new PlexTvShow() // shows are in the directory list + { + Title = x.Title, + ReleaseYear = x.Year + })); + } + } + return shows; + } + + public bool IsTvShowAvailable(PlexTvShow[] plexShows, string title, string year) + { + return plexShows.Any(x => + (x.Title.Equals(title, StringComparison.CurrentCultureIgnoreCase) || x.Title.StartsWith(title, StringComparison.CurrentCultureIgnoreCase)) && + x.ReleaseYear.Equals(year, StringComparison.CurrentCultureIgnoreCase)); + } + + public List GetPlexAlbums() + { + var albums = new List(); + var libs = Cache.Get>(CacheKeys.PlexLibaries); + if (libs != null) + { + var albumLibs = libs.Where(x => + x.Directory.Any(y => + y.Type.Equals(PlexMediaType.Show.ToString(), StringComparison.CurrentCultureIgnoreCase) + ) + ).ToArray(); + + foreach (var lib in albumLibs) + { + albums.AddRange(lib.Directory.Select(x => new PlexAlbum() + { + Title = x.Title, + ReleaseYear = x.Year, + Artist = x.ParentTitle + })); + } + } + return albums; + } + + public bool IsAlbumAvailable(PlexAlbum[] plexAlbums, string title, string year, string artist) + { + return plexAlbums.Any(x => + x.Title.Contains(title) && + //x.ReleaseYear.Equals(year, StringComparison.CurrentCultureIgnoreCase) && + x.Artist.Equals(artist, StringComparison.CurrentCultureIgnoreCase)); + } + + private List CachedLibraries(AuthenticationSettings authSettings, PlexSettings plexSettings, bool setCache) + { + Log.Trace("Obtaining library sections from Plex for the following request"); + + List results = new List(); + + if (!ValidateSettings(plexSettings, authSettings)) + { + Log.Warn("The settings are not configured"); + return results; // don't error out here, just let it go! + } + + if (setCache) + { + results = GetLibraries(authSettings, plexSettings); + Cache.Set(CacheKeys.PlexLibaries, results, 10); + } + else + { + results = Cache.GetOrSet(CacheKeys.PlexLibaries, () => { + return GetLibraries(authSettings, plexSettings); + }, 10); + } + return results; + } + + private List GetLibraries(AuthenticationSettings authSettings, PlexSettings plexSettings) + { + var sections = PlexApi.GetLibrarySections(authSettings.PlexAuthToken, plexSettings.FullUri); + + List libs = new List(); + if (sections != null) + { + foreach (var dir in sections.Directories) + { + Log.Trace("Obtaining results from Plex for the following library section: {0}", dir.Title); + var lib = PlexApi.GetLibrary(authSettings.PlexAuthToken, plexSettings.FullUri, dir.Key); + if (lib != null) + { + libs.Add(lib); + } + } + } + + return libs; + } + + private bool ValidateSettings(PlexSettings plex, AuthenticationSettings auth) + { + if (plex?.Ip == null || auth?.PlexAuthToken == null) + { + Log.Warn("A setting is null, Ensure Plex is configured correctly, and we have a Plex Auth token."); + return false; + } + return true; + } + + public void Execute(IJobExecutionContext context) + { + CheckAndUpdateAll(); + } + } } \ No newline at end of file diff --git a/PlexRequests.Services/SickRageCacher.cs b/PlexRequests.Services/Jobs/SickRageCacher.cs similarity index 90% rename from PlexRequests.Services/SickRageCacher.cs rename to PlexRequests.Services/Jobs/SickRageCacher.cs index 9281cdcf2..3c626a336 100644 --- a/PlexRequests.Services/SickRageCacher.cs +++ b/PlexRequests.Services/Jobs/SickRageCacher.cs @@ -1,78 +1,82 @@ -#region Copyright -// /************************************************************************ -// Copyright (c) 2016 Jamie Rees -// File: PlexAvailabilityChecker.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 NLog; - -using PlexRequests.Api.Interfaces; -using PlexRequests.Core; -using PlexRequests.Core.SettingModels; -using PlexRequests.Helpers; -using PlexRequests.Services.Interfaces; -using PlexRequests.Api.Models.Movie; -using System.Linq; -using PlexRequests.Api.Models.SickRage; - -namespace PlexRequests.Services -{ - public class SickRageCacher : ISickRageCacher - { - public SickRageCacher(ISettingsService srSettings, ISickRageApi srApi, ICacheProvider cache) - { - SrSettings = srSettings; - SrApi = srApi; - Cache = cache; - } - - private ISettingsService SrSettings { get; } - private ICacheProvider Cache { get; } - private ISickRageApi SrApi { get; } - - private static Logger Log = LogManager.GetCurrentClassLogger(); - - public void Queued(long check) - { - Log.Trace("This is check no. {0}", check); - Log.Trace("Getting the settings"); - - var settings = SrSettings.GetSettings(); - if (settings.Enabled) - { - Log.Trace("Getting all shows from SickRage"); - var movies = SrApi.GetShows(settings.ApiKey, settings.FullUri); - Cache.Set(CacheKeys.SickRageQueued, movies.Result); - } - } - - // we do not want to set here... - public int[] QueuedIds() - { - var tv = Cache.Get(CacheKeys.SickRageQueued); - return tv?.data.Values.Select(x => x.tvdbid).ToArray() ?? new int[] { }; - } - } +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: PlexAvailabilityChecker.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.Linq; + +using NLog; + +using PlexRequests.Api.Interfaces; +using PlexRequests.Api.Models.SickRage; +using PlexRequests.Core; +using PlexRequests.Core.SettingModels; +using PlexRequests.Helpers; +using PlexRequests.Services.Interfaces; + +using Quartz; + +namespace PlexRequests.Services.Jobs +{ + public class SickRageCacher : IJob, ISickRageCacher + { + public SickRageCacher(ISettingsService srSettings, ISickRageApi srApi, ICacheProvider cache) + { + SrSettings = srSettings; + SrApi = srApi; + Cache = cache; + } + + private ISettingsService SrSettings { get; } + private ICacheProvider Cache { get; } + private ISickRageApi SrApi { get; } + + private static Logger Log = LogManager.GetCurrentClassLogger(); + + public void Queued() + { + Log.Trace("Getting the settings"); + + var settings = SrSettings.GetSettings(); + if (settings.Enabled) + { + Log.Trace("Getting all shows from SickRage"); + var movies = SrApi.GetShows(settings.ApiKey, settings.FullUri); + Cache.Set(CacheKeys.SickRageQueued, movies.Result); + } + } + + // we do not want to set here... + public int[] QueuedIds() + { + var tv = Cache.Get(CacheKeys.SickRageQueued); + return tv?.data.Values.Select(x => x.tvdbid).ToArray() ?? new int[] { }; + } + + public void Execute(IJobExecutionContext context) + { + Queued(); + } + } } \ No newline at end of file diff --git a/PlexRequests.Services/SonarrCacher.cs b/PlexRequests.Services/Jobs/SonarrCacher.cs similarity index 91% rename from PlexRequests.Services/SonarrCacher.cs rename to PlexRequests.Services/Jobs/SonarrCacher.cs index 34d00b8c3..63e2425d1 100644 --- a/PlexRequests.Services/SonarrCacher.cs +++ b/PlexRequests.Services/Jobs/SonarrCacher.cs @@ -1,77 +1,83 @@ -#region Copyright -// /************************************************************************ -// Copyright (c) 2016 Jamie Rees -// File: PlexAvailabilityChecker.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 NLog; - -using PlexRequests.Api.Interfaces; -using PlexRequests.Core; -using PlexRequests.Core.SettingModels; -using PlexRequests.Helpers; -using PlexRequests.Services.Interfaces; -using System.Linq; -using System.Collections.Generic; -using PlexRequests.Api.Models.Sonarr; - -namespace PlexRequests.Services -{ - public class SonarrCacher : ISonarrCacher - { - public SonarrCacher(ISettingsService sonarrSettings, ISonarrApi sonarrApi, ICacheProvider cache) - { - SonarrSettings = sonarrSettings; - SonarrApi = sonarrApi; - Cache = cache; - } - - private ISettingsService SonarrSettings { get; } - private ICacheProvider Cache { get; } - private ISonarrApi SonarrApi { get; } - - private static Logger Log = LogManager.GetCurrentClassLogger(); - - public void Queued(long check) - { - Log.Trace("This is check no. {0}", check); - Log.Trace("Getting the settings"); - - var settings = SonarrSettings.GetSettings(); - if (settings.Enabled) - { - Log.Trace("Getting all tv series from Sonarr"); - var series = SonarrApi.GetSeries(settings.ApiKey, settings.FullUri); - Cache.Set(CacheKeys.SonarrQueued, series, 10); - } - } - - // we do not want to set here... - public int[] QueuedIds() - { - var series = Cache.Get>(CacheKeys.SonarrQueued); - return series?.Select(x => x.tvdbId).ToArray() ?? new int[] { }; - } - } +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: PlexAvailabilityChecker.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.Collections.Generic; +using System.Linq; + +using NLog; + +using PlexRequests.Api.Interfaces; +using PlexRequests.Api.Models.Sonarr; +using PlexRequests.Core; +using PlexRequests.Core.SettingModels; +using PlexRequests.Helpers; +using PlexRequests.Services.Interfaces; + +using Quartz; + +namespace PlexRequests.Services.Jobs +{ + public class SonarrCacher : IJob, ISonarrCacher + { + public SonarrCacher(ISettingsService sonarrSettings, ISonarrApi sonarrApi, ICacheProvider cache) + { + SonarrSettings = sonarrSettings; + SonarrApi = sonarrApi; + Cache = cache; + } + + private ISettingsService SonarrSettings { get; } + private ICacheProvider Cache { get; } + private ISonarrApi SonarrApi { get; } + + private static Logger Log = LogManager.GetCurrentClassLogger(); + + public void Queued() + { + Log.Trace("Getting the settings"); + + var settings = SonarrSettings.GetSettings(); + if (settings.Enabled) + { + Log.Trace("Getting all tv series from Sonarr"); + var series = SonarrApi.GetSeries(settings.ApiKey, settings.FullUri); + Cache.Set(CacheKeys.SonarrQueued, series, 10); + } + } + + // we do not want to set here... + public int[] QueuedIds() + { + var series = Cache.Get>(CacheKeys.SonarrQueued); + return series?.Select(x => x.tvdbId).ToArray() ?? new int[] { }; + } + + public void Execute(IJobExecutionContext context) + { + Queued(); + } + } } \ No newline at end of file diff --git a/PlexRequests.Services/MediaCacheService.cs b/PlexRequests.Services/MediaCacheService.cs deleted file mode 100644 index 48799f5b6..000000000 --- a/PlexRequests.Services/MediaCacheService.cs +++ /dev/null @@ -1,102 +0,0 @@ -#region Copyright -// /************************************************************************ -// Copyright (c) 2016 Jamie Rees -// File: AvailabilityUpdateService.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.Reactive.Linq; -using System.Web.Hosting; - -using FluentScheduler; - -using Mono.Data.Sqlite; - -using NLog; - -using PlexRequests.Api; -using PlexRequests.Core; -using PlexRequests.Core.SettingModels; -using PlexRequests.Helpers; -using PlexRequests.Services.Interfaces; -using PlexRequests.Store; -using PlexRequests.Store.Repository; -using System.Threading.Tasks; - -namespace PlexRequests.Services -{ - public class MediaCacheService : ITask, IRegisteredObject, IAvailabilityUpdateService - { - public MediaCacheService() - { - var memCache = new MemoryCacheProvider(); - var dbConfig = new DbConfiguration(new SqliteFactory()); - var repo = new SettingsJsonRepository(dbConfig, memCache); - - ConfigurationReader = new ConfigurationReader(); - CpCacher = new CouchPotatoCacher(new SettingsServiceV2(repo), new CouchPotatoApi(), memCache); - SonarrCacher = new SonarrCacher(new SettingsServiceV2(repo), new SonarrApi(), memCache); - SickRageCacher = new SickRageCacher(new SettingsServiceV2(repo), new SickrageApi(), memCache); - HostingEnvironment.RegisterObject(this); - } - - private static Logger Log = LogManager.GetCurrentClassLogger(); - - private IConfigurationReader ConfigurationReader { get; } - private ICouchPotatoCacher CpCacher { get; } - private ISonarrCacher SonarrCacher { get; } - private ISickRageCacher SickRageCacher { get; } - private IDisposable CpSubscription { get; set; } - private IDisposable SonarrSubscription { get; set; } - private IDisposable SickRageSubscription { get; set; } - - public void Start(Configuration c) - { - CpSubscription?.Dispose(); - SonarrSubscription?.Dispose(); - SickRageSubscription?.Dispose(); - - Task.Factory.StartNew(() => CpCacher.Queued(-1)); - Task.Factory.StartNew(() => SonarrCacher.Queued(-1)); - Task.Factory.StartNew(() => SickRageCacher.Queued(-1)); - CpSubscription = Observable.Interval(c.Intervals.Notification).Subscribe(CpCacher.Queued); - SonarrSubscription = Observable.Interval(c.Intervals.Notification).Subscribe(SonarrCacher.Queued); - SickRageSubscription = Observable.Interval(c.Intervals.Notification).Subscribe(SickRageCacher.Queued); - } - - public void Execute() - { - Start(ConfigurationReader.Read()); - } - - public void Stop(bool immediate) - { - HostingEnvironment.UnregisterObject(this); - } - } - - public interface ICouchPotatoCacheService - { - void Start(Configuration c); - } -} diff --git a/PlexRequests.Services/PlexRequests.Services.csproj b/PlexRequests.Services/PlexRequests.Services.csproj index 3afb8086f..49650d33e 100644 --- a/PlexRequests.Services/PlexRequests.Services.csproj +++ b/PlexRequests.Services/PlexRequests.Services.csproj @@ -35,8 +35,12 @@ ..\packages\MimeKit.1.2.22\lib\net45\BouncyCastle.dll True - - ..\packages\FluentScheduler.3.1.46\lib\net40\FluentScheduler.dll + + ..\packages\Common.Logging.3.0.0\lib\net40\Common.Logging.dll + True + + + ..\packages\Common.Logging.Core.3.0.0\lib\net40\Common.Logging.Core.dll True @@ -56,6 +60,10 @@ ..\packages\NLog.4.2.3\lib\net45\NLog.dll True + + ..\packages\Quartz.2.3.3\lib\net40\Quartz.dll + True + @@ -84,20 +92,17 @@ + + + + - - - + - - - - - @@ -107,11 +112,7 @@ - - - - diff --git a/PlexRequests.Services/PlexType.cs b/PlexRequests.Services/PlexType.cs deleted file mode 100644 index 27cd0d1da..000000000 --- a/PlexRequests.Services/PlexType.cs +++ /dev/null @@ -1,35 +0,0 @@ -#region Copyright -// /************************************************************************ -// Copyright (c) 2016 Jamie Rees -// File: PlexType.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 PlexRequests.Services -{ - public enum PlexType - { - Movie, - TvShow, - Music - } -} \ No newline at end of file diff --git a/PlexRequests.Services/UpdateInterval.cs b/PlexRequests.Services/UpdateInterval.cs deleted file mode 100644 index 92045563a..000000000 --- a/PlexRequests.Services/UpdateInterval.cs +++ /dev/null @@ -1,38 +0,0 @@ -#region Copyright -// /************************************************************************ -// Copyright (c) 2016 Jamie Rees -// File: UpdateInterval.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 PlexRequests.Services.Interfaces; - -namespace PlexRequests.Services -{ - public class UpdateInterval : IIntervals - { - public TimeSpan Notification => TimeSpan.FromMinutes(10); - - } -} \ No newline at end of file diff --git a/PlexRequests.Services/app.config b/PlexRequests.Services/app.config index 0404cdcb4..44b249bff 100644 --- a/PlexRequests.Services/app.config +++ b/PlexRequests.Services/app.config @@ -1,11 +1,11 @@ - + - - + + - + diff --git a/PlexRequests.Services/packages.config b/PlexRequests.Services/packages.config index 111fbdb40..41673ca47 100644 --- a/PlexRequests.Services/packages.config +++ b/PlexRequests.Services/packages.config @@ -1,9 +1,11 @@  - + + + diff --git a/PlexRequests.UI/Bootstrapper.cs b/PlexRequests.UI/Bootstrapper.cs index 48aeb8290..fc9908b06 100644 --- a/PlexRequests.UI/Bootstrapper.cs +++ b/PlexRequests.UI/Bootstrapper.cs @@ -52,6 +52,11 @@ using PlexRequests.Store.Repository; using PlexRequests.UI.Helpers; using Nancy.Json; +using PlexRequests.Services.Jobs; +using PlexRequests.UI.Jobs; + +using Quartz.Spi; + namespace PlexRequests.UI { public class Bootstrapper : DefaultNancyBootstrapper @@ -90,8 +95,7 @@ namespace PlexRequests.UI container.Register(); container.Register(); container.Register(); - container.Register(); - container.Register(); + container.Register(); // Api's container.Register(); @@ -105,7 +109,7 @@ namespace PlexRequests.UI // NotificationService container.Register().AsSingleton(); - + JsonSettings.MaxJsonLength = int.MaxValue; SubscribeAllObservers(container); diff --git a/PlexRequests.UI/Helpers/ServiceLocator.cs b/PlexRequests.UI/Helpers/ServiceLocator.cs index 860cd150b..40e82e434 100644 --- a/PlexRequests.UI/Helpers/ServiceLocator.cs +++ b/PlexRequests.UI/Helpers/ServiceLocator.cs @@ -24,11 +24,13 @@ // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // ************************************************************************/ #endregion +using System; + using Nancy.TinyIoc; namespace PlexRequests.UI.Helpers { - public class ServiceLocator + public class ServiceLocator : IServiceLocator { static ServiceLocator() { @@ -46,5 +48,16 @@ namespace PlexRequests.UI.Helpers { return Container?.Resolve(); } + + public object Resolve(Type type) + { + return Container.Resolve(type); + } + } + + public interface IServiceLocator + { + T Resolve() where T : class; + object Resolve(Type type); } } \ No newline at end of file diff --git a/PlexRequests.UI/Jobs/CustomJobFactory.cs b/PlexRequests.UI/Jobs/CustomJobFactory.cs new file mode 100644 index 000000000..0337999cb --- /dev/null +++ b/PlexRequests.UI/Jobs/CustomJobFactory.cs @@ -0,0 +1,94 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: CustomJobFactory.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 NLog; + +using PlexRequests.UI.Helpers; + +using Quartz; +using Quartz.Spi; + +namespace PlexRequests.UI.Jobs +{ + /// + /// The custom job factory we are using so we are able to use our IoC container with DI in our Jobs. + /// + /// + public class CustomJobFactory : IJobFactory + { + private static readonly Logger Log = LogManager.GetCurrentClassLogger(); + private IServiceLocator Locator => ServiceLocator.Instance; + + /// + /// Called by the scheduler at the time of the trigger firing, in order to + /// produce a instance on which to call Execute. + /// This will use the to resolve all dependencies of a job. + /// + /// The TriggerFiredBundle from which the + /// and other info relating to the trigger firing can be obtained. + /// a handle to the scheduler that is about to execute the job + /// + /// the newly instantiated Job + /// + /// + /// It should be extremely rare for this method to throw an exception - + /// basically only the case where there is no way at all to instantiate + /// and prepare the Job for execution. When the exception is thrown, the + /// Scheduler will move all triggers associated with the Job into the + /// state, which will require human + /// intervention (e.g. an application restart after fixing whatever + /// configuration problem led to the issue with instantiating the Job). + /// + /// SchedulerException if there is a problem instantiating the Job. + public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler) + { + try + { + var jobDetail = bundle.JobDetail; + var jobType = jobDetail.JobType; + + var resolvedType = (IJob)Locator.Resolve(jobType); + return resolvedType; + } + catch (Exception e) + { + Log.Error(e); + throw; + } + } + + /// + /// Allows the job factory to destroy/clean-up the job if needed. + /// + /// + public void ReturnJob(IJob job) + { + // No need to do anything since our jobs do not implement IDisposable. + } + } +} \ No newline at end of file diff --git a/PlexRequests.UI/Jobs/MediaCacheRegistry.cs b/PlexRequests.UI/Jobs/MediaCacheRegistry.cs deleted file mode 100644 index 3b4b2b68c..000000000 --- a/PlexRequests.UI/Jobs/MediaCacheRegistry.cs +++ /dev/null @@ -1,41 +0,0 @@ -#region Copyright -// /************************************************************************ -// Copyright (c) 2016 Jamie Rees -// File: PlexRegistry.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 FluentScheduler; - -using PlexRequests.Services; - -namespace PlexRequests.UI.Jobs -{ - public class MediaCacheRegistry : Registry - { - public MediaCacheRegistry() - { - Schedule().ToRunNow(); - } - } -} \ No newline at end of file diff --git a/PlexRequests.UI/Jobs/PlexRegistry.cs b/PlexRequests.UI/Jobs/PlexRegistry.cs deleted file mode 100644 index f124cfaf0..000000000 --- a/PlexRequests.UI/Jobs/PlexRegistry.cs +++ /dev/null @@ -1,41 +0,0 @@ -#region Copyright -// /************************************************************************ -// Copyright (c) 2016 Jamie Rees -// File: PlexRegistry.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 FluentScheduler; - -using PlexRequests.Services; - -namespace PlexRequests.UI.Jobs -{ - public class PlexRegistry : Registry - { - public PlexRegistry() - { - Schedule().ToRunNow(); - } - } -} \ No newline at end of file diff --git a/PlexRequests.UI/Jobs/PlexTaskFactory.cs b/PlexRequests.UI/Jobs/PlexTaskFactory.cs deleted file mode 100644 index 5f5b88b89..000000000 --- a/PlexRequests.UI/Jobs/PlexTaskFactory.cs +++ /dev/null @@ -1,23 +0,0 @@ -using FluentScheduler; -using Nancy.TinyIoc; - -using PlexRequests.Services; - -namespace PlexRequests.UI.Jobs -{ - public class PlexTaskFactory : ITaskFactory - { - public ITask GetTaskInstance() where T : ITask - { - //typeof(AvailabilityUpdateService); - var container = TinyIoCContainer.Current; - - var a= container.Resolve(typeof(T)); - - object outT; - container.TryResolve(typeof(T), out outT); - - return (T)a; - } - } -} \ No newline at end of file diff --git a/PlexRequests.UI/Jobs/Scheduler.cs b/PlexRequests.UI/Jobs/Scheduler.cs new file mode 100644 index 000000000..5a91733e1 --- /dev/null +++ b/PlexRequests.UI/Jobs/Scheduler.cs @@ -0,0 +1,137 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: Scheduler.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion +using System; +using System.Collections.Generic; +using System.Linq; + +using NLog; + +using PlexRequests.Services.Jobs; + +using Quartz; +using Quartz.Impl; + +namespace PlexRequests.UI.Jobs +{ + internal sealed class Scheduler + { + private static readonly Logger Log = LogManager.GetCurrentClassLogger(); + + private readonly ISchedulerFactory _factory; + + public Scheduler() + { + _factory = new StdSchedulerFactory(); + } + + private IEnumerable CreateJobs() + { + var jobs = new List(); + + var plex = JobBuilder.Create().WithIdentity("PlexAvailabilityChecker", "Plex").Build(); + var sickrage = JobBuilder.Create().WithIdentity("SickRageCacher", "Cache").Build(); + var sonarr = JobBuilder.Create().WithIdentity("SonarrCacher", "Cache").Build(); + var cp = JobBuilder.Create().WithIdentity("CouchPotatoCacher", "Cache").Build(); + + jobs.Add(plex); + jobs.Add(sickrage); + jobs.Add(sonarr); + jobs.Add(cp); + + return jobs; + } + + /// + /// Starts the scheduler. + /// + /// The jobs do not match the triggers, we should have a trigger per job + public void StartScheduler() + { + var scheduler = _factory.GetScheduler(); + scheduler.JobFactory = new CustomJobFactory(); + scheduler.Start(); + + var jobs = CreateJobs(); + var triggers = CreateTriggers(); + + var jobDetails = jobs as IJobDetail[] ?? jobs.ToArray(); + var triggerDetails = triggers as ITrigger[] ?? triggers.ToArray(); + + if (jobDetails.Length != triggerDetails.Length) + { + Log.Error("The jobs do not match the triggers, we should have a trigger per job"); + throw new InvalidProgramException("The jobs do not match the triggers, we should have a trigger per job"); + } + + for (var i = 0; i < jobDetails.Length; i++) + { + scheduler.ScheduleJob(jobDetails[i], triggerDetails[i]); + } + } + + private IEnumerable CreateTriggers() + { + var triggers = new List(); + + var plexAvailabilityChecker = + TriggerBuilder.Create() + .WithIdentity("PlexAvailabilityChecker", "Plex") + .StartNow() + .WithSimpleSchedule(x => x.WithIntervalInMinutes(10).RepeatForever()) + .Build(); + + var srCacher = + TriggerBuilder.Create() + .WithIdentity("SickRageCacher", "Cache") + .StartNow() + .WithSimpleSchedule(x => x.WithIntervalInMinutes(10).RepeatForever()) + .Build(); + + var sonarrCacher = + TriggerBuilder.Create() + .WithIdentity("SonarrCacher", "Cache") + .StartNow() + .WithSimpleSchedule(x => x.WithIntervalInMinutes(10).RepeatForever()) + .Build(); + + var cpCacher = + TriggerBuilder.Create() + .WithIdentity("CouchPotatoCacher", "Cache") + .StartNow() + .WithSimpleSchedule(x => x.WithIntervalInMinutes(10).RepeatForever()) + .Build(); + + + triggers.Add(plexAvailabilityChecker); + triggers.Add(srCacher); + triggers.Add(sonarrCacher); + triggers.Add(cpCacher); + + return triggers; + } + } +} \ No newline at end of file diff --git a/PlexRequests.UI/PlexRequests.UI.csproj b/PlexRequests.UI/PlexRequests.UI.csproj index 024b63942..176b1b0fe 100644 --- a/PlexRequests.UI/PlexRequests.UI.csproj +++ b/PlexRequests.UI/PlexRequests.UI.csproj @@ -57,12 +57,16 @@ - - ..\packages\Dapper.1.42\lib\net45\Dapper.dll + + ..\packages\Common.Logging.3.0.0\lib\net40\Common.Logging.dll True - - ..\packages\FluentScheduler.3.1.46\lib\net40\FluentScheduler.dll + + ..\packages\Common.Logging.Core.3.0.0\lib\net40\Common.Logging.Core.dll + True + + + ..\packages\Dapper.1.42\lib\net45\Dapper.dll True @@ -137,6 +141,10 @@ ..\packages\Owin.1.0\lib\net40\Owin.dll True + + ..\packages\Quartz.2.3.3\lib\net40\Quartz.dll + True + ..\packages\RestSharp.105.2.3\lib\net452\RestSharp.dll True @@ -170,7 +178,8 @@ - + + @@ -192,8 +201,6 @@ PreserveNewest - - @@ -304,6 +311,9 @@ PreserveNewest + + Designer + Designer diff --git a/PlexRequests.UI/Program.cs b/PlexRequests.UI/Program.cs index a23309721..fb906e349 100644 --- a/PlexRequests.UI/Program.cs +++ b/PlexRequests.UI/Program.cs @@ -40,9 +40,6 @@ using PlexRequests.Store; using PlexRequests.Store.Repository; using System.Diagnostics; -using FluentScheduler; -using PlexRequests.UI.Jobs; - namespace PlexRequests.UI { class Program @@ -54,7 +51,7 @@ namespace PlexRequests.UI var port = -1; if (args.Length > 0) { - for (int i = 0; i < args.Length; i++) + for (var i = 0; i < args.Length; i++) { var arg = args[i].ToLowerInvariant().Substring(1); switch (arg) @@ -101,8 +98,6 @@ namespace PlexRequests.UI { using (WebApp.Start(options)) { - SetupSchedulers(); - Console.WriteLine($"Request Plex is running on the following: http://+:{port}/"); if (Type.GetType("Mono.Runtime") != null) @@ -165,12 +160,5 @@ namespace PlexRequests.UI LoggingHelper.ReconfigureLogLevel(LogLevel.FromOrdinal(logSettings.Level)); } } - - private static void SetupSchedulers() - { - TaskManager.TaskFactory = new PlexTaskFactory(); - TaskManager.Initialize(new PlexRegistry()); - TaskManager.Initialize(new MediaCacheRegistry()); - } } } diff --git a/PlexRequests.UI/Startup.cs b/PlexRequests.UI/Startup.cs index 60a00ac37..a4a3c81cf 100644 --- a/PlexRequests.UI/Startup.cs +++ b/PlexRequests.UI/Startup.cs @@ -25,36 +25,33 @@ // ************************************************************************/ #endregion using System; -using FluentScheduler; using NLog; using Owin; + using PlexRequests.UI.Jobs; -using TaskFactory = FluentScheduler.TaskFactory; namespace PlexRequests.UI { public class Startup { + private static readonly Logger Log = LogManager.GetCurrentClassLogger(); - private static Logger Log = LogManager.GetCurrentClassLogger(); public void Configuration(IAppBuilder app) { try { app.UseNancy(); - + var scheduler = new Scheduler(); + scheduler.StartScheduler(); } catch (Exception exception) { Log.Fatal(exception); throw; } - } - - } -} +} \ No newline at end of file diff --git a/PlexRequests.UI/app.config b/PlexRequests.UI/app.config index c5d2035eb..747d7cb7a 100644 --- a/PlexRequests.UI/app.config +++ b/PlexRequests.UI/app.config @@ -1,50 +1,50 @@ - + -
+
- - + + - + - + - + - - + + - - + + - - + + - + - + diff --git a/PlexRequests.UI/job_scheduling_data_2_0.xsd b/PlexRequests.UI/job_scheduling_data_2_0.xsd new file mode 100644 index 000000000..d1dabc1a9 --- /dev/null +++ b/PlexRequests.UI/job_scheduling_data_2_0.xsd @@ -0,0 +1,361 @@ + + + + + + + Root level node + + + + + + Commands to be executed before scheduling the jobs and triggers in this file. + + + + + Directives to be followed while scheduling the jobs and triggers in this file. + + + + + + + + + + + + + + Version of the XML Schema instance + + + + + + + + + + Delete all jobs, if any, in the identified group. "*" can be used to identify all groups. Will also result in deleting all triggers related to the jobs. + + + + + Delete all triggers, if any, in the identified group. "*" can be used to identify all groups. Will also result in deletion of related jobs that are non-durable. + + + + + Delete the identified job if it exists (will also result in deleting all triggers related to it). + + + + + + + + + + + Delete the identified trigger if it exists (will also result in deletion of related jobs that are non-durable). + + + + + + + + + + + + + + + + Whether the existing scheduling data (with same identifiers) will be overwritten. If false, and ignore-duplicates is not false, and jobs or triggers with the same names already exist as those in the file, an error will occur. + + + + + If true (and overwrite-existing-data is false) then any job/triggers encountered in this file that have names that already exist in the scheduler will be ignored, and no error will be produced. + + + + + If true trigger's start time is calculated based on earlier run time instead of fixed value. Trigger's start time must be undefined for this to work. + + + + + + + + Define a JobDetail + + + + + + + + + + + + + + + + + Define a JobDataMap + + + + + + + + + Define a JobDataMap entry + + + + + + + + + + Define a Trigger + + + + + + + + + + + Common Trigger definitions + + + + + + + + + + + + + + + + + + + + + + + Define a SimpleTrigger + + + + + + + + + + + + + + + + + Define a CronTrigger + + + + + + + + + + + + + + + Define a DateIntervalTrigger + + + + + + + + + + + + + + + + Cron expression (see JavaDoc for examples) + + Special thanks to Chris Thatcher (thatcher@butterfly.net) for the regular expression! + + Regular expressions are not my strong point but I believe this is complete, + with the caveat that order for expressions like 3-0 is not legal but will pass, + and month and day names must be capitalized. + If you want to examine the correctness look for the [\s] to denote the + seperation of individual regular expressions. This is how I break them up visually + to examine them: + + SECONDS: + ( + ((([0-9]|[0-5][0-9])(-([0-9]|[0-5][0-9]))?,)*([0-9]|[0-5][0-9])(-([0-9]|[0-5][0-9]))?) + | (([\*]|[0-9]|[0-5][0-9])/([0-9]|[0-5][0-9])) + | ([\?]) + | ([\*]) + ) [\s] + MINUTES: + ( + ((([0-9]|[0-5][0-9])(-([0-9]|[0-5][0-9]))?,)*([0-9]|[0-5][0-9])(-([0-9]|[0-5][0-9]))?) + | (([\*]|[0-9]|[0-5][0-9])/([0-9]|[0-5][0-9])) + | ([\?]) + | ([\*]) + ) [\s] + HOURS: + ( + ((([0-9]|[0-1][0-9]|[2][0-3])(-([0-9]|[0-1][0-9]|[2][0-3]))?,)*([0-9]|[0-1][0-9]|[2][0-3])(-([0-9]|[0-1][0-9]|[2][0-3]))?) + | (([\*]|[0-9]|[0-1][0-9]|[2][0-3])/([0-9]|[0-1][0-9]|[2][0-3])) + | ([\?]) + | ([\*]) + ) [\s] + DAY OF MONTH: + ( + ((([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1])(-([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1]))?,)*([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1])(-([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1]))?(C)?) + | (([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1])/([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1])(C)?) + | (L(-[0-9])?) + | (L(-[1-2][0-9])?) + | (L(-[3][0-1])?) + | (LW) + | ([1-9]W) + | ([1-3][0-9]W) + | ([\?]) + | ([\*]) + )[\s] + MONTH: + ( + ((([1-9]|0[1-9]|1[0-2])(-([1-9]|0[1-9]|1[0-2]))?,)*([1-9]|0[1-9]|1[0-2])(-([1-9]|0[1-9]|1[0-2]))?) + | (([1-9]|0[1-9]|1[0-2])/([1-9]|0[1-9]|1[0-2])) + | (((JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)(-(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))?,)*(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)(-(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))?) + | ((JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)/(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)) + | ([\?]) + | ([\*]) + )[\s] + DAY OF WEEK: + ( + (([1-7](-([1-7]))?,)*([1-7])(-([1-7]))?) + | ([1-7]/([1-7])) + | (((MON|TUE|WED|THU|FRI|SAT|SUN)(-(MON|TUE|WED|THU|FRI|SAT|SUN))?,)*(MON|TUE|WED|THU|FRI|SAT|SUN)(-(MON|TUE|WED|THU|FRI|SAT|SUN))?(C)?) + | ((MON|TUE|WED|THU|FRI|SAT|SUN)/(MON|TUE|WED|THU|FRI|SAT|SUN)(C)?) + | (([1-7]|(MON|TUE|WED|THU|FRI|SAT|SUN))(L|LW)?) + | (([1-7]|MON|TUE|WED|THU|FRI|SAT|SUN)#([1-7])?) + | ([\?]) + | ([\*]) + ) + YEAR (OPTIONAL): + ( + [\s]? + ([\*])? + | ((19[7-9][0-9])|(20[0-9][0-9]))? + | (((19[7-9][0-9])|(20[0-9][0-9]))/((19[7-9][0-9])|(20[0-9][0-9])))? + | ((((19[7-9][0-9])|(20[0-9][0-9]))(-((19[7-9][0-9])|(20[0-9][0-9])))?,)*((19[7-9][0-9])|(20[0-9][0-9]))(-((19[7-9][0-9])|(20[0-9][0-9])))?)? + ) + + + + + + + + + + Number of times to repeat the Trigger (-1 for indefinite) + + + + + + + + + + Simple Trigger Misfire Instructions + + + + + + + + + + + + + + Cron Trigger Misfire Instructions + + + + + + + + + + + Date Interval Trigger Misfire Instructions + + + + + + + + + + + Interval Units + + + + + + + + + + + + + \ No newline at end of file diff --git a/PlexRequests.UI/packages.config b/PlexRequests.UI/packages.config index 8c2dea7cd..63d5b6c2b 100644 --- a/PlexRequests.UI/packages.config +++ b/PlexRequests.UI/packages.config @@ -1,7 +1,8 @@  + + - @@ -22,6 +23,7 @@ + From 121465210d58a53210eec8cf10c4960349fa3736 Mon Sep 17 00:00:00 2001 From: Drewster727 Date: Tue, 19 Apr 2016 13:41:56 -0500 Subject: [PATCH 07/76] fix the cacher by adding locking + extra logging in the plex checker + use a const key for scheduler caching time --- PlexRequests.Core/CacheKeys.cs | 5 +++++ PlexRequests.Helpers/MemoryCacheProvider.cs | 15 +++++++++++---- PlexRequests.Services/Jobs/CouchPotatoCacher.cs | 2 +- .../Jobs/PlexAvailabilityChecker.cs | 14 ++++++++++---- PlexRequests.Services/Jobs/SickRageCacher.cs | 2 +- PlexRequests.Services/Jobs/SonarrCacher.cs | 2 +- 6 files changed, 29 insertions(+), 11 deletions(-) diff --git a/PlexRequests.Core/CacheKeys.cs b/PlexRequests.Core/CacheKeys.cs index d10bb0bb2..52994db1c 100644 --- a/PlexRequests.Core/CacheKeys.cs +++ b/PlexRequests.Core/CacheKeys.cs @@ -28,6 +28,11 @@ namespace PlexRequests.Core { public class CacheKeys { + public struct TimeFrameMinutes + { + public const int SchedulerCaching = 10; + } + public const string PlexLibaries = "PlexLibaries"; public const string TvDbToken = "TheTvDbApiToken"; diff --git a/PlexRequests.Helpers/MemoryCacheProvider.cs b/PlexRequests.Helpers/MemoryCacheProvider.cs index 6e513502c..fdb78f291 100644 --- a/PlexRequests.Helpers/MemoryCacheProvider.cs +++ b/PlexRequests.Helpers/MemoryCacheProvider.cs @@ -73,8 +73,8 @@ namespace PlexRequests.Helpers /// public T Get(string key) where T : class { - var item = Cache.Get(key) as T; - return item; + lock (key) + return Cache.Get(key) as T; } /// @@ -86,7 +86,11 @@ namespace PlexRequests.Helpers public void Set(string key, object data, int cacheTime = 20) { var policy = new CacheItemPolicy { AbsoluteExpiration = DateTime.Now + TimeSpan.FromMinutes(cacheTime) }; - Cache.Add(new CacheItem(key, data), policy); + lock (key) + { + Cache.Remove(key); + Cache.Add(new CacheItem(key, data), policy); + } } /// @@ -98,7 +102,10 @@ namespace PlexRequests.Helpers var keys = Cache.Where(x => x.Key.Contains(key)); foreach (var k in keys) { - Cache.Remove(k.Key); + lock (key) + { + Cache.Remove(k.Key); + } } } } diff --git a/PlexRequests.Services/Jobs/CouchPotatoCacher.cs b/PlexRequests.Services/Jobs/CouchPotatoCacher.cs index 8bc50c5b6..66723036a 100644 --- a/PlexRequests.Services/Jobs/CouchPotatoCacher.cs +++ b/PlexRequests.Services/Jobs/CouchPotatoCacher.cs @@ -63,7 +63,7 @@ namespace PlexRequests.Services.Jobs { Log.Trace("Getting all movies from CouchPotato"); var movies = CpApi.GetMovies(settings.FullUri, settings.ApiKey, new[] { "active" }); - Cache.Set(CacheKeys.CouchPotatoQueued, movies, 10); + Cache.Set(CacheKeys.CouchPotatoQueued, movies, CacheKeys.TimeFrameMinutes.SchedulerCaching); } } diff --git a/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs b/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs index bb8f957a0..f1288ec55 100644 --- a/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs +++ b/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs @@ -240,7 +240,7 @@ namespace PlexRequests.Services.Jobs private List CachedLibraries(AuthenticationSettings authSettings, PlexSettings plexSettings, bool setCache) { - Log.Trace("Obtaining library sections from Plex for the following request"); + Log.Trace("Obtaining library sections from Plex"); List results = new List(); @@ -252,14 +252,19 @@ namespace PlexRequests.Services.Jobs if (setCache) { - results = GetLibraries(authSettings, plexSettings); - Cache.Set(CacheKeys.PlexLibaries, results, 10); + Log.Trace("Plex Lib API Call"); + results = GetLibraries(authSettings, plexSettings); + + Log.Trace("Plex Lib Cache Set Call"); + Cache.Set(CacheKeys.PlexLibaries, results, CacheKeys.TimeFrameMinutes.SchedulerCaching); } else { + Log.Trace("Plex Lib GetSet Call"); results = Cache.GetOrSet(CacheKeys.PlexLibaries, () => { + Log.Trace("Plex Lib API Call (inside getset)"); return GetLibraries(authSettings, plexSettings); - }, 10); + }, CacheKeys.TimeFrameMinutes.SchedulerCaching); } return results; } @@ -282,6 +287,7 @@ namespace PlexRequests.Services.Jobs } } + Log.Trace("Returning Plex Libs"); return libs; } diff --git a/PlexRequests.Services/Jobs/SickRageCacher.cs b/PlexRequests.Services/Jobs/SickRageCacher.cs index 3c626a336..8b394eec3 100644 --- a/PlexRequests.Services/Jobs/SickRageCacher.cs +++ b/PlexRequests.Services/Jobs/SickRageCacher.cs @@ -63,7 +63,7 @@ namespace PlexRequests.Services.Jobs { Log.Trace("Getting all shows from SickRage"); var movies = SrApi.GetShows(settings.ApiKey, settings.FullUri); - Cache.Set(CacheKeys.SickRageQueued, movies.Result); + Cache.Set(CacheKeys.SickRageQueued, movies.Result, CacheKeys.TimeFrameMinutes.SchedulerCaching); } } diff --git a/PlexRequests.Services/Jobs/SonarrCacher.cs b/PlexRequests.Services/Jobs/SonarrCacher.cs index 63e2425d1..50b238735 100644 --- a/PlexRequests.Services/Jobs/SonarrCacher.cs +++ b/PlexRequests.Services/Jobs/SonarrCacher.cs @@ -64,7 +64,7 @@ namespace PlexRequests.Services.Jobs { Log.Trace("Getting all tv series from Sonarr"); var series = SonarrApi.GetSeries(settings.ApiKey, settings.FullUri); - Cache.Set(CacheKeys.SonarrQueued, series, 10); + Cache.Set(CacheKeys.SonarrQueued, series, CacheKeys.TimeFrameMinutes.SchedulerCaching); } } From 508baeec04bb3a810f4bfbc565e3dc1914f10cf8 Mon Sep 17 00:00:00 2001 From: Drewster727 Date: Tue, 19 Apr 2016 13:55:37 -0500 Subject: [PATCH 08/76] increase the scheduler cache timeframe to avoid losing cache when the remote api endpoints go offline (due to a reboot or some other reason) -- if they're online, the cache will get refreshed every 10 minutes like normal --- PlexRequests.Core/CacheKeys.cs | 2 +- PlexRequests.Services/Jobs/CouchPotatoCacher.cs | 5 ++++- PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs | 5 ++++- PlexRequests.Services/Jobs/SickRageCacher.cs | 7 +++++-- PlexRequests.Services/Jobs/SonarrCacher.cs | 5 ++++- 5 files changed, 18 insertions(+), 6 deletions(-) diff --git a/PlexRequests.Core/CacheKeys.cs b/PlexRequests.Core/CacheKeys.cs index 52994db1c..439953d94 100644 --- a/PlexRequests.Core/CacheKeys.cs +++ b/PlexRequests.Core/CacheKeys.cs @@ -30,7 +30,7 @@ namespace PlexRequests.Core { public struct TimeFrameMinutes { - public const int SchedulerCaching = 10; + public const int SchedulerCaching = 60; } public const string PlexLibaries = "PlexLibaries"; diff --git a/PlexRequests.Services/Jobs/CouchPotatoCacher.cs b/PlexRequests.Services/Jobs/CouchPotatoCacher.cs index 66723036a..2440477f0 100644 --- a/PlexRequests.Services/Jobs/CouchPotatoCacher.cs +++ b/PlexRequests.Services/Jobs/CouchPotatoCacher.cs @@ -63,7 +63,10 @@ namespace PlexRequests.Services.Jobs { Log.Trace("Getting all movies from CouchPotato"); var movies = CpApi.GetMovies(settings.FullUri, settings.ApiKey, new[] { "active" }); - Cache.Set(CacheKeys.CouchPotatoQueued, movies, CacheKeys.TimeFrameMinutes.SchedulerCaching); + if (movies != null) + { + Cache.Set(CacheKeys.CouchPotatoQueued, movies, CacheKeys.TimeFrameMinutes.SchedulerCaching); + } } } diff --git a/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs b/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs index f1288ec55..401b01a9c 100644 --- a/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs +++ b/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs @@ -256,7 +256,10 @@ namespace PlexRequests.Services.Jobs results = GetLibraries(authSettings, plexSettings); Log.Trace("Plex Lib Cache Set Call"); - Cache.Set(CacheKeys.PlexLibaries, results, CacheKeys.TimeFrameMinutes.SchedulerCaching); + if (results != null) + { + Cache.Set(CacheKeys.PlexLibaries, results, CacheKeys.TimeFrameMinutes.SchedulerCaching); + } } else { diff --git a/PlexRequests.Services/Jobs/SickRageCacher.cs b/PlexRequests.Services/Jobs/SickRageCacher.cs index 8b394eec3..1a28956a5 100644 --- a/PlexRequests.Services/Jobs/SickRageCacher.cs +++ b/PlexRequests.Services/Jobs/SickRageCacher.cs @@ -62,8 +62,11 @@ namespace PlexRequests.Services.Jobs if (settings.Enabled) { Log.Trace("Getting all shows from SickRage"); - var movies = SrApi.GetShows(settings.ApiKey, settings.FullUri); - Cache.Set(CacheKeys.SickRageQueued, movies.Result, CacheKeys.TimeFrameMinutes.SchedulerCaching); + var shows = SrApi.GetShows(settings.ApiKey, settings.FullUri); + if (shows != null) + { + Cache.Set(CacheKeys.SickRageQueued, shows.Result, CacheKeys.TimeFrameMinutes.SchedulerCaching); + } } } diff --git a/PlexRequests.Services/Jobs/SonarrCacher.cs b/PlexRequests.Services/Jobs/SonarrCacher.cs index 50b238735..983a50eea 100644 --- a/PlexRequests.Services/Jobs/SonarrCacher.cs +++ b/PlexRequests.Services/Jobs/SonarrCacher.cs @@ -64,7 +64,10 @@ namespace PlexRequests.Services.Jobs { Log.Trace("Getting all tv series from Sonarr"); var series = SonarrApi.GetSeries(settings.ApiKey, settings.FullUri); - Cache.Set(CacheKeys.SonarrQueued, series, CacheKeys.TimeFrameMinutes.SchedulerCaching); + if (series != null) + { + Cache.Set(CacheKeys.SonarrQueued, series, CacheKeys.TimeFrameMinutes.SchedulerCaching); + } } } From 07c00d232c3b9cb0ee211ba44fb75c412a62ffa0 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Wed, 20 Apr 2016 13:38:41 +0100 Subject: [PATCH 09/76] small changes --- PlexRequests.Core/Setup.cs | 4 ++-- .../Jobs/PlexAvailabilityChecker.cs | 12 ++++-------- PlexRequests.UI/Modules/SearchModule.cs | 7 +++---- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/PlexRequests.Core/Setup.cs b/PlexRequests.Core/Setup.cs index 73c4b0554..70d9e6083 100644 --- a/PlexRequests.Core/Setup.cs +++ b/PlexRequests.Core/Setup.cs @@ -139,7 +139,7 @@ namespace PlexRequests.Core } catch (Exception ex) { - Log.Error("Failed to cache Sonarr quality profiles!", ex); + Log.Error(ex, "Failed to cache Sonarr quality profiles!"); } } @@ -161,7 +161,7 @@ namespace PlexRequests.Core } catch (Exception ex) { - Log.Error("Failed to cache CouchPotato quality profiles!", ex); + Log.Error(ex, "Failed to cache CouchPotato quality profiles!"); } } diff --git a/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs b/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs index bb8f957a0..cf2de344d 100644 --- a/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs +++ b/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs @@ -96,8 +96,8 @@ namespace PlexRequests.Services.Jobs if (libraries == null) { - libraries = new List() { PlexApi.SearchContent(authSettings.PlexAuthToken, r.Title, plexSettings.FullUri) }; - if (libraries == null) + libraries = new List { PlexApi.SearchContent(authSettings.PlexAuthToken, r.Title, plexSettings.FullUri) }; + if (libraries.Count == 0) { Log.Trace("Could not find any matching result for this title."); continue; @@ -105,7 +105,6 @@ namespace PlexRequests.Services.Jobs } Log.Trace("Search results from Plex for the following request: {0}", r.Title); - //Log.Trace(results.DumpJson()); var releaseDate = r.ReleaseDate == DateTime.MinValue ? string.Empty : r.ReleaseDate.ToString("yyyy"); @@ -136,8 +135,7 @@ namespace PlexRequests.Services.Jobs } Log.Trace("Updating the requests now"); - Log.Trace("Requests that will be updates:"); - Log.Trace(modifiedModel.SelectMany(x => x.Title).DumpJson()); + Log.Trace("Requests that will be updated count {0}", modifiedModel.Count); if (modifiedModel.Any()) { @@ -257,9 +255,7 @@ namespace PlexRequests.Services.Jobs } else { - results = Cache.GetOrSet(CacheKeys.PlexLibaries, () => { - return GetLibraries(authSettings, plexSettings); - }, 10); + results = Cache.GetOrSet(CacheKeys.PlexLibaries, () => GetLibraries(authSettings, plexSettings), 10); } return results; } diff --git a/PlexRequests.UI/Modules/SearchModule.cs b/PlexRequests.UI/Modules/SearchModule.cs index 78bf7540d..99150f4c8 100644 --- a/PlexRequests.UI/Modules/SearchModule.cs +++ b/PlexRequests.UI/Modules/SearchModule.cs @@ -164,7 +164,6 @@ namespace PlexRequests.UI.Modules private Response ProcessMovies(MovieSearchType searchType, string searchTerm) { List taskList = new List(); - var cpSettings = CpService.GetSettings(); List apiMovies = new List(); taskList.Add(Task.Factory.StartNew>(() => @@ -264,9 +263,9 @@ namespace PlexRequests.UI.Modules { Log.Trace("Searching for TV Show {0}", searchTerm); - List taskList = new List(); + var taskList = new List(); - List apiTv = new List(); + var apiTv = new List(); taskList.Add(Task.Factory.StartNew(() => { return new TvMazeApi().Search(searchTerm); @@ -276,7 +275,7 @@ namespace PlexRequests.UI.Modules apiTv = t.Result; })); - Dictionary dbTv = new Dictionary(); + var dbTv = new Dictionary(); taskList.Add(Task.Factory.StartNew(() => { return RequestService.GetAll().Where(x => x.Type == RequestType.TvShow); From 54ab4854e613dd6da7a334d7190ed3a837d9c309 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Wed, 20 Apr 2016 13:48:44 +0100 Subject: [PATCH 10/76] Added the 'enable user notifications' to the email settings view and model. --- .../EmailNotificationSettings.cs | 35 ++++++++++++++++--- PlexRequests.UI/Modules/SearchModule.cs | 30 ++++++++-------- .../Views/Admin/EmailNotifications.cshtml | 17 ++++++++- 3 files changed, 62 insertions(+), 20 deletions(-) diff --git a/PlexRequests.Core/SettingModels/EmailNotificationSettings.cs b/PlexRequests.Core/SettingModels/EmailNotificationSettings.cs index 6a151e861..74cb7ec92 100644 --- a/PlexRequests.Core/SettingModels/EmailNotificationSettings.cs +++ b/PlexRequests.Core/SettingModels/EmailNotificationSettings.cs @@ -1,14 +1,41 @@ -namespace PlexRequests.Core.SettingModels +#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 PlexRequests.Core.SettingModels { public class EmailNotificationSettings : Settings { public string EmailHost { get; set; } + public string EmailPassword { get; set; } public int EmailPort { get; set; } - public bool Ssl { get; set; } - public string RecipientEmail { get; set; } public string EmailSender { get; set; } public string EmailUsername { get; set; } - public string EmailPassword { get; set; } public bool Enabled { get; set; } + public bool EnableUserEmailNotifications { get; set; } + public string RecipientEmail { get; set; } + public bool Ssl { get; set; } } } \ No newline at end of file diff --git a/PlexRequests.UI/Modules/SearchModule.cs b/PlexRequests.UI/Modules/SearchModule.cs index 99150f4c8..e6e47cc6e 100644 --- a/PlexRequests.UI/Modules/SearchModule.cs +++ b/PlexRequests.UI/Modules/SearchModule.cs @@ -163,10 +163,10 @@ namespace PlexRequests.UI.Modules private Response ProcessMovies(MovieSearchType searchType, string searchTerm) { - List taskList = new List(); + var taskList = new List(); - List apiMovies = new List(); - taskList.Add(Task.Factory.StartNew>(() => + var apiMovies = new List(); + taskList.Add(Task.Factory.StartNew(() => { switch(searchType) { @@ -212,13 +212,13 @@ namespace PlexRequests.UI.Modules Task.WaitAll(taskList.ToArray()); - int[] cpCached = CpCacher.QueuedIds(); + var cpCached = CpCacher.QueuedIds(); var plexMovies = Checker.GetPlexMovies(); - List viewMovies = new List(); + var viewMovies = new List(); foreach (MovieResult movie in apiMovies) { - var viewMovie = new SearchMovieViewModel() + var viewMovie = new SearchMovieViewModel { Adult = movie.Adult, BackdropPath = movie.BackdropPath, @@ -293,8 +293,8 @@ namespace PlexRequests.UI.Modules return Response.AsJson(""); } - int[] sonarrCached = SonarrCacher.QueuedIds(); - int[] sickRageCache = SickRageCacher.QueuedIds(); // consider just merging sonarr/sickrage arrays + var sonarrCached = SonarrCacher.QueuedIds(); + var sickRageCache = SickRageCacher.QueuedIds(); // consider just merging sonarr/sickrage arrays var plexTvShows = Checker.GetPlexTvShows(); var viewTv = new List(); @@ -322,7 +322,7 @@ namespace PlexRequests.UI.Modules { viewT.Available = true; } - else if (t.show.externals.thetvdb != null) + else if (t.show?.externals?.thetvdb != null) { int tvdbid = (int)t.show.externals.thetvdb; @@ -350,9 +350,9 @@ namespace PlexRequests.UI.Modules private Response SearchMusic(string searchTerm) { - List taskList = new List(); + var taskList = new List(); - List apiAlbums = new List(); + var apiAlbums = new List(); taskList.Add(Task.Factory.StartNew(() => { return MusicBrainzApi.SearchAlbum(searchTerm); @@ -362,7 +362,7 @@ namespace PlexRequests.UI.Modules apiAlbums = t.Result.releases ?? new List(); })); - Dictionary dbAlbum = new Dictionary(); + var dbAlbum = new Dictionary(); taskList.Add(Task.Factory.StartNew(() => { return RequestService.GetAll().Where(x => x.Type == RequestType.Album); @@ -486,7 +486,7 @@ namespace PlexRequests.UI.Modules if (result) { model.Approved = true; - Log.Debug("Adding movie to database requests (No approval required)"); + Log.Info("Adding movie to database (No approval required)"); RequestService.AddRequest(model); var notificationModel = new NotificationModel @@ -511,7 +511,7 @@ namespace PlexRequests.UI.Modules else { model.Approved = true; - Log.Debug("Adding movie to database requests (No approval required)"); + Log.Info("Adding movie to database (No approval required)"); RequestService.AddRequest(model); var notificationModel = new NotificationModel @@ -529,7 +529,7 @@ namespace PlexRequests.UI.Modules try { - Log.Debug("Adding movie to database requests"); + Log.Info("Adding movie to database"); var id = RequestService.AddRequest(model); var notificationModel = new NotificationModel { Title = model.Title, User = Username, DateTime = DateTime.Now, NotificationType = NotificationType.NewRequest }; diff --git a/PlexRequests.UI/Views/Admin/EmailNotifications.cshtml b/PlexRequests.UI/Views/Admin/EmailNotifications.cshtml index 45fda1a0e..978283d2c 100644 --- a/PlexRequests.UI/Views/Admin/EmailNotifications.cshtml +++ b/PlexRequests.UI/Views/Admin/EmailNotifications.cshtml @@ -45,7 +45,22 @@
- +
+ +
+
+ Please note that if user notifications is enabled, the email will get sent with the SMTP set-up below. +
+
From 076a75b82f4cbfa74f02ff29d7098c325a9b9017 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Thu, 21 Apr 2016 12:10:36 +0100 Subject: [PATCH 11/76] Stop the Cachers from bombing out when the response from the 3rd party api returns an exception or invalid response. #171 --- PlexRequests.Api/ApiRequest.cs | 23 ++++++---- PlexRequests.Api/CouchPotatoApi.cs | 18 ++++++-- PlexRequests.Api/PlexApi.cs | 32 ++++++++++---- PlexRequests.Api/SickrageApi.cs | 17 +++++++- PlexRequests.Api/SonarrApi.cs | 12 +++++- PlexRequests.Api/TheMovieDbApi.cs | 4 +- PlexRequests.Api/TheTvDbApi.cs | 3 ++ .../Exceptions/ApiRequestException.cs | 42 +++++++++++++++++++ .../PlexRequests.Helpers.csproj | 1 + PlexRequests.UI.Tests/AdminModuleTests.cs | 9 +++- PlexRequests.UI.Tests/UserLoginModuleTests.cs | 3 ++ PlexRequests.UI/Modules/SearchModule.cs | 3 -- 12 files changed, 138 insertions(+), 29 deletions(-) create mode 100644 PlexRequests.Helpers/Exceptions/ApiRequestException.cs diff --git a/PlexRequests.Api/ApiRequest.cs b/PlexRequests.Api/ApiRequest.cs index d3c26066c..b5ffa8c76 100644 --- a/PlexRequests.Api/ApiRequest.cs +++ b/PlexRequests.Api/ApiRequest.cs @@ -25,7 +25,6 @@ // ************************************************************************/ #endregion using System; -using System.Collections.Generic; using System.IO; using System.Xml.Serialization; @@ -34,14 +33,15 @@ using Newtonsoft.Json; using NLog; using PlexRequests.Api.Interfaces; -using PlexRequests.Helpers; +using PlexRequests.Helpers.Exceptions; + using RestSharp; namespace PlexRequests.Api { public class ApiRequest : IApiRequest { - private JsonSerializerSettings Settings = new JsonSerializerSettings + private readonly JsonSerializerSettings _settings = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, MissingMemberHandling = MissingMemberHandling.Ignore @@ -66,7 +66,8 @@ namespace PlexRequests.Api if (response.ErrorException != null) { var message = "Error retrieving response. Check inner details for more info."; - throw new ApplicationException(message, response.ErrorException); + Log.Error(response.ErrorException); + throw new ApiRequestException(message, response.ErrorException); } return response.Data; @@ -80,8 +81,9 @@ namespace PlexRequests.Api if (response.ErrorException != null) { + Log.Error(response.ErrorException); var message = "Error retrieving response. Check inner details for more info."; - throw new ApplicationException(message, response.ErrorException); + throw new ApiRequestException(message, response.ErrorException); } return response; @@ -95,8 +97,9 @@ namespace PlexRequests.Api if (response.ErrorException != null) { + Log.Error(response.ErrorException); var message = "Error retrieving response. Check inner details for more info."; - throw new ApplicationException(message, response.ErrorException); + throw new ApiRequestException(message, response.ErrorException); } var result = DeserializeXml(response.Content); @@ -112,12 +115,13 @@ namespace PlexRequests.Api Log.Trace(response.Content); if (response.ErrorException != null) { + Log.Error(response.ErrorException); var message = "Error retrieving response. Check inner details for more info."; - throw new ApplicationException(message, response.ErrorException); + throw new ApiRequestException(message, response.ErrorException); } Log.Trace("Deserialzing Object"); - var json = JsonConvert.DeserializeObject(response.Content, Settings); + var json = JsonConvert.DeserializeObject(response.Content, _settings); Log.Trace("Finished Deserialzing Object"); return json; @@ -133,8 +137,9 @@ namespace PlexRequests.Api using (var sr = new StringReader(input)) return (T)ser.Deserialize(sr); } - catch (InvalidOperationException) + catch (InvalidOperationException e) { + Log.Error(e); return null; } } diff --git a/PlexRequests.Api/CouchPotatoApi.cs b/PlexRequests.Api/CouchPotatoApi.cs index 88cc07075..9db43a409 100644 --- a/PlexRequests.Api/CouchPotatoApi.cs +++ b/PlexRequests.Api/CouchPotatoApi.cs @@ -31,6 +31,7 @@ using Newtonsoft.Json.Linq; using NLog; using PlexRequests.Api.Interfaces; using PlexRequests.Api.Models.Movie; +using PlexRequests.Helpers.Exceptions; using RestSharp; @@ -118,13 +119,22 @@ namespace PlexRequests.Api public CouchPotatoMovies GetMovies(Uri baseUrl, string apiKey, string[] status) { - RestRequest request; - request = new RestRequest { Resource = "/api/{apikey}/movie.list?status={status}" }; + var request = new RestRequest + { + Resource = "/api/{apikey}/movie.list?status={status}" + }; request.AddUrlSegment("apikey", apiKey); request.AddUrlSegment("status", string.Join(",", status)); - - return Api.Execute(request, baseUrl); + try + { + return Api.Execute(request, baseUrl); + } + catch (ApiRequestException) // Request error is already logged in the ApiRequest class + { + Log.Error("Error when attempting to GetMovies."); + return new CouchPotatoMovies(); + } } } } \ No newline at end of file diff --git a/PlexRequests.Api/PlexApi.cs b/PlexRequests.Api/PlexApi.cs index a91355c63..cbe06e3a6 100644 --- a/PlexRequests.Api/PlexApi.cs +++ b/PlexRequests.Api/PlexApi.cs @@ -26,14 +26,14 @@ #endregion using System; +using NLog; + using PlexRequests.Api.Interfaces; -using PlexRequests.Api.Models; using PlexRequests.Api.Models.Plex; using PlexRequests.Helpers; +using PlexRequests.Helpers.Exceptions; using RestSharp; -using System.Xml; -using System.Collections.Generic; namespace PlexRequests.Api { @@ -43,6 +43,8 @@ namespace PlexRequests.Api { Version = AssemblyHelper.GetAssemblyVersion(); } + + private static Logger Log = LogManager.GetCurrentClassLogger(); private static string Version { get; } public PlexAuthentication SignIn(string username, string password) @@ -148,9 +150,16 @@ namespace PlexRequests.Api AddHeaders(ref request, authToken); var api = new ApiRequest(); - var sections = api.ExecuteXml(request, plexFullHost); + try + { - return sections; + return api.ExecuteXml(request, plexFullHost); + } + catch (ApiRequestException) + { + Log.Error("There has been a API Exception when attempting to get the Plex Libraries"); + return new PlexLibraries(); + } } public PlexSearch GetLibrary(string authToken, Uri plexFullHost, string libraryId) @@ -161,13 +170,20 @@ namespace PlexRequests.Api Resource = "library/sections/{libraryId}/all" }; - request.AddUrlSegment("libraryId", libraryId.ToString()); + request.AddUrlSegment("libraryId", libraryId); AddHeaders(ref request, authToken); var api = new ApiRequest(); - var search = api.ExecuteXml(request, plexFullHost); + try + { - return search; + return api.ExecuteXml(request, plexFullHost); + } + catch (ApiRequestException) + { + Log.Error("There has been a API Exception when attempting to get the Plex Library"); + return new PlexSearch(); + } } private void AddHeaders(ref RestRequest request, string authToken) diff --git a/PlexRequests.Api/SickrageApi.cs b/PlexRequests.Api/SickrageApi.cs index 7d5aca913..e600d5aec 100644 --- a/PlexRequests.Api/SickrageApi.cs +++ b/PlexRequests.Api/SickrageApi.cs @@ -41,6 +41,8 @@ using PlexRequests.Helpers; using RestSharp; using Newtonsoft.Json.Linq; +using PlexRequests.Helpers.Exceptions; + namespace PlexRequests.Api { public class SickrageApi : ISickRageApi @@ -218,7 +220,20 @@ namespace PlexRequests.Api }; request.AddUrlSegment("apiKey", apiKey); - return await Task.Run(() => Api.Execute(request, baseUrl)).ConfigureAwait(false); + return await Task.Run( + () => + { + try + { + return Api.Execute(request, baseUrl); + } + catch (ApiRequestException) + { + Log.Error("There has been a API exception when Getting the Sickrage shows"); + return null; + } + + }).ConfigureAwait(false); } } } \ No newline at end of file diff --git a/PlexRequests.Api/SonarrApi.cs b/PlexRequests.Api/SonarrApi.cs index e9e1e96ad..3d160187a 100644 --- a/PlexRequests.Api/SonarrApi.cs +++ b/PlexRequests.Api/SonarrApi.cs @@ -38,6 +38,8 @@ using PlexRequests.Helpers; using RestSharp; using Newtonsoft.Json.Linq; +using PlexRequests.Helpers.Exceptions; + namespace PlexRequests.Api { public class SonarrApi : ISonarrApi @@ -128,8 +130,16 @@ namespace PlexRequests.Api { var request = new RestRequest { Resource = "/api/series", Method = Method.GET }; request.AddHeader("X-Api-Key", apiKey); + try + { - return Api.Execute>(request, baseUrl); + return Api.Execute>(request, baseUrl); + } + catch (ApiRequestException) + { + Log.Error("There has been an API exception when getting the Sonarr Series"); + return null; + } } } } \ No newline at end of file diff --git a/PlexRequests.Api/TheMovieDbApi.cs b/PlexRequests.Api/TheMovieDbApi.cs index 6d8fa31ef..ce0d3772c 100644 --- a/PlexRequests.Api/TheMovieDbApi.cs +++ b/PlexRequests.Api/TheMovieDbApi.cs @@ -50,7 +50,7 @@ namespace PlexRequests.Api return results.Results; } - [Obsolete("Should use TheTvDbApi for TV")] + [Obsolete("Should use TvMaze for TV")] public async Task> SearchTv(string searchTerm) { var results = await Client.SearchTvShow(searchTerm); @@ -74,7 +74,7 @@ namespace PlexRequests.Api return movies; } - [Obsolete("Should use TheTvDbApi for TV")] + [Obsolete("Should use TvMaze for TV")] public async Task GetTvShowInformation(int tmdbId) { var show = await Client.GetTvShow(tmdbId); diff --git a/PlexRequests.Api/TheTvDbApi.cs b/PlexRequests.Api/TheTvDbApi.cs index 50c94e7d9..2ac7734f7 100644 --- a/PlexRequests.Api/TheTvDbApi.cs +++ b/PlexRequests.Api/TheTvDbApi.cs @@ -24,12 +24,15 @@ // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // ************************************************************************/ #endregion +using System; + using PlexRequests.Api.Models.Tv; using RestSharp; namespace PlexRequests.Api { + [Obsolete("Use TVMazeAPP")] public class TheTvDbApi : TvBase { public TheTvDbApi() diff --git a/PlexRequests.Helpers/Exceptions/ApiRequestException.cs b/PlexRequests.Helpers/Exceptions/ApiRequestException.cs new file mode 100644 index 000000000..e44a8fdec --- /dev/null +++ b/PlexRequests.Helpers/Exceptions/ApiRequestException.cs @@ -0,0 +1,42 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: ApplicationSettingsException.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; + +namespace PlexRequests.Helpers.Exceptions +{ + public class ApiRequestException : Exception + { + public ApiRequestException(string message) : base(message) + { + + } + public ApiRequestException(string message, Exception innerException) : base(message, innerException) + { + + } + } +} \ No newline at end of file diff --git a/PlexRequests.Helpers/PlexRequests.Helpers.csproj b/PlexRequests.Helpers/PlexRequests.Helpers.csproj index b15baabc3..c8c08a6ec 100644 --- a/PlexRequests.Helpers/PlexRequests.Helpers.csproj +++ b/PlexRequests.Helpers/PlexRequests.Helpers.csproj @@ -53,6 +53,7 @@ + diff --git a/PlexRequests.UI.Tests/AdminModuleTests.cs b/PlexRequests.UI.Tests/AdminModuleTests.cs index e8ae7f04c..77d316deb 100644 --- a/PlexRequests.UI.Tests/AdminModuleTests.cs +++ b/PlexRequests.UI.Tests/AdminModuleTests.cs @@ -45,6 +45,7 @@ using PlexRequests.Store.Repository; using PlexRequests.UI.Models; using PlexRequests.UI.Modules; using PlexRequests.Helpers; +using PlexRequests.UI.Helpers; namespace PlexRequests.UI.Tests { @@ -121,7 +122,13 @@ namespace PlexRequests.UI.Tests with.Dependency(PushoverApi.Object); with.Dependency(NotificationService.Object); with.Dependency(HeadphonesSettings.Object); - with.Dependencies(Cache.Object); + with.Dependency(Cache.Object); + with.ApplicationStartup( + (container, pipelines) => + { + var loc = ServiceLocator.Instance; + loc.SetContainer(container); + }); with.RootPathProvider(); with.RequestStartup((container, pipelines, context) => { diff --git a/PlexRequests.UI.Tests/UserLoginModuleTests.cs b/PlexRequests.UI.Tests/UserLoginModuleTests.cs index eb71e8cd1..d8e5b3e3a 100644 --- a/PlexRequests.UI.Tests/UserLoginModuleTests.cs +++ b/PlexRequests.UI.Tests/UserLoginModuleTests.cs @@ -77,6 +77,9 @@ namespace PlexRequests.UI.Tests with.RootPathProvider(); }); + var loc = ServiceLocator.Instance; + loc.SetContainer(TinyIoCContainer.Current); + bootstrapper.WithSession(new Dictionary()); var browser = new Browser(bootstrapper); diff --git a/PlexRequests.UI/Modules/SearchModule.cs b/PlexRequests.UI/Modules/SearchModule.cs index e6e47cc6e..3001b8511 100644 --- a/PlexRequests.UI/Modules/SearchModule.cs +++ b/PlexRequests.UI/Modules/SearchModule.cs @@ -49,7 +49,6 @@ using PlexRequests.Store; using PlexRequests.UI.Helpers; using PlexRequests.UI.Models; using System.Threading.Tasks; -using TMDbLib.Objects.Search; using PlexRequests.Api.Models.Tv; using TMDbLib.Objects.General; @@ -71,7 +70,6 @@ namespace PlexRequests.UI.Modules CpService = cpSettings; PrService = prSettings; MovieApi = new TheMovieDbApi(); - TvApi = new TheTvDbApi(); Cache = cache; Checker = checker; CpCacher = cpCacher; @@ -108,7 +106,6 @@ namespace PlexRequests.UI.Modules private INotificationService NotificationService { get; } private ICouchPotatoApi CouchPotatoApi { get; } private ISonarrApi SonarrApi { get; } - private TheTvDbApi TvApi { get; } private ISickRageApi SickrageApi { get; } private IRequestService RequestService { get; } private ICacheProvider Cache { get; } From ba06e8630f4bf4db8f751ad5407ce47b62f77c00 Mon Sep 17 00:00:00 2001 From: Drewster727 Date: Thu, 21 Apr 2016 09:00:06 -0500 Subject: [PATCH 12/76] additional cacher error handling + don't bother checking the requests when we don't get data back from plex --- .../Jobs/CouchPotatoCacher.cs | 13 ++++-- .../Jobs/PlexAvailabilityChecker.cs | 45 ++++++++++++------- PlexRequests.Services/Jobs/SickRageCacher.cs | 13 ++++-- PlexRequests.Services/Jobs/SonarrCacher.cs | 13 ++++-- 4 files changed, 60 insertions(+), 24 deletions(-) diff --git a/PlexRequests.Services/Jobs/CouchPotatoCacher.cs b/PlexRequests.Services/Jobs/CouchPotatoCacher.cs index 2440477f0..d31bb7aa4 100644 --- a/PlexRequests.Services/Jobs/CouchPotatoCacher.cs +++ b/PlexRequests.Services/Jobs/CouchPotatoCacher.cs @@ -62,10 +62,17 @@ namespace PlexRequests.Services.Jobs if (settings.Enabled) { Log.Trace("Getting all movies from CouchPotato"); - var movies = CpApi.GetMovies(settings.FullUri, settings.ApiKey, new[] { "active" }); - if (movies != null) + try { - Cache.Set(CacheKeys.CouchPotatoQueued, movies, CacheKeys.TimeFrameMinutes.SchedulerCaching); + var movies = CpApi.GetMovies(settings.FullUri, settings.ApiKey, new[] { "active" }); + if (movies != null) + { + Cache.Set(CacheKeys.CouchPotatoQueued, movies, CacheKeys.TimeFrameMinutes.SchedulerCaching); + } + } + catch (System.Exception ex) + { + Log.Error(ex, "Failed caching queued items from CouchPotato"); } } } diff --git a/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs b/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs index 3c7915fa6..b47651123 100644 --- a/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs +++ b/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs @@ -75,6 +75,13 @@ namespace PlexRequests.Services.Jobs } var libraries = CachedLibraries(authSettings, plexSettings, true); //force setting the cache (10 min intervals via scheduler) + + if (libraries == null || !libraries.Any()) + { + Log.Info("Did not find any libraries in Plex."); + return; + } + var movies = GetPlexMovies().ToArray(); var shows = GetPlexTvShows().ToArray(); var albums = GetPlexAlbums().ToArray(); @@ -248,25 +255,33 @@ namespace PlexRequests.Services.Jobs return results; // don't error out here, just let it go! } - if (setCache) + try { - Log.Trace("Plex Lib API Call"); - results = GetLibraries(authSettings, plexSettings); - - Log.Trace("Plex Lib Cache Set Call"); - if (results != null) + if (setCache) { - Cache.Set(CacheKeys.PlexLibaries, results, CacheKeys.TimeFrameMinutes.SchedulerCaching); + Log.Trace("Plex Lib API Call"); + results = GetLibraries(authSettings, plexSettings); + + Log.Trace("Plex Lib Cache Set Call"); + if (results != null) + { + Cache.Set(CacheKeys.PlexLibaries, results, CacheKeys.TimeFrameMinutes.SchedulerCaching); + } + } + else + { + Log.Trace("Plex Lib GetSet Call"); + results = Cache.GetOrSet(CacheKeys.PlexLibaries, () => { + Log.Trace("Plex Lib API Call (inside getset)"); + return GetLibraries(authSettings, plexSettings); + }, CacheKeys.TimeFrameMinutes.SchedulerCaching); } - } - else - { - Log.Trace("Plex Lib GetSet Call"); - results = Cache.GetOrSet(CacheKeys.PlexLibaries, () => { - Log.Trace("Plex Lib API Call (inside getset)"); - return GetLibraries(authSettings, plexSettings); - }, CacheKeys.TimeFrameMinutes.SchedulerCaching); } + catch (Exception ex) + { + Log.Error(ex, "Failed to obtain Plex libraries"); + } + return results; } diff --git a/PlexRequests.Services/Jobs/SickRageCacher.cs b/PlexRequests.Services/Jobs/SickRageCacher.cs index 1a28956a5..3591cf7bb 100644 --- a/PlexRequests.Services/Jobs/SickRageCacher.cs +++ b/PlexRequests.Services/Jobs/SickRageCacher.cs @@ -62,10 +62,17 @@ namespace PlexRequests.Services.Jobs if (settings.Enabled) { Log.Trace("Getting all shows from SickRage"); - var shows = SrApi.GetShows(settings.ApiKey, settings.FullUri); - if (shows != null) + try { - Cache.Set(CacheKeys.SickRageQueued, shows.Result, CacheKeys.TimeFrameMinutes.SchedulerCaching); + var shows = SrApi.GetShows(settings.ApiKey, settings.FullUri); + if (shows != null) + { + Cache.Set(CacheKeys.SickRageQueued, shows.Result, CacheKeys.TimeFrameMinutes.SchedulerCaching); + } + } + catch (System.Exception ex) + { + Log.Error(ex, "Failed caching queued items from SickRage"); } } } diff --git a/PlexRequests.Services/Jobs/SonarrCacher.cs b/PlexRequests.Services/Jobs/SonarrCacher.cs index 983a50eea..15aef545d 100644 --- a/PlexRequests.Services/Jobs/SonarrCacher.cs +++ b/PlexRequests.Services/Jobs/SonarrCacher.cs @@ -63,10 +63,17 @@ namespace PlexRequests.Services.Jobs if (settings.Enabled) { Log.Trace("Getting all tv series from Sonarr"); - var series = SonarrApi.GetSeries(settings.ApiKey, settings.FullUri); - if (series != null) + try { - Cache.Set(CacheKeys.SonarrQueued, series, CacheKeys.TimeFrameMinutes.SchedulerCaching); + var series = SonarrApi.GetSeries(settings.ApiKey, settings.FullUri); + if (series != null) + { + Cache.Set(CacheKeys.SonarrQueued, series, CacheKeys.TimeFrameMinutes.SchedulerCaching); + } + } + catch (System.Exception ex) + { + Log.Error(ex, "Failed caching queued items from Sonarr"); } } } From b0ef40a5604cb7e335a1def3ea603f8332da19af Mon Sep 17 00:00:00 2001 From: tidusjar Date: Mon, 25 Apr 2016 10:10:20 +0100 Subject: [PATCH 13/76] Remove old migration code and added new migration code. --- PlexRequests.Core/Setup.cs | 96 ++--------------------------- PlexRequests.Store/TableCreation.cs | 17 +++-- 2 files changed, 19 insertions(+), 94 deletions(-) diff --git a/PlexRequests.Core/Setup.cs b/PlexRequests.Core/Setup.cs index 70d9e6083..a73a5752d 100644 --- a/PlexRequests.Core/Setup.cs +++ b/PlexRequests.Core/Setup.cs @@ -58,10 +58,9 @@ namespace PlexRequests.Core var version = CheckSchema(); if (version > 0) { - if (version > 1300 && version <= 1699) + if (version > 1700 && version <= 1799) { - MigrateDbFrom1300(); - UpdateRequestBlobsTable(); + MigrateToVersion1700(); } } @@ -164,94 +163,11 @@ namespace PlexRequests.Core Log.Error(ex, "Failed to cache CouchPotato quality profiles!"); } } - - private void UpdateRequestBlobsTable() // TODO: Remove in v1.7 + public void MigrateToVersion1700() { - try - { - TableCreation.AlterTable(Db.DbConnection(), "RequestBlobs", "ADD COLUMN", "MusicId", false, "TEXT"); - } - catch (Exception e) - { - Log.Error("Tried updating the schema to alter the request blobs table"); - Log.Error(e); - } - } - private void MigrateDbFrom1300() // TODO: Remove in v1.7 - { - - var result = new List(); - RequestedModel[] requestedModels; - var repo = new GenericRepository(Db, new MemoryCacheProvider()); - try - { - var records = repo.GetAll(); - requestedModels = records as RequestedModel[] ?? records.ToArray(); - } - catch (SqliteException) - { - // There is no requested table so they do not have an old version of the DB - return; - } - - if (!requestedModels.Any()) - { return; } - - var jsonRepo = new JsonRequestService(new RequestJsonRepository(Db, new MemoryCacheProvider())); - - var api = new TvMazeApi(); - - foreach (var r in requestedModels.Where(x => x.Type == RequestType.TvShow)) - { - var show = api.ShowLookupByTheTvDbId(r.ProviderId); - - var model = new RequestedModel - { - Title = show.name, - PosterPath = show.image?.medium, - Type = RequestType.TvShow, - ProviderId = show.externals.thetvdb ?? 0, - ReleaseDate = r.ReleaseDate, - AdminNote = r.AdminNote, - Approved = r.Approved, - Available = r.Available, - ImdbId = show.externals.imdb, - Issues = r.Issues, - OtherMessage = r.OtherMessage, - Overview = show.summary.RemoveHtml(), - RequestedUsers = r.AllUsers, // should pull in the RequestedBy property and merge with RequestedUsers - RequestedDate = r.ReleaseDate, - Status = show.status - }; - var id = jsonRepo.AddRequest(model); - result.Add(id); - } - - foreach (var source in requestedModels.Where(x => x.Type == RequestType.Movie)) - { - var id = jsonRepo.AddRequest(source); - result.Add(id); - } - - - if (result.Any(x => x == -1)) - { - throw new SqliteException("Could not migrate the DB!"); - } - - - if (result.Count != requestedModels.Length) - { - throw new SqliteException("Could not migrate the DB! count is different"); - } - - - // Now delete the old requests - foreach (var oldRequest in requestedModels) - { - repo.Delete(oldRequest); - } - + // Drop old tables + TableCreation.DropTable(Db.DbConnection(), "User"); + TableCreation.DropTable(Db.DbConnection(), "Log"); } } } diff --git a/PlexRequests.Store/TableCreation.cs b/PlexRequests.Store/TableCreation.cs index 717bd2dd9..1e3a9d5a7 100644 --- a/PlexRequests.Store/TableCreation.cs +++ b/PlexRequests.Store/TableCreation.cs @@ -44,7 +44,18 @@ namespace PlexRequests.Store connection.Close(); } - public static void AlterTable(IDbConnection connection, string tableName, string alterType, string newColumn, bool isNullable, string dataType) + public static void DropTable(IDbConnection con, string tableName) + { + using (con) + { + con.Open(); + var query = $"DROP TABLE IF EXISTS {tableName}"; + con.Execute(query); + con.Close(); + } + } + + public static void AddColumn(IDbConnection connection, string tableName, string alterType, string newColumn, bool isNullable, string dataType) { connection.Open(); var result = connection.Query($"PRAGMA table_info({tableName});"); @@ -83,7 +94,7 @@ namespace PlexRequests.Store public static void CreateSchema(this IDbConnection con, int version) { con.Open(); - con.Query(string.Format("INSERT INTO DBInfo (SchemaVersion) values ({0})", version)); + con.Query($"INSERT INTO DBInfo (SchemaVersion) values ({version})"); con.Close(); } @@ -115,7 +126,5 @@ namespace PlexRequests.Store public string dflt_value { get; set; } public int pk { get; set; } } - - } } From 186b18ccb91d597b8148ffa776b6d7af83170217 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Mon, 25 Apr 2016 11:08:24 +0100 Subject: [PATCH 14/76] Improved the startup of the application. We now properaly parse any args passed into the console. --- PlexRequests.Helpers/LoggingHelper.cs | 2 +- PlexRequests.UI/PlexRequests.UI.csproj | 5 ++ PlexRequests.UI/Program.cs | 65 ++++++++++++------------- PlexRequests.UI/Start/StartupOptions.cs | 56 +++++++++++++++++++++ PlexRequests.UI/packages.config | 1 + 5 files changed, 93 insertions(+), 36 deletions(-) create mode 100644 PlexRequests.UI/Start/StartupOptions.cs diff --git a/PlexRequests.Helpers/LoggingHelper.cs b/PlexRequests.Helpers/LoggingHelper.cs index 03a80d3db..ce9497a8b 100644 --- a/PlexRequests.Helpers/LoggingHelper.cs +++ b/PlexRequests.Helpers/LoggingHelper.cs @@ -61,7 +61,7 @@ namespace PlexRequests.Helpers return dumpTarget.ToString(); } - public static void ConfigureLogging(string connectionString) + public static void ConfigureLogging(string connectionString) { LogManager.ThrowExceptions = true; // Step 1. Create configuration object diff --git a/PlexRequests.UI/PlexRequests.UI.csproj b/PlexRequests.UI/PlexRequests.UI.csproj index 176b1b0fe..45668bc1a 100644 --- a/PlexRequests.UI/PlexRequests.UI.csproj +++ b/PlexRequests.UI/PlexRequests.UI.csproj @@ -57,6 +57,10 @@ + + ..\packages\CommandLineParser.2.0.275-beta\lib\net45\CommandLine.dll + True + ..\packages\Common.Logging.3.0.0\lib\net40\Common.Logging.dll True @@ -187,6 +191,7 @@ + diff --git a/PlexRequests.UI/Program.cs b/PlexRequests.UI/Program.cs index fb906e349..471085c19 100644 --- a/PlexRequests.UI/Program.cs +++ b/PlexRequests.UI/Program.cs @@ -40,6 +40,10 @@ using PlexRequests.Store; using PlexRequests.Store.Repository; using System.Diagnostics; +using CommandLine; + +using PlexRequests.UI.Start; + namespace PlexRequests.UI { class Program @@ -47,37 +51,18 @@ namespace PlexRequests.UI private static Logger Log = LogManager.GetCurrentClassLogger(); static void Main(string[] args) { - var baseUrl = string.Empty; - var port = -1; - if (args.Length > 0) - { - for (var i = 0; i < args.Length; i++) - { - var arg = args[i].ToLowerInvariant().Substring(1); - switch (arg) - { - case "base": - i++; - var value = args[i]; - Console.WriteLine($"Using a Base URL {args[i]}"); - baseUrl = value; - break; - default: - int portResult; - if (!int.TryParse(args[i], out portResult)) - { - Console.WriteLine("Didn't pass in a valid port"); - Console.ReadLine(); - Environment.Exit(1); - } - else - { - port = portResult; - } - break; - } - } - } + + var result = Parser.Default.ParseArguments(args); + var baseUrl = result.MapResult( + o => o.BaseUrl, + e => string.Empty); + + var port = result.MapResult( + x => x.Port, + e => -1); + + PrintToConsole("Starting Up! Please wait, this can usually take a few seconds.", ConsoleColor.Yellow); + Log.Trace("Getting product version"); WriteOutVersion(); @@ -87,7 +72,7 @@ namespace PlexRequests.UI ConfigureTargets(cn); SetupLogging(); - if (port == -1) + if (port == -1 || port == 3579) port = GetStartupPort(); var options = new StartOptions(Debugger.IsAttached ? $"http://localhost:{port}" : $"http://+:{port}") @@ -98,11 +83,13 @@ namespace PlexRequests.UI { using (WebApp.Start(options)) { - Console.WriteLine($"Request Plex is running on the following: http://+:{port}/"); + Console.WriteLine($"Plex Requests is running on the following: http://+:{port}/{baseUrl}"); + PrintToConsole("All setup, Plex Requests is now ready!", ConsoleColor.Yellow); if (Type.GetType("Mono.Runtime") != null) { - Log.Trace("We are on Mono!"); + Log.Info("We are on Mono!"); + // on mono, processes will usually run as daemons - this allows you to listen // for termination signals (ctrl+c, shutdown, etc) and finalize correctly UnixSignal.WaitAny( @@ -110,7 +97,7 @@ namespace PlexRequests.UI } else { - Log.Trace("This is not Mono"); + Log.Info("This is not Mono"); Console.WriteLine("Press any key to exit"); Console.ReadLine(); } @@ -119,6 +106,7 @@ namespace PlexRequests.UI catch (Exception e) { Log.Fatal(e); + Console.WriteLine(e); throw; } } @@ -160,5 +148,12 @@ namespace PlexRequests.UI LoggingHelper.ReconfigureLogLevel(LogLevel.FromOrdinal(logSettings.Level)); } } + + private static void PrintToConsole(string message, ConsoleColor colour = ConsoleColor.Gray) + { + Console.ForegroundColor = colour; + Console.WriteLine(message); + Console.ForegroundColor = ConsoleColor.Gray; + } } } diff --git a/PlexRequests.UI/Start/StartupOptions.cs b/PlexRequests.UI/Start/StartupOptions.cs new file mode 100644 index 000000000..a911a00b3 --- /dev/null +++ b/PlexRequests.UI/Start/StartupOptions.cs @@ -0,0 +1,56 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: StartupOptions.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.Text; + +using CommandLine; + +namespace PlexRequests.UI.Start +{ + public class StartupOptions + { + /// + /// Gets or sets the base URL. + /// + /// + /// The base URL. + /// + [Option('b',"base", Required = false, HelpText = "Provide a base url for Plex Requests")] + public string BaseUrl { get; set; } + + /// + /// Gets or sets the port. + /// + /// + /// The port. + /// + [Option('p', "port", Required = false, HelpText = "Provide a port for Plex Requests to run on. You can also change this in the settings page in the UI", Default = 3579)] + public int Port { get; set; } + + + + } +} \ No newline at end of file diff --git a/PlexRequests.UI/packages.config b/PlexRequests.UI/packages.config index 63d5b6c2b..196008837 100644 --- a/PlexRequests.UI/packages.config +++ b/PlexRequests.UI/packages.config @@ -1,5 +1,6 @@  + From 7051c31dd3961d9f801c43b1dfdabe2cfa47743e Mon Sep 17 00:00:00 2001 From: tidusjar Date: Mon, 25 Apr 2016 13:45:22 +0100 Subject: [PATCH 15/76] More work on #27 Added a new notify button to the search UI (Needs styling). Also fixed a bug where if the user could only see their own requests, if they search for something that has been requested, it will show as requested. --- PlexRequests.Store/RequestedModel.cs | 24 ++++++--- PlexRequests.UI/Content/search.js | 4 ++ PlexRequests.UI/Modules/SearchModule.cs | 35 ++++++++++--- PlexRequests.UI/Views/Search/Index.cshtml | 60 +++++++++++++---------- 4 files changed, 83 insertions(+), 40 deletions(-) diff --git a/PlexRequests.Store/RequestedModel.cs b/PlexRequests.Store/RequestedModel.cs index 4b6f288cf..0f4a28f23 100644 --- a/PlexRequests.Store/RequestedModel.cs +++ b/PlexRequests.Store/RequestedModel.cs @@ -40,6 +40,7 @@ namespace PlexRequests.Store public List RequestedUsers { get; set; } public string ArtistName { get; set; } public string ArtistId { get; set; } + public List UsersToNotify { get; private set; } [JsonIgnore] public List AllUsers @@ -61,18 +62,27 @@ namespace PlexRequests.Store } [JsonIgnore] - public bool CanApprove - { - get - { - return !Approved && !Available; - } - } + public bool CanApprove => !Approved && !Available; public bool UserHasRequested(string username) { return AllUsers.Any(x => x.Equals(username, StringComparison.OrdinalIgnoreCase)); } + + public void AddUserToNotification(string username) + { + if (UsersToNotify == null) + { + UsersToNotify = new List(); + } + if (UsersToNotify.FirstOrDefault(x => x == username) != null) + { + // User already exists in the notification list + return; + } + + UsersToNotify.Add(username); + } } public enum RequestType diff --git a/PlexRequests.UI/Content/search.js b/PlexRequests.UI/Content/search.js index e92b32454..363e79689 100644 --- a/PlexRequests.UI/Content/search.js +++ b/PlexRequests.UI/Content/search.js @@ -114,6 +114,10 @@ $(function () { var url = $form.prop('action'); var data = $form.serialize(); + var $notify = $('#notifyUser').is(':checked'); + + data = data + "¬ify=" + $notify; + sendRequestAjax(data, type, url, buttonId); }); diff --git a/PlexRequests.UI/Modules/SearchModule.cs b/PlexRequests.UI/Modules/SearchModule.cs index 3001b8511..8f9e1ed4c 100644 --- a/PlexRequests.UI/Modules/SearchModule.cs +++ b/PlexRequests.UI/Modules/SearchModule.cs @@ -97,7 +97,7 @@ namespace PlexRequests.UI.Modules Get["movie/upcoming"] = parameters => UpcomingMovies(); Get["movie/playing"] = parameters => CurrentlyPlayingMovies(); - Post["request/movie"] = parameters => RequestMovie((int)Request.Form.movieId); + Post["request/movie"] = parameters => RequestMovie((int)Request.Form.movieId, (bool)Request.Form.notify); Post["request/tv"] = parameters => RequestTvShow((int)Request.Form.tvId, (string)Request.Form.seasons); Post["request/album"] = parameters => RequestAlbum((string)Request.Form.albumId); } @@ -211,7 +211,7 @@ namespace PlexRequests.UI.Modules var cpCached = CpCacher.QueuedIds(); var plexMovies = Checker.GetPlexMovies(); - + var settings = PrService.GetSettings(); var viewMovies = new List(); foreach (MovieResult movie in apiMovies) { @@ -232,12 +232,12 @@ namespace PlexRequests.UI.Modules VoteAverage = movie.VoteAverage, VoteCount = movie.VoteCount }; - + var canSee = CanUserSeeThisRequest(viewMovie.Id, settings.UsersCanViewOnlyOwnRequests, dbMovies); if (Checker.IsMovieAvailable(plexMovies.ToArray(), movie.Title, movie.ReleaseDate?.Year.ToString())) { viewMovie.Available = true; } - else if (dbMovies.ContainsKey(movie.Id)) // compare to the requests db + else if (dbMovies.ContainsKey(movie.Id) && canSee) // compare to the requests db { var dbm = dbMovies[movie.Id]; @@ -245,7 +245,7 @@ namespace PlexRequests.UI.Modules viewMovie.Approved = dbm.Approved; viewMovie.Available = dbm.Available; } - else if (cpCached.Contains(movie.Id)) // compare to the couchpotato db + else if (cpCached.Contains(movie.Id) && canSee) // compare to the couchpotato db { viewMovie.Requested = true; } @@ -256,6 +256,17 @@ namespace PlexRequests.UI.Modules 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 Response SearchTvShow(string searchTerm) { Log.Trace("Searching for TV Show {0}", searchTerm); @@ -409,7 +420,7 @@ namespace PlexRequests.UI.Modules return Response.AsJson(viewAlbum); } - private Response RequestMovie(int movieId) + private Response RequestMovie(int movieId, bool notify = false) { var movieApi = new TheMovieDbApi(); var movieInfo = movieApi.GetMovieInformation(movieId).Result; @@ -428,6 +439,10 @@ namespace PlexRequests.UI.Modules // check if the current user is already marked as a requester for this movie, if not, add them if (!existingRequest.UserHasRequested(Username)) { + if (notify) + { + existingRequest.AddUserToNotification(Username); + } existingRequest.RequestedUsers.Add(Username); RequestService.UpdateRequest(existingRequest); } @@ -463,10 +478,16 @@ namespace PlexRequests.UI.Modules Status = movieInfo.Status, RequestedDate = DateTime.UtcNow, Approved = false, - RequestedUsers = new List() { Username }, + RequestedUsers = new List { Username }, Issues = IssueState.None, + }; + if (notify) + { + model.AddUserToNotification(Username); + } + Log.Trace(settings.DumpJson()); if (ShouldAutoApprove(RequestType.Movie, settings)) { diff --git a/PlexRequests.UI/Views/Search/Index.cshtml b/PlexRequests.UI/Views/Search/Index.cshtml index 9293f05d9..1c419cdc8 100644 --- a/PlexRequests.UI/Views/Search/Index.cshtml +++ b/PlexRequests.UI/Views/Search/Index.cshtml @@ -12,6 +12,14 @@

Want to watch something that is not currently on Plex?! No problem! Just search for it below and request it!


+
+
+ +
+
+ - +
@if (Model.SearchForMovies) @@ -136,26 +144,26 @@ {{#if_eq available true}} {{else}} - {{#if_eq requested true}} - - {{else}} - {{#if_eq type "movie"}} - - {{/if_eq}} - {{#if_eq type "tv"}} - - {{/if_eq}} - {{/if_eq}} + {{#if_eq requested true}} + + {{else}} + {{#if_eq type "movie"}} + + {{/if_eq}} + {{#if_eq type "tv"}} + + {{/if_eq}} + {{/if_eq}} {{/if_eq}}

@@ -201,11 +209,11 @@ {{#if_eq available true}} {{else}} - {{#if_eq requested true}} - - {{else}} - - {{/if_eq}} + {{#if_eq requested true}} + + {{else}} + + {{/if_eq}} {{/if_eq}}
Track Count: {{trackCount}} From 3ea708aca507c492d56367d4b2f895c3c8cb002a Mon Sep 17 00:00:00 2001 From: tidusjar Date: Mon, 25 Apr 2016 13:52:37 +0100 Subject: [PATCH 16/76] added the missing baseurl bit on the login page for #72 --- PlexRequests.UI/Views/Login/Index.cshtml | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/PlexRequests.UI/Views/Login/Index.cshtml b/PlexRequests.UI/Views/Login/Index.cshtml index 0662e2cb2..ce438d249 100644 --- a/PlexRequests.UI/Views/Login/Index.cshtml +++ b/PlexRequests.UI/Views/Login/Index.cshtml @@ -1,4 +1,13 @@ -
+@using PlexRequests.UI.Helpers +@{ + var baseUrl = Html.GetBaseUrl(); + var url = string.Empty; + if (!string.IsNullOrEmpty(baseUrl)) + { + url = "/" + baseUrl; + } +} + Username
Password @@ -10,7 +19,7 @@
@if (!Model.AdminExists) { -
If you have not yet created an Admin account you can do here: Register
+
If you have not yet created an Admin account you can do here: Register
} @if (Model.Errored) { From c50e2bb644a6fcf62418eb54b35eb82c0b40f70f Mon Sep 17 00:00:00 2001 From: tidusjar Date: Mon, 25 Apr 2016 16:41:18 +0100 Subject: [PATCH 17/76] Added the actual notification part of #27 --- .../Interfaces/INotification.cs | 7 +- .../Interfaces/INotificationService.cs | 11 +++ .../Jobs/PlexAvailabilityChecker.cs | 81 ++++++++++++++----- .../Notification/EmailMessageNotification.cs | 68 +++++++--------- .../Notification/NotificationModel.cs | 1 + .../Notification/NotificationService.cs | 11 +++ .../Notification/NotificationType.cs | 3 +- .../Notification/PushbulletNotification.cs | 34 ++------ .../Notification/PushoverNotification.cs | 32 ++------ 9 files changed, 135 insertions(+), 113 deletions(-) diff --git a/PlexRequests.Services/Interfaces/INotification.cs b/PlexRequests.Services/Interfaces/INotification.cs index 2e4e55ea4..ea3488cfd 100644 --- a/PlexRequests.Services/Interfaces/INotification.cs +++ b/PlexRequests.Services/Interfaces/INotification.cs @@ -36,7 +36,12 @@ namespace PlexRequests.Services.Interfaces string NotificationName { get; } Task NotifyAsync(NotificationModel model); - + /// + /// Sends a notification to the user, this is usually for testing the settings. + /// + /// The model. + /// The settings. + /// Task NotifyAsync(NotificationModel model, Settings settings); } } \ No newline at end of file diff --git a/PlexRequests.Services/Interfaces/INotificationService.cs b/PlexRequests.Services/Interfaces/INotificationService.cs index 91563c6de..66f0853cd 100644 --- a/PlexRequests.Services/Interfaces/INotificationService.cs +++ b/PlexRequests.Services/Interfaces/INotificationService.cs @@ -33,7 +33,18 @@ namespace PlexRequests.Services.Interfaces { public interface INotificationService { + /// + /// Sends a notification to the user. This one is used in normal notification scenarios + /// + /// The model. + /// Task Publish(NotificationModel model); + /// + /// Sends a notification to the user, this is usually for testing the settings. + /// + /// The model. + /// The settings. + /// Task Publish(NotificationModel model, Settings settings); void Subscribe(INotification notification); void UnSubscribe(INotification notification); diff --git a/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs b/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs index b47651123..54feb6262 100644 --- a/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs +++ b/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs @@ -37,6 +37,7 @@ using PlexRequests.Core.SettingModels; using PlexRequests.Helpers; using PlexRequests.Services.Interfaces; using PlexRequests.Services.Models; +using PlexRequests.Services.Notification; using PlexRequests.Store; using Quartz; @@ -45,13 +46,15 @@ namespace PlexRequests.Services.Jobs { public class PlexAvailabilityChecker : IJob, IAvailabilityChecker { - public PlexAvailabilityChecker(ISettingsService plexSettings, ISettingsService auth, IRequestService request, IPlexApi plex, ICacheProvider cache) + public PlexAvailabilityChecker(ISettingsService plexSettings, ISettingsService auth, IRequestService request, IPlexApi plex, ICacheProvider cache, + INotificationService notify) { Plex = plexSettings; Auth = auth; RequestService = request; PlexApi = plex; Cache = cache; + Notification = notify; } private ISettingsService Plex { get; } @@ -60,6 +63,7 @@ namespace PlexRequests.Services.Jobs private static Logger Log = LogManager.GetCurrentClassLogger(); private IPlexApi PlexApi { get; } private ICacheProvider Cache { get; } + private INotificationService Notification { get; } public void CheckAndUpdateAll() { @@ -67,7 +71,7 @@ namespace PlexRequests.Services.Jobs var plexSettings = Plex.GetSettings(); var authSettings = Auth.GetSettings(); Log.Trace("Getting all the requests"); - + if (!ValidateSettings(plexSettings, authSettings)) { Log.Info("Validation of the plex settings failed."); @@ -95,22 +99,12 @@ namespace PlexRequests.Services.Jobs Log.Info("There are no requests to check."); return; } - + var modifiedModel = new List(); foreach (var r in requestedModels) { Log.Trace("We are going to see if Plex has the following title: {0}", r.Title); - if (libraries == null) - { - libraries = new List { PlexApi.SearchContent(authSettings.PlexAuthToken, r.Title, plexSettings.FullUri) }; - if (libraries.Count == 0) - { - Log.Trace("Could not find any matching result for this title."); - continue; - } - } - Log.Trace("Search results from Plex for the following request: {0}", r.Title); var releaseDate = r.ReleaseDate == DateTime.MinValue ? string.Empty : r.ReleaseDate.ToString("yyyy"); @@ -130,7 +124,7 @@ namespace PlexRequests.Services.Jobs default: throw new ArgumentOutOfRangeException(); } - + if (matchResult) { r.Available = true; @@ -146,8 +140,10 @@ namespace PlexRequests.Services.Jobs if (modifiedModel.Any()) { + NotifyUsers(modifiedModel, authSettings.PlexAuthToken); RequestService.BatchUpdate(modifiedModel); } + } public List GetPlexMovies() @@ -205,8 +201,8 @@ namespace PlexRequests.Services.Jobs public bool IsTvShowAvailable(PlexTvShow[] plexShows, string title, string year) { - return plexShows.Any(x => - (x.Title.Equals(title, StringComparison.CurrentCultureIgnoreCase) || x.Title.StartsWith(title, StringComparison.CurrentCultureIgnoreCase)) && + return plexShows.Any(x => + (x.Title.Equals(title, StringComparison.CurrentCultureIgnoreCase) || x.Title.StartsWith(title, StringComparison.CurrentCultureIgnoreCase)) && x.ReleaseYear.Equals(year, StringComparison.CurrentCultureIgnoreCase)); } @@ -237,8 +233,8 @@ namespace PlexRequests.Services.Jobs public bool IsAlbumAvailable(PlexAlbum[] plexAlbums, string title, string year, string artist) { - return plexAlbums.Any(x => - x.Title.Contains(title) && + return plexAlbums.Any(x => + x.Title.Contains(title) && //x.ReleaseYear.Equals(year, StringComparison.CurrentCultureIgnoreCase) && x.Artist.Equals(artist, StringComparison.CurrentCultureIgnoreCase)); } @@ -271,7 +267,8 @@ namespace PlexRequests.Services.Jobs else { Log.Trace("Plex Lib GetSet Call"); - results = Cache.GetOrSet(CacheKeys.PlexLibaries, () => { + results = Cache.GetOrSet(CacheKeys.PlexLibaries, () => + { Log.Trace("Plex Lib API Call (inside getset)"); return GetLibraries(authSettings, plexSettings); }, CacheKeys.TimeFrameMinutes.SchedulerCaching); @@ -281,7 +278,7 @@ namespace PlexRequests.Services.Jobs { Log.Error(ex, "Failed to obtain Plex libraries"); } - + return results; } @@ -305,7 +302,7 @@ namespace PlexRequests.Services.Jobs Log.Trace("Returning Plex Libs"); return libs; - } + } private bool ValidateSettings(PlexSettings plex, AuthenticationSettings auth) { @@ -317,9 +314,49 @@ namespace PlexRequests.Services.Jobs return true; } + private void NotifyUsers(IEnumerable modelChanged, string apiKey) + { + try + { + var plexUser = PlexApi.GetUsers(apiKey); + if (plexUser?.User == null || plexUser.User.Length == 0) + { + return; + } + + foreach (var model in modelChanged) + { + var usersToNotify = model.UsersToNotify; // Users that selected the notification button when requesting a movie/tv show + foreach (var user in usersToNotify) + { + var email = plexUser.User.FirstOrDefault(x => x.Username == user); + if (email == null) + { + // We do not have a plex user that requested this! + continue; + } + var notificationModel = new NotificationModel + { + User = email.Username, + UserEmail = email.Email, + NotificationType = NotificationType.RequestAvailable, + Title = model.Title + }; + + // Send the notification to the user. + Notification.Publish(notificationModel); + } + } + } + catch (Exception e) + { + Log.Error(e); + } + } + public void Execute(IJobExecutionContext context) { - CheckAndUpdateAll(); + CheckAndUpdateAll(); } } } \ No newline at end of file diff --git a/PlexRequests.Services/Notification/EmailMessageNotification.cs b/PlexRequests.Services/Notification/EmailMessageNotification.cs index 4a359fb23..cd4c6b0a2 100644 --- a/PlexRequests.Services/Notification/EmailMessageNotification.cs +++ b/PlexRequests.Services/Notification/EmailMessageNotification.cs @@ -71,8 +71,8 @@ namespace PlexRequests.Services.Notification await EmailIssue(model, emailSettings); break; case NotificationType.RequestAvailable: - throw new NotImplementedException(); - + await EmailAvailableRequest(model, emailSettings); + break; case NotificationType.RequestApproved: throw new NotImplementedException(); @@ -120,23 +120,7 @@ namespace PlexRequests.Services.Notification Subject = $"Plex Requests: New request for {model.Title}!" }; - try - { - using (var smtp = new SmtpClient(settings.EmailHost, settings.EmailPort)) - { - smtp.Credentials = new NetworkCredential(settings.EmailUsername, settings.EmailPassword); - smtp.EnableSsl = settings.Ssl; - await smtp.SendMailAsync(message).ConfigureAwait(false); - } - } - catch (SmtpException smtp) - { - Log.Error(smtp); - } - catch (Exception e) - { - Log.Error(e); - } + await Send(message, settings); } private async Task EmailIssue(NotificationModel model, EmailNotificationSettings settings) @@ -146,10 +130,34 @@ namespace PlexRequests.Services.Notification IsBodyHtml = true, To = { new MailAddress(settings.RecipientEmail) }, Body = $"Hello! The user '{model.User}' has reported a new issue {model.Body} for the title {model.Title}!", - From = new MailAddress(settings.RecipientEmail), + From = new MailAddress(settings.EmailSender), Subject = $"Plex Requests: New issue for {model.Title}!" }; + await Send(message, settings); + } + + private async Task EmailAvailableRequest(NotificationModel model, EmailNotificationSettings settings) + { + if (!settings.EnableUserEmailNotifications) + { + await Task.FromResult(false); + } + + var message = new MailMessage + { + IsBodyHtml = true, + To = { new MailAddress(model.UserEmail) }, + Body = $"Hello! You requested {model.Title} on PlexRequests! This is now available on Plex! :)", + From = new MailAddress(settings.EmailSender), + Subject = $"Plex Requests: {model.Title} is now available!" + }; + + await Send(message, settings); + } + + private async Task Send(MailMessage message, EmailNotificationSettings settings) + { try { using (var smtp = new SmtpClient(settings.EmailHost, settings.EmailPort)) @@ -176,27 +184,11 @@ namespace PlexRequests.Services.Notification IsBodyHtml = true, To = { new MailAddress(settings.RecipientEmail) }, Body = "This is just a test! Success!", - From = new MailAddress(settings.RecipientEmail), + From = new MailAddress(settings.EmailSender), Subject = "Plex Requests: Test Message!" }; - try - { - using (var smtp = new SmtpClient(settings.EmailHost, settings.EmailPort)) - { - smtp.Credentials = new NetworkCredential(settings.EmailUsername, settings.EmailPassword); - smtp.EnableSsl = settings.Ssl; - await smtp.SendMailAsync(message).ConfigureAwait(false); - } - } - catch (SmtpException smtp) - { - Log.Error(smtp); - } - catch (Exception e) - { - Log.Error(e); - } + await Send(message, settings); } } } \ No newline at end of file diff --git a/PlexRequests.Services/Notification/NotificationModel.cs b/PlexRequests.Services/Notification/NotificationModel.cs index 264d3e609..2cc4e7fdb 100644 --- a/PlexRequests.Services/Notification/NotificationModel.cs +++ b/PlexRequests.Services/Notification/NotificationModel.cs @@ -35,5 +35,6 @@ namespace PlexRequests.Services.Notification public DateTime DateTime { get; set; } public NotificationType NotificationType { get; set; } public string User { get; set; } + public string UserEmail { get; set; } } } \ No newline at end of file diff --git a/PlexRequests.Services/Notification/NotificationService.cs b/PlexRequests.Services/Notification/NotificationService.cs index 35e52fd7d..3e52e43fa 100644 --- a/PlexRequests.Services/Notification/NotificationService.cs +++ b/PlexRequests.Services/Notification/NotificationService.cs @@ -41,6 +41,11 @@ namespace PlexRequests.Services.Notification private static Logger Log = LogManager.GetCurrentClassLogger(); public ConcurrentDictionary Observers { get; } = new ConcurrentDictionary(); + /// + /// Sends a notification to the user. This one is used in normal notification scenarios + /// + /// The model. + /// public async Task Publish(NotificationModel model) { var notificationTasks = Observers.Values.Select(notification => NotifyAsync(notification, model)); @@ -48,6 +53,12 @@ namespace PlexRequests.Services.Notification await Task.WhenAll(notificationTasks).ConfigureAwait(false); } + /// + /// Sends a notification to the user, this is usually for testing the settings. + /// + /// The model. + /// The settings. + /// public async Task Publish(NotificationModel model, Settings settings) { var notificationTasks = Observers.Values.Select(notification => NotifyAsync(notification, model, settings)); diff --git a/PlexRequests.Services/Notification/NotificationType.cs b/PlexRequests.Services/Notification/NotificationType.cs index 22d0d29b1..5e7b32370 100644 --- a/PlexRequests.Services/Notification/NotificationType.cs +++ b/PlexRequests.Services/Notification/NotificationType.cs @@ -33,6 +33,7 @@ namespace PlexRequests.Services.Notification RequestAvailable, RequestApproved, AdminNote, - Test + Test, + } } diff --git a/PlexRequests.Services/Notification/PushbulletNotification.cs b/PlexRequests.Services/Notification/PushbulletNotification.cs index 521855dca..f4d6f802e 100644 --- a/PlexRequests.Services/Notification/PushbulletNotification.cs +++ b/PlexRequests.Services/Notification/PushbulletNotification.cs @@ -45,7 +45,6 @@ namespace PlexRequests.Services.Notification } private IPushbulletApi PushbulletApi { get; } private ISettingsService SettingsService { get; } - private PushbulletNotificationSettings Settings => GetSettings(); private static Logger Log = LogManager.GetCurrentClassLogger(); public string NotificationName => "PushbulletNotification"; @@ -107,45 +106,28 @@ namespace PlexRequests.Services.Notification { var message = $"{model.Title} has been requested by user: {model.User}"; var pushTitle = $"Plex Requests: {model.Title} has been requested!"; - try - { - var result = await PushbulletApi.PushAsync(settings.AccessToken, pushTitle, message, settings.DeviceIdentifier); - if (result == null) - { - Log.Error("Pushbullet api returned a null value, the notification did not get pushed"); - } - } - catch (Exception e) - { - Log.Error(e); - } + await Push(settings, message, pushTitle); } private async Task PushIssueAsync(NotificationModel model, PushbulletNotificationSettings settings) { var message = $"A new issue: {model.Body} has been reported by user: {model.User} for the title: {model.Title}"; var pushTitle = $"Plex Requests: A new issue has been reported for {model.Title}"; - try - { - var result = await PushbulletApi.PushAsync(settings.AccessToken, pushTitle, message, settings.DeviceIdentifier); - if (result != null) - { - Log.Error("Pushbullet api returned a null value, the notification did not get pushed"); - } - } - catch (Exception e) - { - Log.Error(e); - } + await Push(settings, message, pushTitle); } private async Task PushTestAsync(NotificationModel model, PushbulletNotificationSettings settings) { var message = "This is just a test! Success!"; var pushTitle = "Plex Requests: Test Message!"; + await Push(settings, message, pushTitle); + } + + private async Task Push(PushbulletNotificationSettings settings, string message, string title) + { try { - var result = await PushbulletApi.PushAsync(settings.AccessToken, pushTitle, message, settings.DeviceIdentifier); + var result = await PushbulletApi.PushAsync(settings.AccessToken, title, message, settings.DeviceIdentifier); if (result != null) { Log.Error("Pushbullet api returned a null value, the notification did not get pushed"); diff --git a/PlexRequests.Services/Notification/PushoverNotification.cs b/PlexRequests.Services/Notification/PushoverNotification.cs index 47854b1d5..d39492563 100644 --- a/PlexRequests.Services/Notification/PushoverNotification.cs +++ b/PlexRequests.Services/Notification/PushoverNotification.cs @@ -45,7 +45,6 @@ namespace PlexRequests.Services.Notification } private IPushoverApi PushoverApi { get; } private ISettingsService SettingsService { get; } - private PushoverNotificationSettings Settings => GetSettings(); private static Logger Log = LogManager.GetCurrentClassLogger(); public string NotificationName => "PushoverNotification"; @@ -106,40 +105,23 @@ namespace PlexRequests.Services.Notification private async Task PushNewRequestAsync(NotificationModel model, PushoverNotificationSettings settings) { var message = $"Plex Requests: {model.Title} has been requested by user: {model.User}"; - try - { - var result = await PushoverApi.PushAsync(settings.AccessToken, message, settings.UserToken); - if (result?.status != 1) - { - Log.Error("Pushover api returned a status that was not 1, the notification did not get pushed"); - } - } - catch (Exception e) - { - Log.Error(e); - } + await Push(settings, message); } private async Task PushIssueAsync(NotificationModel model, PushoverNotificationSettings settings) { var message = $"Plex Requests: A new issue: {model.Body} has been reported by user: {model.User} for the title: {model.Title}"; - try - { - var result = await PushoverApi.PushAsync(settings.AccessToken, message, settings.UserToken); - if (result?.status != 1) - { - Log.Error("Pushover api returned a status that was not 1, the notification did not get pushed"); - } - } - catch (Exception e) - { - Log.Error(e); - } + await Push(settings, message); } private async Task PushTestAsync(NotificationModel model, PushoverNotificationSettings settings) { var message = $"Plex Requests: Test Message!"; + await Push(settings, message); + } + + private async Task Push(PushoverNotificationSettings settings, string message) + { try { var result = await PushoverApi.PushAsync(settings.AccessToken, message, settings.UserToken); From d07f544099af5802937c54307bd20ca313418463 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Mon, 25 Apr 2016 16:46:59 +0100 Subject: [PATCH 18/76] Fixed bug --- PlexRequests.UI/Views/Login/Index.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PlexRequests.UI/Views/Login/Index.cshtml b/PlexRequests.UI/Views/Login/Index.cshtml index ce438d249..83d349d2e 100644 --- a/PlexRequests.UI/Views/Login/Index.cshtml +++ b/PlexRequests.UI/Views/Login/Index.cshtml @@ -1,6 +1,6 @@ @using PlexRequests.UI.Helpers @{ - var baseUrl = Html.GetBaseUrl(); + var baseUrl = Html.GetBaseUrl().ToHtmlString(); var url = string.Empty; if (!string.IsNullOrEmpty(baseUrl)) { From 4c59e9d19c298f7a26848e2d70b2eeb4d6abc365 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Mon, 25 Apr 2016 17:01:39 +0100 Subject: [PATCH 19/76] #27 added TV Search to the notification --- PlexRequests.UI/Content/search.js | 5 ++++- PlexRequests.UI/Modules/SearchModule.cs | 13 +++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/PlexRequests.UI/Content/search.js b/PlexRequests.UI/Content/search.js index 363e79689..55de573eb 100644 --- a/PlexRequests.UI/Content/search.js +++ b/PlexRequests.UI/Content/search.js @@ -78,9 +78,12 @@ $(function () { if (seasons === "1") { // Send over the first season data = data + "&seasons=first"; - } + var $notify = $('#notifyUser').is(':checked'); + + data = data + "¬ify=" + $notify; + var type = $form.prop('method'); var url = $form.prop('action'); diff --git a/PlexRequests.UI/Modules/SearchModule.cs b/PlexRequests.UI/Modules/SearchModule.cs index 8f9e1ed4c..07c96bf49 100644 --- a/PlexRequests.UI/Modules/SearchModule.cs +++ b/PlexRequests.UI/Modules/SearchModule.cs @@ -98,7 +98,7 @@ namespace PlexRequests.UI.Modules Get["movie/playing"] = parameters => CurrentlyPlayingMovies(); Post["request/movie"] = parameters => RequestMovie((int)Request.Form.movieId, (bool)Request.Form.notify); - Post["request/tv"] = parameters => RequestTvShow((int)Request.Form.tvId, (string)Request.Form.seasons); + Post["request/tv"] = parameters => RequestTvShow((int)Request.Form.tvId, (string)Request.Form.seasons, (bool)Request.Form.notify); Post["request/album"] = parameters => RequestAlbum((string)Request.Form.albumId); } private IPlexApi PlexApi { get; } @@ -568,8 +568,9 @@ namespace PlexRequests.UI.Modules ///
/// The show identifier. /// The seasons. + /// if set to true [notify]. /// - private Response RequestTvShow(int showId, string seasons) + private Response RequestTvShow(int showId, string seasons, bool notify) { var tvApi = new TvMazeApi(); @@ -589,6 +590,10 @@ namespace PlexRequests.UI.Modules // check if the current user is already marked as a requester for this show, if not, add them if (!existingRequest.UserHasRequested(Username)) { + if (notify) + { + existingRequest.AddUserToNotification(Username); + } existingRequest.RequestedUsers.Add(Username); RequestService.UpdateRequest(existingRequest); } @@ -626,6 +631,10 @@ namespace PlexRequests.UI.Modules ImdbId = showInfo.externals?.imdb ?? string.Empty, SeasonCount = showInfo.seasonCount }; + if (notify) + { + model.AddUserToNotification(Username); + } var seasonsList = new List(); switch (seasons) { From 230fa5ba3bae53d8446afef4b70f8010c2105e46 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Mon, 25 Apr 2016 17:03:03 +0100 Subject: [PATCH 20/76] Added #27 to albums --- PlexRequests.UI/Content/search.js | 3 +++ PlexRequests.UI/Modules/SearchModule.cs | 13 ++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/PlexRequests.UI/Content/search.js b/PlexRequests.UI/Content/search.js index 55de573eb..ad1dcf298 100644 --- a/PlexRequests.UI/Content/search.js +++ b/PlexRequests.UI/Content/search.js @@ -142,6 +142,9 @@ $(function () { var type = $form.prop('method'); var url = $form.prop('action'); var data = $form.serialize(); + var $notify = $('#notifyUser').is(':checked'); + + data = data + "¬ify=" + $notify; sendRequestAjax(data, type, url, buttonId); }); diff --git a/PlexRequests.UI/Modules/SearchModule.cs b/PlexRequests.UI/Modules/SearchModule.cs index 07c96bf49..7943f642f 100644 --- a/PlexRequests.UI/Modules/SearchModule.cs +++ b/PlexRequests.UI/Modules/SearchModule.cs @@ -99,7 +99,7 @@ namespace PlexRequests.UI.Modules Post["request/movie"] = parameters => RequestMovie((int)Request.Form.movieId, (bool)Request.Form.notify); Post["request/tv"] = parameters => RequestTvShow((int)Request.Form.tvId, (string)Request.Form.seasons, (bool)Request.Form.notify); - Post["request/album"] = parameters => RequestAlbum((string)Request.Form.albumId); + Post["request/album"] = parameters => RequestAlbum((string)Request.Form.albumId, (bool)Request.Form.notify); } private IPlexApi PlexApi { get; } private TheMovieDbApi MovieApi { get; } @@ -706,7 +706,7 @@ namespace PlexRequests.UI.Modules return Response.AsJson(new JsonResponseModel { Result = true, Message = $"{fullShowName} was successfully added!" }); } - private Response RequestAlbum(string releaseId) + private Response RequestAlbum(string releaseId, bool notify) { var settings = PrService.GetSettings(); var existingRequest = RequestService.CheckRequest(releaseId); @@ -717,6 +717,10 @@ namespace PlexRequests.UI.Modules Log.Debug("We do have an existing album request"); if (!existingRequest.UserHasRequested(Username)) { + if (notify) + { + existingRequest.AddUserToNotification(Username); + } Log.Debug("Not in the requested list so adding them and updating the request. User: {0}", Username); existingRequest.RequestedUsers.Add(Username); RequestService.UpdateRequest(existingRequest); @@ -774,7 +778,10 @@ namespace PlexRequests.UI.Modules ArtistId = artist.id }; - + if (notify) + { + existingRequest.AddUserToNotification(Username); + } if (ShouldAutoApprove(RequestType.Album, settings)) { Log.Debug("We don't require approval OR the user is in the whitelist"); From 4960063cca71b11126b86723a0d710cf16515e79 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Mon, 25 Apr 2016 17:03:30 +0100 Subject: [PATCH 21/76] fixed issue in #27 with albums --- PlexRequests.UI/Modules/SearchModule.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PlexRequests.UI/Modules/SearchModule.cs b/PlexRequests.UI/Modules/SearchModule.cs index 7943f642f..094f07171 100644 --- a/PlexRequests.UI/Modules/SearchModule.cs +++ b/PlexRequests.UI/Modules/SearchModule.cs @@ -780,7 +780,7 @@ namespace PlexRequests.UI.Modules if (notify) { - existingRequest.AddUserToNotification(Username); + model.AddUserToNotification(Username); } if (ShouldAutoApprove(RequestType.Album, settings)) { From a004eca6ece5f1d42853cf1d5a3963550cda5313 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Thu, 28 Apr 2016 13:06:40 +0100 Subject: [PATCH 22/76] Fixed #185 --- PlexRequests.UI/Modules/AdminModule.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PlexRequests.UI/Modules/AdminModule.cs b/PlexRequests.UI/Modules/AdminModule.cs index f30d0cee3..71885bdc5 100644 --- a/PlexRequests.UI/Modules/AdminModule.cs +++ b/PlexRequests.UI/Modules/AdminModule.cs @@ -270,7 +270,7 @@ namespace PlexRequests.UI.Modules return Response.AsJson(string.Empty); } - var usernames = users.User.Select(x => x.Username); + var usernames = users.User.Select(x => x.Title); return Response.AsJson(usernames); } From af2c661d6170e753c9f91d647e7c3073de3131db Mon Sep 17 00:00:00 2001 From: tidusjar Date: Thu, 28 Apr 2016 14:20:35 +0100 Subject: [PATCH 23/76] Finished #186 --- PlexRequests.Services/Jobs/StoreBackup.cs | 88 +++++++++++++++++++ .../PlexRequests.Services.csproj | 2 + PlexRequests.Store/DbConfiguration.cs | 4 +- PlexRequests.Store/ISqliteConfiguration.cs | 5 +- PlexRequests.UI/Jobs/Scheduler.cs | 10 +++ 5 files changed, 106 insertions(+), 3 deletions(-) create mode 100644 PlexRequests.Services/Jobs/StoreBackup.cs diff --git a/PlexRequests.Services/Jobs/StoreBackup.cs b/PlexRequests.Services/Jobs/StoreBackup.cs new file mode 100644 index 000000000..2d63987b0 --- /dev/null +++ b/PlexRequests.Services/Jobs/StoreBackup.cs @@ -0,0 +1,88 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: StoreBackup.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.Linq; + +using NLog; + +using PlexRequests.Store; + +using Quartz; + +using Directory = System.IO.Directory; + +namespace PlexRequests.Services.Jobs +{ + public class StoreBackup : IJob + { + public StoreBackup(ISqliteConfiguration sql) + { + Sql = sql; + } + + private ISqliteConfiguration Sql { get; } + + private static Logger Log = LogManager.GetCurrentClassLogger(); + + public void Execute(IJobExecutionContext context) + { + TakeBackup(); + } + + private void TakeBackup() + { + Log.Trace("Starting DB Backup"); + var dbPath = Sql.CurrentPath; + var dir = Path.GetDirectoryName(dbPath); + if (dir == null) + { + Log.Warn("We couldn't find the DB path. We cannot backup."); + return; + } + var backupDir = Directory.CreateDirectory(Path.Combine(dir, "Backup")); + + + if (string.IsNullOrEmpty(dbPath)) + { + Log.Warn("Could not find the actual database. We cannot backup."); + return; + } + + try + { + File.Copy(dbPath, Path.Combine(backupDir.FullName, $"PlexRequests.sqlite_{DateTime.Now.ToString("yyyy-MM-dd hh.mm.ss")}.bak")); + } + catch (Exception e) + { + Log.Warn(e); + Log.Warn("Exception when trying to copy the backup."); + } + + } + } +} \ No newline at end of file diff --git a/PlexRequests.Services/PlexRequests.Services.csproj b/PlexRequests.Services/PlexRequests.Services.csproj index 49650d33e..1b088294a 100644 --- a/PlexRequests.Services/PlexRequests.Services.csproj +++ b/PlexRequests.Services/PlexRequests.Services.csproj @@ -84,6 +84,7 @@ + @@ -92,6 +93,7 @@ + diff --git a/PlexRequests.Store/DbConfiguration.cs b/PlexRequests.Store/DbConfiguration.cs index 7ddd60483..27e7f20f9 100644 --- a/PlexRequests.Store/DbConfiguration.cs +++ b/PlexRequests.Store/DbConfiguration.cs @@ -44,7 +44,7 @@ namespace PlexRequests.Store } private SqliteFactory Factory { get; } - private string CurrentPath =>Path.Combine(Path.GetDirectoryName(Application.ExecutablePath) ?? string.Empty, DbFile); + public string CurrentPath =>Path.Combine(Path.GetDirectoryName(Application.ExecutablePath) ?? string.Empty, DbFile); public virtual bool CheckDb() { @@ -59,7 +59,7 @@ namespace PlexRequests.Store return false; } - public string DbFile = "PlexRequests.sqlite"; + public const string DbFile = "PlexRequests.sqlite"; /// /// Gets the database connection. diff --git a/PlexRequests.Store/ISqliteConfiguration.cs b/PlexRequests.Store/ISqliteConfiguration.cs index 19c5b6756..528944e95 100644 --- a/PlexRequests.Store/ISqliteConfiguration.cs +++ b/PlexRequests.Store/ISqliteConfiguration.cs @@ -45,6 +45,9 @@ namespace PlexRequests.Store /// Creates the database. /// void CreateDatabase(); - } + + string CurrentPath { get; } + + } } diff --git a/PlexRequests.UI/Jobs/Scheduler.cs b/PlexRequests.UI/Jobs/Scheduler.cs index 5a91733e1..0ccae710b 100644 --- a/PlexRequests.UI/Jobs/Scheduler.cs +++ b/PlexRequests.UI/Jobs/Scheduler.cs @@ -56,11 +56,13 @@ namespace PlexRequests.UI.Jobs var sickrage = JobBuilder.Create().WithIdentity("SickRageCacher", "Cache").Build(); var sonarr = JobBuilder.Create().WithIdentity("SonarrCacher", "Cache").Build(); var cp = JobBuilder.Create().WithIdentity("CouchPotatoCacher", "Cache").Build(); + var store = JobBuilder.Create().WithIdentity("StoreBackup", "Backup").Build(); jobs.Add(plex); jobs.Add(sickrage); jobs.Add(sonarr); jobs.Add(cp); + jobs.Add(store); return jobs; } @@ -125,11 +127,19 @@ namespace PlexRequests.UI.Jobs .WithSimpleSchedule(x => x.WithIntervalInMinutes(10).RepeatForever()) .Build(); + var storeBackup = + TriggerBuilder.Create() + .WithIdentity("StoreBackup", "Backup") + .StartNow() + .WithSimpleSchedule(x => x.WithIntervalInHours(24).RepeatForever()) + .Build(); + triggers.Add(plexAvailabilityChecker); triggers.Add(srCacher); triggers.Add(sonarrCacher); triggers.Add(cpCacher); + triggers.Add(storeBackup); return triggers; } From af028f0b564b9ce7f2c185cb9cd6607d17f841a6 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Thu, 28 Apr 2016 14:23:09 +0100 Subject: [PATCH 24/76] Did the login bit on #185 --- PlexRequests.UI/Modules/UserLoginModule.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/PlexRequests.UI/Modules/UserLoginModule.cs b/PlexRequests.UI/Modules/UserLoginModule.cs index 48adb3989..1bf57150f 100644 --- a/PlexRequests.UI/Modules/UserLoginModule.cs +++ b/PlexRequests.UI/Modules/UserLoginModule.cs @@ -173,10 +173,10 @@ namespace PlexRequests.UI.Modules private bool CheckIfUserIsInPlexFriends(string username, string authToken) { var users = Api.GetUsers(authToken); - Log.Debug("Plex Users: "); - Log.Debug(users.DumpJson()); - var allUsers = users?.User?.Where(x => !string.IsNullOrEmpty(x.Username)); - return allUsers != null && allUsers.Any(x => x.Username.Equals(username, StringComparison.CurrentCultureIgnoreCase)); + Log.Trace("Plex Users: "); + Log.Trace(users.DumpJson()); + var allUsers = users?.User?.Where(x => !string.IsNullOrEmpty(x.Title)); + return allUsers != null && allUsers.Any(x => x.Title.Equals(username, StringComparison.CurrentCultureIgnoreCase)); } private bool IsUserInDeniedList(string username, AuthenticationSettings settings) From df3dc4ac04ebe32942e342866c427b68789a4112 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Fri, 29 Apr 2016 14:08:30 +0100 Subject: [PATCH 25/76] - Added a visual indication on the UI to tell the admin there is a update available. - We are now also recording the last scheduled run in the database --- PlexRequests.Core/CacheKeys.cs | 2 + PlexRequests.Core/JsonRequestService.cs | 3 +- PlexRequests.Core/Models/StatusModel.cs | 10 --- PlexRequests.Core/Setup.cs | 10 ++- .../Interfaces/IJobRecord.cs | 33 ++++++++ .../Jobs/CouchPotatoCacher.cs | 8 +- PlexRequests.Services/Jobs/JobNames.cs | 37 +++++++++ PlexRequests.Services/Jobs/JobRecord.cs | 59 ++++++++++++++ .../Jobs/PlexAvailabilityChecker.cs | 6 +- PlexRequests.Services/Jobs/SickRageCacher.cs | 8 +- PlexRequests.Services/Jobs/SonarrCacher.cs | 10 ++- PlexRequests.Services/Jobs/StoreBackup.cs | 11 ++- .../PlexRequests.Services.csproj | 3 + PlexRequests.Store/Models/ScheduledJobs.cs | 39 +++++++++ PlexRequests.Store/PlexRequests.Store.csproj | 1 + PlexRequests.Store/SqlTables.sql | 9 ++- PlexRequests.UI/Bootstrapper.cs | 2 + PlexRequests.UI/Content/custom.css | 4 + PlexRequests.UI/Content/custom.min.css | 2 +- PlexRequests.UI/Content/custom.scss | 8 +- PlexRequests.UI/Jobs/CustomJobFactory.cs | 2 +- .../Models/JsonUpdateAvailableModel.cs | 33 ++++++++ PlexRequests.UI/Modules/AdminModule.cs | 14 ++-- PlexRequests.UI/Modules/BaseAuthModule.cs | 7 ++ PlexRequests.UI/Modules/SearchModule.cs | 12 +-- .../Modules/UpdateCheckerModule.cs | 79 +++++++++++++++++++ PlexRequests.UI/PlexRequests.UI.csproj | 2 + PlexRequests.UI/Views/Shared/_Layout.cshtml | 23 +++++- 28 files changed, 392 insertions(+), 45 deletions(-) create mode 100644 PlexRequests.Services/Interfaces/IJobRecord.cs create mode 100644 PlexRequests.Services/Jobs/JobNames.cs create mode 100644 PlexRequests.Services/Jobs/JobRecord.cs create mode 100644 PlexRequests.Store/Models/ScheduledJobs.cs create mode 100644 PlexRequests.UI/Models/JsonUpdateAvailableModel.cs create mode 100644 PlexRequests.UI/Modules/UpdateCheckerModule.cs diff --git a/PlexRequests.Core/CacheKeys.cs b/PlexRequests.Core/CacheKeys.cs index 439953d94..6bb8f4c66 100644 --- a/PlexRequests.Core/CacheKeys.cs +++ b/PlexRequests.Core/CacheKeys.cs @@ -47,5 +47,7 @@ namespace PlexRequests.Core public const string CouchPotatoQueued = "CouchPotatoQueued"; public const string GetBaseUrl = "GetBaseUrl"; + + public const string LastestProductVersion = "LatestProductVersion"; } } \ No newline at end of file diff --git a/PlexRequests.Core/JsonRequestService.cs b/PlexRequests.Core/JsonRequestService.cs index 4808cde3b..deefdfccd 100644 --- a/PlexRequests.Core/JsonRequestService.cs +++ b/PlexRequests.Core/JsonRequestService.cs @@ -49,10 +49,9 @@ namespace PlexRequests.Core var entity = new RequestBlobs { Type = model.Type, Content = ByteConverterHelper.ReturnBytes(model), ProviderId = model.ProviderId }; var id = Repo.Insert(entity); - // TODO Keep an eye on this, since we are now doing 2 DB update for 1 single request, inserting and then updating model.Id = (int)id; - entity = new RequestBlobs { Type = model.Type, Content = ByteConverterHelper.ReturnBytes(model), ProviderId = model.ProviderId, Id = (int)id, MusicId = model.MusicBrainzId}; + entity = new RequestBlobs { Type = model.Type, Content = ByteConverterHelper.ReturnBytes(model), ProviderId = model.ProviderId, Id = (int)id, MusicId = model.MusicBrainzId }; var result = Repo.Update(entity); return result ? id : -1; diff --git a/PlexRequests.Core/Models/StatusModel.cs b/PlexRequests.Core/Models/StatusModel.cs index 1e04b35e6..62047126e 100644 --- a/PlexRequests.Core/Models/StatusModel.cs +++ b/PlexRequests.Core/Models/StatusModel.cs @@ -24,21 +24,11 @@ // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // ************************************************************************/ #endregion - -using System.Text.RegularExpressions; - namespace PlexRequests.Core.Models { public class StatusModel { public string Version { get; set; } - public int DBVersion { - get - { - string trimStatus = new Regex("[^0-9]", RegexOptions.Compiled).Replace(Version, string.Empty).PadRight(4, '0'); - return int.Parse(trimStatus); - } - } public bool UpdateAvailable { get; set; } public string UpdateUri { get; set; } public string DownloadUri { get; set; } diff --git a/PlexRequests.Core/Setup.cs b/PlexRequests.Core/Setup.cs index a73a5752d..f00f2a144 100644 --- a/PlexRequests.Core/Setup.cs +++ b/PlexRequests.Core/Setup.cs @@ -28,6 +28,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.RegularExpressions; using Mono.Data.Sqlite; using NLog; @@ -72,18 +73,19 @@ namespace PlexRequests.Core private int CheckSchema() { - var checker = new StatusChecker(); - var status = checker.GetStatus(); + var productVersion = AssemblyHelper.GetProductVersion(); + var trimStatus = new Regex("[^0-9]", RegexOptions.Compiled).Replace(productVersion, string.Empty).PadRight(4, '0'); + var version = int.Parse(trimStatus); var connection = Db.DbConnection(); var schema = connection.GetSchemaVersion(); if (schema == null) { - connection.CreateSchema(status.DBVersion); // Set the default. + connection.CreateSchema(version); // Set the default. schema = connection.GetSchemaVersion(); } - var version = schema.SchemaVersion; + version = schema.SchemaVersion; return version; } diff --git a/PlexRequests.Services/Interfaces/IJobRecord.cs b/PlexRequests.Services/Interfaces/IJobRecord.cs new file mode 100644 index 000000000..66c3be88c --- /dev/null +++ b/PlexRequests.Services/Interfaces/IJobRecord.cs @@ -0,0 +1,33 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: IJobRecord.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 PlexRequests.Services.Interfaces +{ + public interface IJobRecord + { + void Record(string jobName); + } +} \ No newline at end of file diff --git a/PlexRequests.Services/Jobs/CouchPotatoCacher.cs b/PlexRequests.Services/Jobs/CouchPotatoCacher.cs index d31bb7aa4..6b905a9b3 100644 --- a/PlexRequests.Services/Jobs/CouchPotatoCacher.cs +++ b/PlexRequests.Services/Jobs/CouchPotatoCacher.cs @@ -41,16 +41,18 @@ namespace PlexRequests.Services.Jobs { public class CouchPotatoCacher : IJob, ICouchPotatoCacher { - public CouchPotatoCacher(ISettingsService cpSettings, ICouchPotatoApi cpApi, ICacheProvider cache) + public CouchPotatoCacher(ISettingsService cpSettings, ICouchPotatoApi cpApi, ICacheProvider cache, IJobRecord rec) { CpSettings = cpSettings; CpApi = cpApi; Cache = cache; + Job = rec; } private ISettingsService CpSettings { get; } private ICacheProvider Cache { get; } private ICouchPotatoApi CpApi { get; } + private IJobRecord Job { get; } private static Logger Log = LogManager.GetCurrentClassLogger(); @@ -74,6 +76,10 @@ namespace PlexRequests.Services.Jobs { Log.Error(ex, "Failed caching queued items from CouchPotato"); } + finally + { + Job.Record(JobNames.CpCacher); + } } } diff --git a/PlexRequests.Services/Jobs/JobNames.cs b/PlexRequests.Services/Jobs/JobNames.cs new file mode 100644 index 000000000..d996ac01e --- /dev/null +++ b/PlexRequests.Services/Jobs/JobNames.cs @@ -0,0 +1,37 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: JobNames.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 PlexRequests.Services +{ + public static class JobNames + { + public const string StoreBackup = "Database Backup"; + public const string CpCacher = "CouchPotato Cacher"; + public const string SonarrCacher = "Sonarr Cacher"; + public const string SrCacher = "SickRage Cacher"; + public const string PlexChecker = "Plex Availability Cacher"; + } +} \ No newline at end of file diff --git a/PlexRequests.Services/Jobs/JobRecord.cs b/PlexRequests.Services/Jobs/JobRecord.cs new file mode 100644 index 000000000..fd97f785f --- /dev/null +++ b/PlexRequests.Services/Jobs/JobRecord.cs @@ -0,0 +1,59 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: JobRecord.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.Linq; + +using PlexRequests.Services.Interfaces; +using PlexRequests.Store.Models; +using PlexRequests.Store.Repository; + +namespace PlexRequests.Services +{ + public class JobRecord : IJobRecord + { + public JobRecord(IRepository repo) + { + Repo = repo; + } + private IRepository Repo { get; } + public void Record(string jobName) + { + var allJobs = Repo.GetAll(); + var storeJob = allJobs.FirstOrDefault(x => x.Name == jobName); + if (storeJob != null) + { + storeJob.LastRun = DateTime.UtcNow; + Repo.Update(storeJob); + } + else + { + var job = new ScheduledJobs { LastRun = DateTime.UtcNow, Name = jobName }; + Repo.Insert(job); + } + } + } +} \ No newline at end of file diff --git a/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs b/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs index 54feb6262..0e29b93d5 100644 --- a/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs +++ b/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs @@ -47,7 +47,7 @@ namespace PlexRequests.Services.Jobs public class PlexAvailabilityChecker : IJob, IAvailabilityChecker { public PlexAvailabilityChecker(ISettingsService plexSettings, ISettingsService auth, IRequestService request, IPlexApi plex, ICacheProvider cache, - INotificationService notify) + INotificationService notify, IJobRecord rec) { Plex = plexSettings; Auth = auth; @@ -55,6 +55,7 @@ namespace PlexRequests.Services.Jobs PlexApi = plex; Cache = cache; Notification = notify; + Job = rec; } private ISettingsService Plex { get; } @@ -64,6 +65,7 @@ namespace PlexRequests.Services.Jobs private IPlexApi PlexApi { get; } private ICacheProvider Cache { get; } private INotificationService Notification { get; } + private IJobRecord Job { get; } public void CheckAndUpdateAll() { @@ -144,6 +146,8 @@ namespace PlexRequests.Services.Jobs RequestService.BatchUpdate(modifiedModel); } + Job.Record(JobNames.PlexChecker); + } public List GetPlexMovies() diff --git a/PlexRequests.Services/Jobs/SickRageCacher.cs b/PlexRequests.Services/Jobs/SickRageCacher.cs index 3591cf7bb..69b50b358 100644 --- a/PlexRequests.Services/Jobs/SickRageCacher.cs +++ b/PlexRequests.Services/Jobs/SickRageCacher.cs @@ -41,11 +41,12 @@ namespace PlexRequests.Services.Jobs { public class SickRageCacher : IJob, ISickRageCacher { - public SickRageCacher(ISettingsService srSettings, ISickRageApi srApi, ICacheProvider cache) + public SickRageCacher(ISettingsService srSettings, ISickRageApi srApi, ICacheProvider cache, IJobRecord rec) { SrSettings = srSettings; SrApi = srApi; Cache = cache; + Job = rec; } private ISettingsService SrSettings { get; } @@ -53,6 +54,7 @@ namespace PlexRequests.Services.Jobs private ISickRageApi SrApi { get; } private static Logger Log = LogManager.GetCurrentClassLogger(); + private IJobRecord Job { get; } public void Queued() { @@ -74,6 +76,10 @@ namespace PlexRequests.Services.Jobs { Log.Error(ex, "Failed caching queued items from SickRage"); } + finally + { + Job.Record(JobNames.SrCacher); + } } } diff --git a/PlexRequests.Services/Jobs/SonarrCacher.cs b/PlexRequests.Services/Jobs/SonarrCacher.cs index 15aef545d..97089da27 100644 --- a/PlexRequests.Services/Jobs/SonarrCacher.cs +++ b/PlexRequests.Services/Jobs/SonarrCacher.cs @@ -35,6 +35,8 @@ using PlexRequests.Core; using PlexRequests.Core.SettingModels; using PlexRequests.Helpers; using PlexRequests.Services.Interfaces; +using PlexRequests.Store.Models; +using PlexRequests.Store.Repository; using Quartz; @@ -42,16 +44,18 @@ namespace PlexRequests.Services.Jobs { public class SonarrCacher : IJob, ISonarrCacher { - public SonarrCacher(ISettingsService sonarrSettings, ISonarrApi sonarrApi, ICacheProvider cache) + public SonarrCacher(ISettingsService sonarrSettings, ISonarrApi sonarrApi, ICacheProvider cache, IJobRecord rec) { SonarrSettings = sonarrSettings; SonarrApi = sonarrApi; + Job = rec; Cache = cache; } private ISettingsService SonarrSettings { get; } private ICacheProvider Cache { get; } private ISonarrApi SonarrApi { get; } + private IJobRecord Job { get; } private static Logger Log = LogManager.GetCurrentClassLogger(); @@ -75,6 +79,10 @@ namespace PlexRequests.Services.Jobs { Log.Error(ex, "Failed caching queued items from Sonarr"); } + finally + { + Job.Record(JobNames.SonarrCacher); + } } } diff --git a/PlexRequests.Services/Jobs/StoreBackup.cs b/PlexRequests.Services/Jobs/StoreBackup.cs index 2d63987b0..64a1fbc57 100644 --- a/PlexRequests.Services/Jobs/StoreBackup.cs +++ b/PlexRequests.Services/Jobs/StoreBackup.cs @@ -30,7 +30,10 @@ using System.Linq; using NLog; +using PlexRequests.Services.Interfaces; using PlexRequests.Store; +using PlexRequests.Store.Models; +using PlexRequests.Store.Repository; using Quartz; @@ -40,12 +43,14 @@ namespace PlexRequests.Services.Jobs { public class StoreBackup : IJob { - public StoreBackup(ISqliteConfiguration sql) + public StoreBackup(ISqliteConfiguration sql, IJobRecord rec) { Sql = sql; + JobRecord = rec; } private ISqliteConfiguration Sql { get; } + private IJobRecord JobRecord { get; } private static Logger Log = LogManager.GetCurrentClassLogger(); @@ -82,6 +87,10 @@ namespace PlexRequests.Services.Jobs Log.Warn(e); Log.Warn("Exception when trying to copy the backup."); } + finally + { + JobRecord.Record(JobNames.StoreBackup); + } } } diff --git a/PlexRequests.Services/PlexRequests.Services.csproj b/PlexRequests.Services/PlexRequests.Services.csproj index 1b088294a..72b0e9372 100644 --- a/PlexRequests.Services/PlexRequests.Services.csproj +++ b/PlexRequests.Services/PlexRequests.Services.csproj @@ -93,6 +93,9 @@ + + + diff --git a/PlexRequests.Store/Models/ScheduledJobs.cs b/PlexRequests.Store/Models/ScheduledJobs.cs new file mode 100644 index 000000000..7c242f0bd --- /dev/null +++ b/PlexRequests.Store/Models/ScheduledJobs.cs @@ -0,0 +1,39 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: LogEntity.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion +using System; + +using Dapper.Contrib.Extensions; + +namespace PlexRequests.Store.Models +{ + [Table("ScheduledJobs")] + public class ScheduledJobs : Entity + { + public string Name { get; set; } + public DateTime LastRun { get; set; } + } +} diff --git a/PlexRequests.Store/PlexRequests.Store.csproj b/PlexRequests.Store/PlexRequests.Store.csproj index ae047a192..927969b86 100644 --- a/PlexRequests.Store/PlexRequests.Store.csproj +++ b/PlexRequests.Store/PlexRequests.Store.csproj @@ -63,6 +63,7 @@ + diff --git a/PlexRequests.Store/SqlTables.sql b/PlexRequests.Store/SqlTables.sql index 7392c4efc..7a1cbed63 100644 --- a/PlexRequests.Store/SqlTables.sql +++ b/PlexRequests.Store/SqlTables.sql @@ -45,5 +45,12 @@ CREATE UNIQUE INDEX IF NOT EXISTS Logs_Id ON Logs (Id); CREATE TABLE IF NOT EXISTS DBInfo ( SchemaVersion INTEGER +); -); \ No newline at end of file +CREATE TABLE IF NOT EXISTS ScheduledJobs +( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + Name varchar(100) NOT NULL, + LastRun varchar(100) NOT NULL +); +CREATE UNIQUE INDEX IF NOT EXISTS ScheduledJobs_Id ON ScheduledJobs (Id); \ No newline at end of file diff --git a/PlexRequests.UI/Bootstrapper.cs b/PlexRequests.UI/Bootstrapper.cs index fc9908b06..ed1cd1bfb 100644 --- a/PlexRequests.UI/Bootstrapper.cs +++ b/PlexRequests.UI/Bootstrapper.cs @@ -87,8 +87,10 @@ namespace PlexRequests.UI // Repo's container.Register, GenericRepository>(); + container.Register, GenericRepository>(); container.Register(); container.Register(); + container.Register(); // Services container.Register(); diff --git a/PlexRequests.UI/Content/custom.css b/PlexRequests.UI/Content/custom.css index 520b2cc39..c9ec38106 100644 --- a/PlexRequests.UI/Content/custom.css +++ b/PlexRequests.UI/Content/custom.css @@ -220,3 +220,7 @@ label { border-radius: 0 0.25rem 0.25rem 0 !important; padding: 12px 8px; } +#updateAvailable { + background-color: #ffa400; + text-align: center; } + diff --git a/PlexRequests.UI/Content/custom.min.css b/PlexRequests.UI/Content/custom.min.css index 34e07289c..e5e3c6f8d 100644 --- a/PlexRequests.UI/Content/custom.min.css +++ b/PlexRequests.UI/Content/custom.min.css @@ -1 +1 @@ -@media(min-width:768px){.row{position:relative;}.bottom-align-text{position:absolute;bottom:0;right:0;}}@media(max-width:48em){.home{padding-top:1rem;}}@media(min-width:48em){.home{padding-top:4rem;}}.btn{border-radius:.25rem !important;}.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;}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{background:#4e5d6c;}.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:13px;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;} \ No newline at end of file +@media(min-width:768px){.row{position:relative;}.bottom-align-text{position:absolute;bottom:0;right:0;}}@media(max-width:48em){.home{padding-top:1rem;}}@media(min-width:48em){.home{padding-top:4rem;}}.btn{border-radius:.25rem !important;}.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;}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{background:#4e5d6c;}.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:13px;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:#ffa400;text-align:center;} \ No newline at end of file diff --git a/PlexRequests.UI/Content/custom.scss b/PlexRequests.UI/Content/custom.scss index e2324512b..c9d0f60a8 100644 --- a/PlexRequests.UI/Content/custom.scss +++ b/PlexRequests.UI/Content/custom.scss @@ -7,8 +7,7 @@ $warning-colour: #f0ad4e; $danger-colour: #d9534f; $success-colour: #5cb85c; $i: -!important -; +!important; @media (min-width: 768px ) { .row { @@ -279,4 +278,9 @@ $border-radius: 10px; .btn-split .btn.dropdown-toggle { border-radius: 0 .25rem .25rem 0 $i; padding: 12px 8px; +} + +#updateAvailable { + background-color: rgb(255, 164, 0); + text-align: center; } \ No newline at end of file diff --git a/PlexRequests.UI/Jobs/CustomJobFactory.cs b/PlexRequests.UI/Jobs/CustomJobFactory.cs index 0337999cb..643222844 100644 --- a/PlexRequests.UI/Jobs/CustomJobFactory.cs +++ b/PlexRequests.UI/Jobs/CustomJobFactory.cs @@ -36,7 +36,7 @@ using Quartz.Spi; namespace PlexRequests.UI.Jobs { /// - /// The custom job factory we are using so we are able to use our IoC container with DI in our Jobs. + /// The custom job factory we are using so we are able to use our IoC container with DI in our JobNames. /// /// public class CustomJobFactory : IJobFactory diff --git a/PlexRequests.UI/Models/JsonUpdateAvailableModel.cs b/PlexRequests.UI/Models/JsonUpdateAvailableModel.cs new file mode 100644 index 000000000..38931f86a --- /dev/null +++ b/PlexRequests.UI/Models/JsonUpdateAvailableModel.cs @@ -0,0 +1,33 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: JsonUpdateAvailableModel.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 PlexRequests.UI.Models +{ + public class JsonUpdateAvailableModel + { + public bool UpdateAvailable { get; set; } + } +} \ No newline at end of file diff --git a/PlexRequests.UI/Modules/AdminModule.cs b/PlexRequests.UI/Modules/AdminModule.cs index 71885bdc5..6c93b00d1 100644 --- a/PlexRequests.UI/Modules/AdminModule.cs +++ b/PlexRequests.UI/Modules/AdminModule.cs @@ -295,8 +295,8 @@ namespace PlexRequests.UI.Modules } var result = CpService.SaveSettings(couchPotatoSettings); - return Response.AsJson(result - ? new JsonResponseModel { Result = true, Message = "Successfully Updated the Settings for CouchPotato!" } + return Response.AsJson(result + ? new JsonResponseModel { Result = true, Message = "Successfully Updated the Settings for CouchPotato!" } : new JsonResponseModel { Result = false, Message = "Could not update the settings, take a look at the logs." }); } @@ -429,7 +429,7 @@ namespace PlexRequests.UI.Modules finally { NotificationService.UnSubscribe(new EmailMessageNotification(EmailService)); - } + } return Response.AsJson(new JsonResponseModel { Result = true, Message = "Successfully sent a test Email Notification!" }); } @@ -464,7 +464,7 @@ namespace PlexRequests.UI.Modules { var checker = new StatusChecker(); var status = checker.GetStatus(); - var md = new Markdown(); + var md = new Markdown(new MarkdownOptions { AutoNewLines = true }); status.ReleaseNotes = md.Transform(status.ReleaseNotes); return View["Status", status]; } @@ -623,7 +623,7 @@ namespace PlexRequests.UI.Modules { JsonSettings.MaxJsonLength = int.MaxValue; var allLogs = LogsRepo.GetAll().OrderByDescending(x => x.Id).Take(200); - var model = new DatatablesModel {Data = new List()}; + var model = new DatatablesModel { Data = new List() }; foreach (var l in allLogs) { l.DateString = l.Date.ToString("G"); @@ -650,7 +650,7 @@ namespace PlexRequests.UI.Modules settings.Level = level; LogService.SaveSettings(settings); - return Response.AsJson(new JsonResponseModel { Result = true, Message = $"The new log level is now {newLevel}"}); + return Response.AsJson(new JsonResponseModel { Result = true, Message = $"The new log level is now {newLevel}" }); } private Negotiator Headphones() @@ -673,7 +673,7 @@ namespace PlexRequests.UI.Modules Log.Trace(settings.DumpJson()); var result = HeadphonesService.SaveSettings(settings); - + Log.Info("Saved headphones settings, result: {0}", result); return Response.AsJson(result ? new JsonResponseModel { Result = true, Message = "Successfully Updated the Settings for Headphones!" } diff --git a/PlexRequests.UI/Modules/BaseAuthModule.cs b/PlexRequests.UI/Modules/BaseAuthModule.cs index 3dcf930da..1cb5fe225 100644 --- a/PlexRequests.UI/Modules/BaseAuthModule.cs +++ b/PlexRequests.UI/Modules/BaseAuthModule.cs @@ -30,8 +30,11 @@ using Nancy.Extensions; using PlexRequests.UI.Models; using System; +using Nancy.Security; + using PlexRequests.Core; using PlexRequests.Core.SettingModels; +using PlexRequests.Helpers; namespace PlexRequests.UI.Modules { @@ -52,6 +55,8 @@ namespace PlexRequests.UI.Modules } } + protected bool IsAdmin => Context.CurrentUser.IsAuthenticated(); + protected int DateTimeOffset { get @@ -87,5 +92,7 @@ namespace PlexRequests.UI.Modules : null; } + + } } \ No newline at end of file diff --git a/PlexRequests.UI/Modules/SearchModule.cs b/PlexRequests.UI/Modules/SearchModule.cs index 094f07171..92bcb1f38 100644 --- a/PlexRequests.UI/Modules/SearchModule.cs +++ b/PlexRequests.UI/Modules/SearchModule.cs @@ -124,14 +124,6 @@ namespace PlexRequests.UI.Modules private IHeadphonesApi HeadphonesApi { get; } private static Logger Log = LogManager.GetCurrentClassLogger(); - private bool IsAdmin - { - get - { - return Context.CurrentUser.IsAuthenticated(); - } - } - private Negotiator RequestLoad() { var settings = PrService.GetSettings(); @@ -626,7 +618,7 @@ namespace PlexRequests.UI.Modules Status = showInfo.status, RequestedDate = DateTime.UtcNow, Approved = false, - RequestedUsers = new List() { Username }, + RequestedUsers = new List { Username }, Issues = IssueState.None, ImdbId = showInfo.externals?.imdb ?? string.Empty, SeasonCount = showInfo.seasonCount @@ -802,7 +794,7 @@ namespace PlexRequests.UI.Modules } var sender = new HeadphonesSender(HeadphonesApi, hpSettings, RequestService); - sender.AddAlbum(model); + sender.AddAlbum(model).Wait(); model.Approved = true; RequestService.AddRequest(model); diff --git a/PlexRequests.UI/Modules/UpdateCheckerModule.cs b/PlexRequests.UI/Modules/UpdateCheckerModule.cs new file mode 100644 index 000000000..2223b9a4f --- /dev/null +++ b/PlexRequests.UI/Modules/UpdateCheckerModule.cs @@ -0,0 +1,79 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: UpdateCheckerModule.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 Nancy; + +using NLog; + +using PlexRequests.Core; +using PlexRequests.Helpers; +using PlexRequests.UI.Models; + +namespace PlexRequests.UI.Modules +{ + public class UpdateCheckerModule : BaseAuthModule + { + public UpdateCheckerModule(ICacheProvider provider) : base("updatechecker") + { + Cache = provider; + + Get["/"] = _ => CheckLatestVersion(); + } + + private ICacheProvider Cache { get; } + + private static Logger Log = LogManager.GetCurrentClassLogger(); + + private Response CheckLatestVersion() + { + try + { + if (!IsAdmin) + { + return Response.AsJson(new JsonUpdateAvailableModel { UpdateAvailable = false }); + } + + var checker = new StatusChecker(); + var release = Cache.GetOrSet(CacheKeys.LastestProductVersion, () => checker.GetStatus(), 30); + + if (release.UpdateAvailable) + { + return Response.AsJson(new JsonUpdateAvailableModel { UpdateAvailable = true}); + } + + return Response.AsJson(new JsonUpdateAvailableModel { UpdateAvailable = false }); + } + catch (Exception e) + { + Log.Warn("Exception Thrown when attempting to check the status"); + Log.Warn(e); + return Response.AsJson(new JsonUpdateAvailableModel { UpdateAvailable = false }); + } + } + } +} \ No newline at end of file diff --git a/PlexRequests.UI/PlexRequests.UI.csproj b/PlexRequests.UI/PlexRequests.UI.csproj index 45668bc1a..f836325ac 100644 --- a/PlexRequests.UI/PlexRequests.UI.csproj +++ b/PlexRequests.UI/PlexRequests.UI.csproj @@ -185,12 +185,14 @@ + + diff --git a/PlexRequests.UI/Views/Shared/_Layout.cshtml b/PlexRequests.UI/Views/Shared/_Layout.cshtml index a61aae9c3..ae61d4690 100644 --- a/PlexRequests.UI/Views/Shared/_Layout.cshtml +++ b/PlexRequests.UI/Views/Shared/_Layout.cshtml @@ -34,7 +34,7 @@ Plex Requests - + +
@@ -82,6 +83,24 @@ $(function () { + var urlBase = '@Html.GetBaseUrl()'; + var url = createBaseUrl(urlBase, '/updatechecker'); + $.ajax({ + type: "GET", + url: url, + dataType: "json", + success: function (response) { + if (response.updateAvailable) { + var status = createBaseUrl(urlBase, '/admin/status'); + $('#updateAvailable').html("There is a new update available! Click Here!"); + $('#updateAvailable').removeAttr("hidden"); + } + }, + error: function (e) { + console.log(e); + } + }); + $(document).on('scroll', function () { if ($(window).scrollTop() > 100) { From 21a383010123bd647f83585b9f21745d5348421b Mon Sep 17 00:00:00 2001 From: tidusjar Date: Fri, 29 Apr 2016 14:36:00 +0100 Subject: [PATCH 26/76] Slight adjustments to #189 --- PlexRequests.UI/Content/custom.css | 3 ++- PlexRequests.UI/Content/custom.min.css | 2 +- PlexRequests.UI/Content/custom.scss | 1 + PlexRequests.UI/Modules/UpdateCheckerModule.cs | 9 +++------ PlexRequests.UI/Views/Shared/_Layout.cshtml | 2 +- 5 files changed, 8 insertions(+), 9 deletions(-) diff --git a/PlexRequests.UI/Content/custom.css b/PlexRequests.UI/Content/custom.css index c9ec38106..ccdecf826 100644 --- a/PlexRequests.UI/Content/custom.css +++ b/PlexRequests.UI/Content/custom.css @@ -222,5 +222,6 @@ label { #updateAvailable { background-color: #ffa400; - text-align: center; } + text-align: center; + font-size: 15px; } diff --git a/PlexRequests.UI/Content/custom.min.css b/PlexRequests.UI/Content/custom.min.css index e5e3c6f8d..0a8928887 100644 --- a/PlexRequests.UI/Content/custom.min.css +++ b/PlexRequests.UI/Content/custom.min.css @@ -1 +1 @@ -@media(min-width:768px){.row{position:relative;}.bottom-align-text{position:absolute;bottom:0;right:0;}}@media(max-width:48em){.home{padding-top:1rem;}}@media(min-width:48em){.home{padding-top:4rem;}}.btn{border-radius:.25rem !important;}.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;}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{background:#4e5d6c;}.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:13px;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:#ffa400;text-align:center;} \ No newline at end of file +@media(min-width:768px){.row{position:relative;}.bottom-align-text{position:absolute;bottom:0;right:0;}}@media(max-width:48em){.home{padding-top:1rem;}}@media(min-width:48em){.home{padding-top:4rem;}}.btn{border-radius:.25rem !important;}.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;}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{background:#4e5d6c;}.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:13px;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:#ffa400;text-align:center;font-size:15px;} \ No newline at end of file diff --git a/PlexRequests.UI/Content/custom.scss b/PlexRequests.UI/Content/custom.scss index c9d0f60a8..e2fd7c26d 100644 --- a/PlexRequests.UI/Content/custom.scss +++ b/PlexRequests.UI/Content/custom.scss @@ -283,4 +283,5 @@ $border-radius: 10px; #updateAvailable { background-color: rgb(255, 164, 0); text-align: center; + font-size: 15px; } \ No newline at end of file diff --git a/PlexRequests.UI/Modules/UpdateCheckerModule.cs b/PlexRequests.UI/Modules/UpdateCheckerModule.cs index 2223b9a4f..3d89ff2ff 100644 --- a/PlexRequests.UI/Modules/UpdateCheckerModule.cs +++ b/PlexRequests.UI/Modules/UpdateCheckerModule.cs @@ -61,12 +61,9 @@ namespace PlexRequests.UI.Modules var checker = new StatusChecker(); var release = Cache.GetOrSet(CacheKeys.LastestProductVersion, () => checker.GetStatus(), 30); - if (release.UpdateAvailable) - { - return Response.AsJson(new JsonUpdateAvailableModel { UpdateAvailable = true}); - } - - return Response.AsJson(new JsonUpdateAvailableModel { UpdateAvailable = false }); + return Response.AsJson(release.UpdateAvailable + ? new JsonUpdateAvailableModel { UpdateAvailable = true} + : new JsonUpdateAvailableModel { UpdateAvailable = false }); } catch (Exception e) { diff --git a/PlexRequests.UI/Views/Shared/_Layout.cshtml b/PlexRequests.UI/Views/Shared/_Layout.cshtml index ae61d4690..041925c2c 100644 --- a/PlexRequests.UI/Views/Shared/_Layout.cshtml +++ b/PlexRequests.UI/Views/Shared/_Layout.cshtml @@ -92,7 +92,7 @@ success: function (response) { if (response.updateAvailable) { var status = createBaseUrl(urlBase, '/admin/status'); - $('#updateAvailable').html("There is a new update available! Click Here!"); + $('#updateAvailable').html(" There is a new update available! Click Here!"); $('#updateAvailable').removeAttr("hidden"); } }, From 1f2e460f6d1b2979e9dfe3661076371679a73076 Mon Sep 17 00:00:00 2001 From: Chris Lees Date: Fri, 29 Apr 2016 14:46:39 -0400 Subject: [PATCH 27/76] Added Released propety to RequestViewModel. Added Released filter to the Requests page --- PlexRequests.UI/Content/requests.js | 1 + PlexRequests.UI/Models/RequestViewModel.cs | 3 ++- PlexRequests.UI/Modules/RequestsModule.cs | 5 ++++- PlexRequests.UI/Views/Requests/Index.cshtml | 6 ++++-- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/PlexRequests.UI/Content/requests.js b/PlexRequests.UI/Content/requests.js index f36967c7c..fbff4a934 100644 --- a/PlexRequests.UI/Content/requests.js +++ b/PlexRequests.UI/Content/requests.js @@ -540,6 +540,7 @@ function buildRequestContext(result, type) { requestedUsers: result.requestedUsers ? result.requestedUsers.join(', ') : '', requestedDate: Humanize(result.requestedDate), requestedDateTicks: result.requestedDateTicks, + released: result.released, available: result.available, admin: result.admin, issues: result.issues, diff --git a/PlexRequests.UI/Models/RequestViewModel.cs b/PlexRequests.UI/Models/RequestViewModel.cs index e5dc09746..136c511aa 100644 --- a/PlexRequests.UI/Models/RequestViewModel.cs +++ b/PlexRequests.UI/Models/RequestViewModel.cs @@ -38,7 +38,8 @@ namespace PlexRequests.UI.Models public string Title { get; set; } public string PosterPath { get; set; } public DateTime ReleaseDate { get; set; } - public long ReleaseDateTicks { get; set; } + public bool Released { get; set; } + public long ReleaseDateTicks { get; set; } public RequestType Type { get; set; } public string Status { get; set; } public bool Approved { get; set; } diff --git a/PlexRequests.UI/Modules/RequestsModule.cs b/PlexRequests.UI/Modules/RequestsModule.cs index afb98b454..bb3bd7b49 100644 --- a/PlexRequests.UI/Modules/RequestsModule.cs +++ b/PlexRequests.UI/Modules/RequestsModule.cs @@ -163,6 +163,7 @@ namespace PlexRequests.UI.Modules ReleaseDate = movie.ReleaseDate, ReleaseDateTicks = movie.ReleaseDate.Ticks, RequestedDate = movie.RequestedDate, + Released = DateTime.Now > movie.ReleaseDate, RequestedDateTicks = DateTimeHelper.OffsetUTCDateTime(movie.RequestedDate, DateTimeOffset).Ticks, Approved = movie.Available || movie.Approved, Title = movie.Title, @@ -246,6 +247,7 @@ namespace PlexRequests.UI.Modules ReleaseDateTicks = tv.ReleaseDate.Ticks, RequestedDate = tv.RequestedDate, RequestedDateTicks = DateTimeHelper.OffsetUTCDateTime(tv.RequestedDate, DateTimeOffset).Ticks, + Released = DateTime.Now > tv.ReleaseDate, Approved = tv.Available || tv.Approved, Title = tv.Title, Overview = tv.Overview, @@ -288,6 +290,7 @@ namespace PlexRequests.UI.Modules ReleaseDateTicks = album.ReleaseDate.Ticks, RequestedDate = album.RequestedDate, RequestedDateTicks = DateTimeHelper.OffsetUTCDateTime(album.RequestedDate, DateTimeOffset).Ticks, + Released = DateTime.Now > album.ReleaseDate, Approved = album.Available || album.Approved, Title = album.Title, Overview = album.Overview, @@ -411,4 +414,4 @@ namespace PlexRequests.UI.Modules : new JsonResponseModel { Result = false, Message = "Could not update the notes, please try again or check the logs" }); } } -} \ No newline at end of file +} diff --git a/PlexRequests.UI/Views/Requests/Index.cshtml b/PlexRequests.UI/Views/Requests/Index.cshtml index b6fe26214..27565156e 100644 --- a/PlexRequests.UI/Views/Requests/Index.cshtml +++ b/PlexRequests.UI/Views/Requests/Index.cshtml @@ -62,7 +62,9 @@
  • Approved
  • Not Approved
  • Available
  • -
  • Not Available
  • +
  • Not Available
  • +
  • Released
  • +
  • Not Released
  • @@ -125,7 +127,7 @@ \ No newline at end of file diff --git a/PlexRequests.UI/packages.config b/PlexRequests.UI/packages.config index 196008837..304f7dab8 100644 --- a/PlexRequests.UI/packages.config +++ b/PlexRequests.UI/packages.config @@ -1,41 +1,41 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From c7e3409935f90c6f3c78929d4651e262b0973453 Mon Sep 17 00:00:00 2001 From: TidusJar Date: Thu, 12 May 2016 16:04:25 -0400 Subject: [PATCH 30/76] resolved #209 --- .../Validators/PlexRequestsValidator.cs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 PlexRequests.UI/Validators/PlexRequestsValidator.cs diff --git a/PlexRequests.UI/Validators/PlexRequestsValidator.cs b/PlexRequests.UI/Validators/PlexRequestsValidator.cs new file mode 100644 index 000000000..44481bf60 --- /dev/null +++ b/PlexRequests.UI/Validators/PlexRequestsValidator.cs @@ -0,0 +1,26 @@ +using System; +using FluentValidation; +using PlexRequests.Core.SettingModels; + +namespace PlexRequests.UI +{ + public class PlexRequestsValidator : AbstractValidator + { + public PlexRequestsValidator () + { + RuleFor (x => x.BaseUrl).NotEqual ("requests").WithMessage ("You cannot use 'requests' as this is reserved by the application."); + RuleFor (x => x.BaseUrl).NotEqual ("admin").WithMessage ("You cannot use 'admin' as this is reserved by the application."); + RuleFor (x => x.BaseUrl).NotEqual ("search").WithMessage ("You cannot use 'search' as this is reserved by the application."); + RuleFor (x => x.BaseUrl).NotEqual ("issues").WithMessage ("You cannot use 'issues' as this is reserved by the application."); + RuleFor (x => x.BaseUrl).NotEqual ("userlogin").WithMessage ("You cannot use 'userlogin' as this is reserved by the application."); + RuleFor (x => x.BaseUrl).NotEqual ("login").WithMessage ("You cannot use 'login' as this is reserved by the application."); + RuleFor (x => x.BaseUrl).NotEqual ("test").WithMessage ("You cannot use 'test' as this is reserved by the application."); + RuleFor (x => x.BaseUrl).NotEqual ("approval").WithMessage ("You cannot use 'approval' as this is reserved by the application."); + RuleFor (x => x.BaseUrl).NotEqual ("updatechecker").WithMessage ("You cannot use 'updatechecker' as this is reserved by the application."); + + + + } + } +} + From f9205fc02733598e767f5285ee11bcdd415feb0b Mon Sep 17 00:00:00 2001 From: TidusJar Date: Thu, 12 May 2016 16:09:09 -0400 Subject: [PATCH 31/76] This should help #202 --- PlexRequests.Api/CouchPotatoApi.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/PlexRequests.Api/CouchPotatoApi.cs b/PlexRequests.Api/CouchPotatoApi.cs index 9db43a409..96d245cf9 100644 --- a/PlexRequests.Api/CouchPotatoApi.cs +++ b/PlexRequests.Api/CouchPotatoApi.cs @@ -130,9 +130,10 @@ namespace PlexRequests.Api { return Api.Execute(request, baseUrl); } - catch (ApiRequestException) // Request error is already logged in the ApiRequest class + catch (Exception e) // Request error is already logged in the ApiRequest class { Log.Error("Error when attempting to GetMovies."); + Log.Error (e); return new CouchPotatoMovies(); } } From adeeb7824d4f6cd7486044230185e10600dfb724 Mon Sep 17 00:00:00 2001 From: TidusJar Date: Thu, 12 May 2016 22:39:21 -0400 Subject: [PATCH 32/76] Added a retry handler into the solution. We can now retry failed api requests. this should resolve #202 and start on #208 --- PlexRequests.Api/PlexApi.cs | 58 ++++++++++++++++--- PlexRequests.Api/PlexRequests.Api.csproj | 4 ++ PlexRequests.Api/RetryHandler.cs | 34 +++++++++++ PlexRequests.Api/packages.config | 1 + PlexRequests.UI/Validators/SonarrValidator.cs | 1 - 5 files changed, 90 insertions(+), 8 deletions(-) create mode 100644 PlexRequests.Api/RetryHandler.cs diff --git a/PlexRequests.Api/PlexApi.cs b/PlexRequests.Api/PlexApi.cs index cbe06e3a6..5367cb267 100644 --- a/PlexRequests.Api/PlexApi.cs +++ b/PlexRequests.Api/PlexApi.cs @@ -23,6 +23,9 @@ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // ************************************************************************/ +using Polly; + + #endregion using System; @@ -67,7 +70,14 @@ namespace PlexRequests.Api request.AddJsonBody(userModel); var api = new ApiRequest(); - return api.Execute(request, new Uri("https://plex.tv/users/sign_in.json")); + + var policy = RetryHandler.RetryAndWaitPolicy (new TimeSpan[] { + TimeSpan.FromSeconds (2), + TimeSpan.FromSeconds(5), + TimeSpan.FromSeconds(10) + }, (exception, timespan) => Log.Error (exception, "Exception when calling SignIn for Plex, Retrying {0}", timespan)); + + return (PlexAuthentication)policy.Execute(() => api.Execute(request, new Uri("https://plex.tv/users/sign_in.json"))); } public PlexFriends GetUsers(string authToken) @@ -80,7 +90,13 @@ namespace PlexRequests.Api AddHeaders(ref request, authToken); var api = new ApiRequest(); - var users = api.ExecuteXml(request, new Uri("https://plex.tv/pms/friends/all")); + var policy = RetryHandler.RetryAndWaitPolicy (new TimeSpan[] { + TimeSpan.FromSeconds (2), + TimeSpan.FromSeconds(5), + TimeSpan.FromSeconds(10) + }, (exception, timespan) => Log.Error (exception, "Exception when calling GetUsers for Plex, Retrying {0}", timespan)); + + var users = (PlexFriends)policy.Execute(() =>api.ExecuteXml(request, new Uri("https://plex.tv/pms/friends/all"))); return users; } @@ -104,7 +120,13 @@ namespace PlexRequests.Api AddHeaders(ref request, authToken); var api = new ApiRequest(); - var search = api.ExecuteXml(request, plexFullHost); + var policy = RetryHandler.RetryAndWaitPolicy (new TimeSpan[] { + TimeSpan.FromSeconds (2), + TimeSpan.FromSeconds(5), + TimeSpan.FromSeconds(10) + }, (exception, timespan) => Log.Error (exception, "Exception when calling SearchContent for Plex, Retrying {0}", timespan)); + + var search = (PlexSearch)policy.Execute(() => api.ExecuteXml(request, plexFullHost)); return search; } @@ -117,9 +139,14 @@ namespace PlexRequests.Api }; AddHeaders(ref request, authToken); + var policy = RetryHandler.RetryAndWaitPolicy (new TimeSpan[] { + TimeSpan.FromSeconds (2), + TimeSpan.FromSeconds(5), + TimeSpan.FromSeconds(10) + }, (exception, timespan) => Log.Error (exception, "Exception when calling GetStatus for Plex, Retrying {0}", timespan)); var api = new ApiRequest(); - var users = api.ExecuteXml(request, uri); + var users = (PlexStatus)policy.Execute(() => api.ExecuteXml(request, uri)); return users; } @@ -133,8 +160,15 @@ namespace PlexRequests.Api AddHeaders(ref request, authToken); + var policy = RetryHandler.RetryAndWaitPolicy (new TimeSpan[] { + TimeSpan.FromSeconds (2), + TimeSpan.FromSeconds(5), + TimeSpan.FromSeconds(10) + }, (exception, timespan) => Log.Error (exception, "Exception when calling GetAccount for Plex, Retrying: {0}", timespan)); + + var api = new ApiRequest(); - var account = api.ExecuteXml(request, new Uri("https://plex.tv/users/account")); + var account = (PlexAccount)policy.Execute(() => api.ExecuteXml(request, new Uri("https://plex.tv/users/account"))); return account; } @@ -152,8 +186,13 @@ namespace PlexRequests.Api var api = new ApiRequest(); try { + var policy = RetryHandler.RetryAndWaitPolicy (new TimeSpan[] { + TimeSpan.FromSeconds (5), + TimeSpan.FromSeconds(10), + TimeSpan.FromSeconds(30) + }, (exception, timespan) => Log.Error (exception, "Exception when calling GetLibrarySections for Plex, Retrying {0}", timespan)); - return api.ExecuteXml(request, plexFullHost); + return (PlexLibraries)policy.Execute(() => api.ExecuteXml(request, plexFullHost)); } catch (ApiRequestException) { @@ -176,8 +215,13 @@ namespace PlexRequests.Api var api = new ApiRequest(); try { + var policy = RetryHandler.RetryAndWaitPolicy (new TimeSpan[] { + TimeSpan.FromSeconds (5), + TimeSpan.FromSeconds(10), + TimeSpan.FromSeconds(30) + }, (exception, timespan) => Log.Error (exception, "Exception when calling GetLibrary for Plex, Retrying {0}", timespan)); - return api.ExecuteXml(request, plexFullHost); + return (PlexSearch)policy.Execute(() => api.ExecuteXml(request, plexFullHost)); } catch (ApiRequestException) { diff --git a/PlexRequests.Api/PlexRequests.Api.csproj b/PlexRequests.Api/PlexRequests.Api.csproj index aa7db4a5b..a90f60f22 100644 --- a/PlexRequests.Api/PlexRequests.Api.csproj +++ b/PlexRequests.Api/PlexRequests.Api.csproj @@ -57,6 +57,9 @@ ..\packages\TMDbLib.0.9.0.0-alpha\lib\net45\TMDbLib.dll + + ..\packages\Polly-Signed.4.2.0\lib\net45\Polly.dll + @@ -81,6 +84,7 @@ + diff --git a/PlexRequests.Api/RetryHandler.cs b/PlexRequests.Api/RetryHandler.cs new file mode 100644 index 000000000..20267b228 --- /dev/null +++ b/PlexRequests.Api/RetryHandler.cs @@ -0,0 +1,34 @@ +using System; +using Polly.Retry; +using Polly; + +namespace PlexRequests.Api +{ + public static class RetryHandler + { + public static RetryPolicy RetryAndWaitPolicy(TimeSpan[] TimeSpan, Action action) + { + var policy = Policy.Handle () + .WaitAndRetry(TimeSpan, (exception, timeSpan) => action()); + + return policy; + } + + public static RetryPolicy RetryAndWaitPolicy(TimeSpan[] TimeSpan) + { + var policy = Policy.Handle () + .WaitAndRetry(TimeSpan); + + return policy; + } + + public static RetryPolicy RetryAndWaitPolicy(TimeSpan[] TimeSpan, Action action) + { + var policy = Policy.Handle () + .WaitAndRetry(TimeSpan, (exception, timeSpan) => action(exception, timeSpan)); + + return policy; + } + } +} + diff --git a/PlexRequests.Api/packages.config b/PlexRequests.Api/packages.config index 23ed979dc..5ebdcba2a 100644 --- a/PlexRequests.Api/packages.config +++ b/PlexRequests.Api/packages.config @@ -4,6 +4,7 @@ + \ No newline at end of file diff --git a/PlexRequests.UI/Validators/SonarrValidator.cs b/PlexRequests.UI/Validators/SonarrValidator.cs index e35eb654b..b252bd3b0 100644 --- a/PlexRequests.UI/Validators/SonarrValidator.cs +++ b/PlexRequests.UI/Validators/SonarrValidator.cs @@ -38,7 +38,6 @@ namespace PlexRequests.UI.Validators RuleFor(request => request.Ip).NotEmpty().WithMessage("You must specify a IP/Host name."); RuleFor(request => request.Port).NotEmpty().WithMessage("You must specify a Port."); RuleFor(request => request.QualityProfile).NotEmpty().WithMessage("You must specify a Quality Profile."); - RuleFor(request => request.RootPath).NotEmpty().WithMessage("You must specify a Root Path."); } } } \ No newline at end of file From e9937884adb4d7aff48e154684ae3f0f5da162e0 Mon Sep 17 00:00:00 2001 From: TidusJar Date: Thu, 12 May 2016 23:06:17 -0400 Subject: [PATCH 33/76] Finished #208 and #202 --- .../PlexRequests.Api.Interfaces.csproj | 3 - PlexRequests.Api/CouchPotatoApi.cs | 31 ++++++++-- PlexRequests.Api/SickrageApi.cs | 60 +++++++++++++++---- PlexRequests.Api/SonarrApi.cs | 32 ++++++++-- 4 files changed, 101 insertions(+), 25 deletions(-) diff --git a/PlexRequests.Api.Interfaces/PlexRequests.Api.Interfaces.csproj b/PlexRequests.Api.Interfaces/PlexRequests.Api.Interfaces.csproj index 033b18133..9e7c8e613 100644 --- a/PlexRequests.Api.Interfaces/PlexRequests.Api.Interfaces.csproj +++ b/PlexRequests.Api.Interfaces/PlexRequests.Api.Interfaces.csproj @@ -39,9 +39,6 @@ - - ..\packages\RestSharp.105.2.3\lib\net452\RestSharp.dll - diff --git a/PlexRequests.Api/CouchPotatoApi.cs b/PlexRequests.Api/CouchPotatoApi.cs index 96d245cf9..e48cfd84d 100644 --- a/PlexRequests.Api/CouchPotatoApi.cs +++ b/PlexRequests.Api/CouchPotatoApi.cs @@ -62,7 +62,13 @@ namespace PlexRequests.Api request.AddUrlSegment("imdbid", imdbid); request.AddUrlSegment("title", title); - var obj = Api.ExecuteJson(request, baseUrl); + var policy = RetryHandler.RetryAndWaitPolicy (new TimeSpan[] { + TimeSpan.FromSeconds (2), + TimeSpan.FromSeconds(5), + TimeSpan.FromSeconds(10) + }, (exception, timespan) => Log.Error (exception, "Exception when calling AddMovie for CP, Retrying {0}", timespan)); + + var obj = (JObject)policy.Execute( () => Api.ExecuteJson(request, baseUrl)); Log.Trace("CP movie Add result count {0}", obj.Count); if (obj.Count > 0) @@ -99,8 +105,13 @@ namespace PlexRequests.Api }; request.AddUrlSegment("apikey", apiKey); + var policy = RetryHandler.RetryAndWaitPolicy (new TimeSpan[] { + TimeSpan.FromSeconds (2), + TimeSpan.FromSeconds(5), + TimeSpan.FromSeconds(10) + }, (exception, timespan) => Log.Error (exception, "Exception when calling GetStatus for CP, Retrying {0}", timespan)); - return Api.Execute(request,url); + return (CouchPotatoStatus)policy.Execute( () => Api.Execute(request,url)); } public CouchPotatoProfiles GetProfiles(Uri url, string apiKey) @@ -114,7 +125,13 @@ namespace PlexRequests.Api request.AddUrlSegment("apikey", apiKey); - return Api.Execute(request, url); + var policy = RetryHandler.RetryAndWaitPolicy (new TimeSpan[] { + TimeSpan.FromSeconds (2), + TimeSpan.FromSeconds(5), + TimeSpan.FromSeconds(10) + }, (exception, timespan) => Log.Error (exception, "Exception when calling GetProfiles for CP, Retrying {0}", timespan)); + + return (CouchPotatoProfiles)policy.Execute( () => Api.Execute(request,url)); } public CouchPotatoMovies GetMovies(Uri baseUrl, string apiKey, string[] status) @@ -128,7 +145,13 @@ namespace PlexRequests.Api request.AddUrlSegment("status", string.Join(",", status)); try { - return Api.Execute(request, baseUrl); + var policy = RetryHandler.RetryAndWaitPolicy (new TimeSpan[] { + TimeSpan.FromSeconds (5), + TimeSpan.FromSeconds(10), + TimeSpan.FromSeconds(30) + }, (exception, timespan) => Log.Error (exception, "Exception when calling GetMovies for CP, Retrying {0}", timespan)); + + return (CouchPotatoMovies)policy.Execute( () => Api.Execute(request,baseUrl)); } catch (Exception e) // Request error is already logged in the ApiRequest class { diff --git a/PlexRequests.Api/SickrageApi.cs b/PlexRequests.Api/SickrageApi.cs index e600d5aec..72dc9d468 100644 --- a/PlexRequests.Api/SickrageApi.cs +++ b/PlexRequests.Api/SickrageApi.cs @@ -24,6 +24,7 @@ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // ************************************************************************/ +using Polly; #endregion @@ -82,9 +83,14 @@ namespace PlexRequests.Api request.AddQueryParameter("initial", quality); } - Log.Trace("Entering `Execute`"); - var obj = Api.Execute(request, baseUrl); - Log.Trace("Exiting `Execute`"); + var policy = RetryHandler.RetryAndWaitPolicy (new TimeSpan[] { + TimeSpan.FromSeconds (2), + TimeSpan.FromSeconds(5), + TimeSpan.FromSeconds(10) + }, (exception, timespan) => Log.Error (exception, "Exception when calling AddSeries for SR, Retrying {0}", timespan)); + + + var obj = policy.Execute( () => Api.Execute(request, baseUrl)); Log.Trace("obj Result:"); Log.Trace(obj.DumpJson()); @@ -95,7 +101,6 @@ namespace PlexRequests.Api var seasonIncrement = 0; var seasonList = new SickRageSeasonList(); - Log.Trace("while (seasonIncrement < seasonCount) where seasonCount = {0}", seasonCount); try { while (seasonIncrement < seasonCount) @@ -132,7 +137,8 @@ namespace PlexRequests.Api foreach (var s in seasons) { Log.Trace("Adding season {0}", s); - var result = await AddSeason(tvdbId, s, apiKey, baseUrl); + + var result = await AddSeason(tvdbId, s, apiKey, baseUrl); Log.Trace("SickRage adding season results: "); Log.Trace(result.DumpJson()); } @@ -157,8 +163,17 @@ namespace PlexRequests.Api Method = Method.GET }; + + var policy = RetryHandler.RetryAndWaitPolicy (new TimeSpan[] { + TimeSpan.FromSeconds (2), + TimeSpan.FromSeconds(5), + TimeSpan.FromSeconds(10) + }, (exception, timespan) => Log.Error (exception, "Exception when calling Ping for SR, Retrying {0}", timespan)); + + + request.AddUrlSegment("apiKey", apiKey); - var obj = Api.ExecuteJson(request, baseUrl); + var obj = policy.Execute( () => Api.ExecuteJson(request, baseUrl)); return obj; } @@ -176,9 +191,14 @@ namespace PlexRequests.Api try { - Log.Trace("Entering `ExecuteJson`"); - var obj = Api.ExecuteJson(request, baseUrl); - Log.Trace("Exited `ExecuteJson`"); + var policy = RetryHandler.RetryAndWaitPolicy (new TimeSpan[] { + TimeSpan.FromSeconds (2), + TimeSpan.FromSeconds(5), + TimeSpan.FromSeconds(10) + }, (exception, timespan) => Log.Error (exception, "Exception when calling VerifyShowHasLoaded for SR, Retrying {0}", timespan)); + + + var obj = policy.Execute( () => Api.ExecuteJson(request, baseUrl)); return obj; } catch (Exception e) @@ -203,10 +223,16 @@ namespace PlexRequests.Api await Task.Run(() => Thread.Sleep(2000)); return await Task.Run(() => { - Log.Trace("Entering `Execute` in a new `Task`"); - var result = Api.Execute(request, baseUrl); - Log.Trace("Exiting `Execute` and yeilding `Task` result"); + var policy = RetryHandler.RetryAndWaitPolicy (new TimeSpan[] { + TimeSpan.FromSeconds (2), + TimeSpan.FromSeconds(5), + TimeSpan.FromSeconds(10) + }, (exception, timespan) => Log.Error (exception, "Exception when calling AddSeason for SR, Retrying {0}", timespan)); + + + var result = policy.Execute(() => Api.Execute(request, baseUrl)); + return result; }).ConfigureAwait(false); } @@ -225,7 +251,15 @@ namespace PlexRequests.Api { try { - return Api.Execute(request, baseUrl); + var policy = RetryHandler.RetryAndWaitPolicy (new TimeSpan[] { + TimeSpan.FromSeconds (5), + TimeSpan.FromSeconds(10), + TimeSpan.FromSeconds(30) + }, (exception, timespan) => Log.Error (exception, "Exception when calling GetShows for SR, Retrying {0}", timespan)); + + + return policy.Execute(() => Api.Execute(request, baseUrl)); + } catch (ApiRequestException) { diff --git a/PlexRequests.Api/SonarrApi.cs b/PlexRequests.Api/SonarrApi.cs index 3d160187a..7d26a8d5e 100644 --- a/PlexRequests.Api/SonarrApi.cs +++ b/PlexRequests.Api/SonarrApi.cs @@ -56,8 +56,13 @@ namespace PlexRequests.Api var request = new RestRequest { Resource = "/api/profile", Method = Method.GET }; request.AddHeader("X-Api-Key", apiKey); + var policy = RetryHandler.RetryAndWaitPolicy (new TimeSpan[] { + TimeSpan.FromSeconds (2), + TimeSpan.FromSeconds(5), + TimeSpan.FromSeconds(10) + }, (exception, timespan) => Log.Error (exception, "Exception when calling GetProfiles for Sonarr, Retrying {0}", timespan)); - var obj = Api.ExecuteJson>(request, baseUrl); + var obj = policy.Execute(() => Api.ExecuteJson>(request, baseUrl)); return obj; } @@ -102,7 +107,13 @@ namespace PlexRequests.Api SonarrAddSeries result; try { - result = Api.ExecuteJson(request, baseUrl); + var policy = RetryHandler.RetryAndWaitPolicy (new TimeSpan[] { + TimeSpan.FromSeconds (2), + TimeSpan.FromSeconds(5), + TimeSpan.FromSeconds(10) + }, (exception, timespan) => Log.Error (exception, "Exception when calling AddSeries for Sonarr, Retrying {0}", timespan)); + + result = policy.Execute(() => Api.ExecuteJson(request, baseUrl)); } catch (JsonSerializationException jse) { @@ -121,7 +132,13 @@ namespace PlexRequests.Api var request = new RestRequest { Resource = "/api/system/status", Method = Method.GET }; request.AddHeader("X-Api-Key", apiKey); - var obj = Api.ExecuteJson(request, baseUrl); + var policy = RetryHandler.RetryAndWaitPolicy (new TimeSpan[] { + TimeSpan.FromSeconds (2), + TimeSpan.FromSeconds(5), + TimeSpan.FromSeconds(10) + }, (exception, timespan) => Log.Error (exception, "Exception when calling SystemStatus for Sonarr, Retrying {0}", timespan)); + + var obj = policy.Execute(() => Api.ExecuteJson(request, baseUrl)); return obj; } @@ -132,9 +149,14 @@ namespace PlexRequests.Api request.AddHeader("X-Api-Key", apiKey); try { + var policy = RetryHandler.RetryAndWaitPolicy (new TimeSpan[] { + TimeSpan.FromSeconds (5), + TimeSpan.FromSeconds(10), + TimeSpan.FromSeconds(30) + }, (exception, timespan) => Log.Error (exception, "Exception when calling GetSeries for Sonarr, Retrying {0}", timespan)); - return Api.Execute>(request, baseUrl); - } + return policy.Execute(() => Api.ExecuteJson>(request, baseUrl)); + } catch (ApiRequestException) { Log.Error("There has been an API exception when getting the Sonarr Series"); From ba78ca282d92bf0b7b4f4048755813bfb33790c1 Mon Sep 17 00:00:00 2001 From: TidusJar Date: Thu, 12 May 2016 23:09:09 -0400 Subject: [PATCH 34/76] better handling for #202 --- PlexRequests.Api/PlexApi.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/PlexRequests.Api/PlexApi.cs b/PlexRequests.Api/PlexApi.cs index 5367cb267..4ef2e9fe2 100644 --- a/PlexRequests.Api/PlexApi.cs +++ b/PlexRequests.Api/PlexApi.cs @@ -194,9 +194,9 @@ namespace PlexRequests.Api return (PlexLibraries)policy.Execute(() => api.ExecuteXml(request, plexFullHost)); } - catch (ApiRequestException) + catch (Exception e) { - Log.Error("There has been a API Exception when attempting to get the Plex Libraries"); + Log.Error(e,"There has been a API Exception when attempting to get the Plex Libraries"); return new PlexLibraries(); } } @@ -223,9 +223,9 @@ namespace PlexRequests.Api return (PlexSearch)policy.Execute(() => api.ExecuteXml(request, plexFullHost)); } - catch (ApiRequestException) + catch (Exception e) { - Log.Error("There has been a API Exception when attempting to get the Plex Library"); + Log.Error(e,"There has been a API Exception when attempting to get the Plex Library"); return new PlexSearch(); } } From 674e2bdc074eaffb50d98372d1ec1ba21fdac13d Mon Sep 17 00:00:00 2001 From: TidusJar Date: Thu, 12 May 2016 23:15:29 -0400 Subject: [PATCH 35/76] updated packages --- .../PlexRequests.Api.Interfaces.csproj | 3 +++ PlexRequests.Api.Interfaces/packages.config | 2 +- PlexRequests.Api/PlexRequests.Api.csproj | 6 +++--- PlexRequests.Api/packages.config | 2 +- PlexRequests.Services/PlexRequests.Services.csproj | 12 ------------ 5 files changed, 8 insertions(+), 17 deletions(-) diff --git a/PlexRequests.Api.Interfaces/PlexRequests.Api.Interfaces.csproj b/PlexRequests.Api.Interfaces/PlexRequests.Api.Interfaces.csproj index 9e7c8e613..62fc2658d 100644 --- a/PlexRequests.Api.Interfaces/PlexRequests.Api.Interfaces.csproj +++ b/PlexRequests.Api.Interfaces/PlexRequests.Api.Interfaces.csproj @@ -39,6 +39,9 @@ + + ..\packages\RestSharp.105.2.3\lib\net45\RestSharp.dll + diff --git a/PlexRequests.Api.Interfaces/packages.config b/PlexRequests.Api.Interfaces/packages.config index d14ce056f..a63cb4deb 100644 --- a/PlexRequests.Api.Interfaces/packages.config +++ b/PlexRequests.Api.Interfaces/packages.config @@ -1,4 +1,4 @@  - + \ No newline at end of file diff --git a/PlexRequests.Api/PlexRequests.Api.csproj b/PlexRequests.Api/PlexRequests.Api.csproj index a90f60f22..0886cc72b 100644 --- a/PlexRequests.Api/PlexRequests.Api.csproj +++ b/PlexRequests.Api/PlexRequests.Api.csproj @@ -51,15 +51,15 @@ ..\packages\NLog.4.2.3\lib\net45\NLog.dll - - ..\packages\RestSharp.105.2.3\lib\net452\RestSharp.dll - ..\packages\TMDbLib.0.9.0.0-alpha\lib\net45\TMDbLib.dll ..\packages\Polly-Signed.4.2.0\lib\net45\Polly.dll + + ..\packages\RestSharp.105.2.3\lib\net45\RestSharp.dll + diff --git a/PlexRequests.Api/packages.config b/PlexRequests.Api/packages.config index 5ebdcba2a..6484cf63a 100644 --- a/PlexRequests.Api/packages.config +++ b/PlexRequests.Api/packages.config @@ -5,6 +5,6 @@ - + \ No newline at end of file diff --git a/PlexRequests.Services/PlexRequests.Services.csproj b/PlexRequests.Services/PlexRequests.Services.csproj index d69e3d162..607bc20f9 100644 --- a/PlexRequests.Services/PlexRequests.Services.csproj +++ b/PlexRequests.Services/PlexRequests.Services.csproj @@ -67,18 +67,6 @@ ..\packages\Quartz.2.3.3\lib\net40\Quartz.dll - - ..\packages\Rx-Core.2.2.5\lib\net45\System.Reactive.Core.dll - - - ..\packages\Rx-Interfaces.2.2.5\lib\net45\System.Reactive.Interfaces.dll - - - ..\packages\Rx-Linq.2.2.5\lib\net45\System.Reactive.Linq.dll - - - ..\packages\Rx-PlatformServices.2.2.5\lib\net45\System.Reactive.PlatformServices.dll - From 24b329b132455bbebf79d539546112e5bb8ce49a Mon Sep 17 00:00:00 2001 From: TidusJar Date: Thu, 12 May 2016 23:23:06 -0400 Subject: [PATCH 36/76] Downgraded packages --- PlexRequests.Api.Models/packages.config | 2 +- PlexRequests.Api/packages.config | 10 ++-- PlexRequests.Helpers/packages.config | 4 +- PlexRequests.Services/packages.config | 22 +++---- PlexRequests.UI/packages.config | 76 ++++++++++++------------- 5 files changed, 57 insertions(+), 57 deletions(-) diff --git a/PlexRequests.Api.Models/packages.config b/PlexRequests.Api.Models/packages.config index 382dd3fdd..62d43545b 100644 --- a/PlexRequests.Api.Models/packages.config +++ b/PlexRequests.Api.Models/packages.config @@ -1,4 +1,4 @@  - + \ No newline at end of file diff --git a/PlexRequests.Api/packages.config b/PlexRequests.Api/packages.config index 6484cf63a..dd0b7d8d3 100644 --- a/PlexRequests.Api/packages.config +++ b/PlexRequests.Api/packages.config @@ -1,10 +1,10 @@  - - - - + + + + - + \ No newline at end of file diff --git a/PlexRequests.Helpers/packages.config b/PlexRequests.Helpers/packages.config index dc63c2a11..e9ff15713 100644 --- a/PlexRequests.Helpers/packages.config +++ b/PlexRequests.Helpers/packages.config @@ -1,5 +1,5 @@  - - + + \ No newline at end of file diff --git a/PlexRequests.Services/packages.config b/PlexRequests.Services/packages.config index 4560acea8..f5ede1176 100644 --- a/PlexRequests.Services/packages.config +++ b/PlexRequests.Services/packages.config @@ -1,14 +1,14 @@  - - - - - - - - - - - + + + + + + + + + + + \ No newline at end of file diff --git a/PlexRequests.UI/packages.config b/PlexRequests.UI/packages.config index 304f7dab8..b915a0ecf 100644 --- a/PlexRequests.UI/packages.config +++ b/PlexRequests.UI/packages.config @@ -1,41 +1,41 @@  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 8f16869c68150d1247551a425f1063753614db07 Mon Sep 17 00:00:00 2001 From: TidusJar Date: Thu, 12 May 2016 23:32:23 -0400 Subject: [PATCH 37/76] nm, --- PlexRequests.Services/packages.config | 5 ----- PlexRequests.UI/packages.config | 4 ++-- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/PlexRequests.Services/packages.config b/PlexRequests.Services/packages.config index f5ede1176..bed7e714f 100644 --- a/PlexRequests.Services/packages.config +++ b/PlexRequests.Services/packages.config @@ -6,9 +6,4 @@ - - - - - \ No newline at end of file diff --git a/PlexRequests.UI/packages.config b/PlexRequests.UI/packages.config index b915a0ecf..85b9b4822 100644 --- a/PlexRequests.UI/packages.config +++ b/PlexRequests.UI/packages.config @@ -25,9 +25,9 @@ - + - + From 96abba49f7aa16b88a2197dd0d8d8b9fc799669e Mon Sep 17 00:00:00 2001 From: TidusJar Date: Thu, 12 May 2016 23:41:14 -0400 Subject: [PATCH 38/76] packages --- PlexRequests.Core/packages.config | 12 ++++++------ PlexRequests.Store/packages.config | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/PlexRequests.Core/packages.config b/PlexRequests.Core/packages.config index 6fae42bd4..d378174d8 100644 --- a/PlexRequests.Core/packages.config +++ b/PlexRequests.Core/packages.config @@ -1,9 +1,9 @@  - - - - - - + + + + + + \ No newline at end of file diff --git a/PlexRequests.Store/packages.config b/PlexRequests.Store/packages.config index e6a6a64fa..0347c0d1b 100644 --- a/PlexRequests.Store/packages.config +++ b/PlexRequests.Store/packages.config @@ -1,7 +1,7 @@  - - - - + + + + \ No newline at end of file From 741a4ae75c80a42740473e5020c6d6d6ff6472b3 Mon Sep 17 00:00:00 2001 From: TidusJar Date: Sun, 15 May 2016 20:32:59 -0400 Subject: [PATCH 39/76] - Improved the RetryHandler. - Made the tester buttons on the settings pages a bit more robust and added an indication when it's testing (spinner) --- PlexRequests.Api/CouchPotatoApi.cs | 46 ++++---- PlexRequests.Api/HeadphonesApi.cs | 4 +- PlexRequests.Api/PlexApi.cs | 104 ++++++++---------- PlexRequests.Api/RetryHandler.cs | 49 ++++++++- PlexRequests.Api/SickrageApi.cs | 31 ++---- PlexRequests.Api/SonarrApi.cs | 6 +- PlexRequests.UI/Modules/AdminModule.cs | 35 ++++-- .../Modules/ApplicationTesterModule.cs | 10 +- PlexRequests.UI/Modules/LoginModule.cs | 8 +- .../Views/Admin/Authentication.cshtml | 13 ++- .../Views/Admin/CouchPotato.cshtml | 9 +- .../Views/Admin/EmailNotifications.cshtml | 8 +- PlexRequests.UI/Views/Admin/Headphones.cshtml | 7 +- PlexRequests.UI/Views/Admin/Plex.cshtml | 11 +- .../Admin/PushbulletNotifications.cshtml | 7 +- .../Views/Admin/PushoverNotifications.cshtml | 6 +- PlexRequests.UI/Views/Admin/Sickrage.cshtml | 7 +- PlexRequests.UI/Views/Admin/Sonarr.cshtml | 16 ++- PlexRequests.UI/Views/Login/Index.cshtml | 1 + PlexRequests.UI/Views/Shared/_Layout.cshtml | 3 +- 20 files changed, 232 insertions(+), 149 deletions(-) diff --git a/PlexRequests.Api/CouchPotatoApi.cs b/PlexRequests.Api/CouchPotatoApi.cs index e48cfd84d..b63c85661 100644 --- a/PlexRequests.Api/CouchPotatoApi.cs +++ b/PlexRequests.Api/CouchPotatoApi.cs @@ -62,13 +62,12 @@ namespace PlexRequests.Api request.AddUrlSegment("imdbid", imdbid); request.AddUrlSegment("title", title); - var policy = RetryHandler.RetryAndWaitPolicy (new TimeSpan[] { + var obj = RetryHandler.Execute(() => Api.ExecuteJson (request, baseUrl),new TimeSpan[] { TimeSpan.FromSeconds (2), TimeSpan.FromSeconds(5), - TimeSpan.FromSeconds(10) - }, (exception, timespan) => Log.Error (exception, "Exception when calling AddMovie for CP, Retrying {0}", timespan)); + TimeSpan.FromSeconds(10)}, + (exception, timespan) => Log.Error (exception, "Exception when calling AddMovie for CP, Retrying {0}", timespan)); - var obj = (JObject)policy.Execute( () => Api.ExecuteJson(request, baseUrl)); Log.Trace("CP movie Add result count {0}", obj.Count); if (obj.Count > 0) @@ -105,13 +104,15 @@ namespace PlexRequests.Api }; request.AddUrlSegment("apikey", apiKey); - var policy = RetryHandler.RetryAndWaitPolicy (new TimeSpan[] { + + + var obj = RetryHandler.Execute(() => Api.Execute (request, url),new TimeSpan[] { TimeSpan.FromSeconds (2), TimeSpan.FromSeconds(5), - TimeSpan.FromSeconds(10) - }, (exception, timespan) => Log.Error (exception, "Exception when calling GetStatus for CP, Retrying {0}", timespan)); - - return (CouchPotatoStatus)policy.Execute( () => Api.Execute(request,url)); + TimeSpan.FromSeconds(10)}, + (exception, timespan) => Log.Error (exception, "Exception when calling GetStatus for CP, Retrying {0}", timespan)); + + return obj; } public CouchPotatoProfiles GetProfiles(Uri url, string apiKey) @@ -125,13 +126,10 @@ namespace PlexRequests.Api request.AddUrlSegment("apikey", apiKey); - var policy = RetryHandler.RetryAndWaitPolicy (new TimeSpan[] { - TimeSpan.FromSeconds (2), - TimeSpan.FromSeconds(5), - TimeSpan.FromSeconds(10) - }, (exception, timespan) => Log.Error (exception, "Exception when calling GetProfiles for CP, Retrying {0}", timespan)); - - return (CouchPotatoProfiles)policy.Execute( () => Api.Execute(request,url)); + var obj = RetryHandler.Execute(() => Api.Execute (request, url),null, + (exception, timespan) => Log.Error (exception, "Exception when calling GetProfiles for CP, Retrying {0}", timespan)); + + return obj; } public CouchPotatoMovies GetMovies(Uri baseUrl, string apiKey, string[] status) @@ -145,13 +143,15 @@ namespace PlexRequests.Api request.AddUrlSegment("status", string.Join(",", status)); try { - var policy = RetryHandler.RetryAndWaitPolicy (new TimeSpan[] { - TimeSpan.FromSeconds (5), - TimeSpan.FromSeconds(10), - TimeSpan.FromSeconds(30) - }, (exception, timespan) => Log.Error (exception, "Exception when calling GetMovies for CP, Retrying {0}", timespan)); - - return (CouchPotatoMovies)policy.Execute( () => Api.Execute(request,baseUrl)); + var obj = RetryHandler.Execute(() => Api.Execute (request, baseUrl), + new TimeSpan[] { + TimeSpan.FromSeconds (5), + TimeSpan.FromSeconds(10), + TimeSpan.FromSeconds(30) + }, + (exception, timespan) => Log.Error (exception, "Exception when calling GetMovies for CP, Retrying {0}", timespan)); + + return obj; } catch (Exception e) // Request error is already logged in the ApiRequest class { diff --git a/PlexRequests.Api/HeadphonesApi.cs b/PlexRequests.Api/HeadphonesApi.cs index 34cecb8fe..94c29f126 100644 --- a/PlexRequests.Api/HeadphonesApi.cs +++ b/PlexRequests.Api/HeadphonesApi.cs @@ -71,7 +71,7 @@ namespace PlexRequests.Api return albumResult; } - catch (JsonSerializationException jse) + catch (Exception jse) { Log.Error(jse); return false; // If there is no matching result we do not get returned a JSON string, it just returns "false". @@ -94,7 +94,7 @@ namespace PlexRequests.Api return result; } - catch (JsonSerializationException jse) + catch (Exception jse) { Log.Error(jse); return new List(); diff --git a/PlexRequests.Api/PlexApi.cs b/PlexRequests.Api/PlexApi.cs index 4ef2e9fe2..1b20a68e9 100644 --- a/PlexRequests.Api/PlexApi.cs +++ b/PlexRequests.Api/PlexApi.cs @@ -47,6 +47,17 @@ namespace PlexRequests.Api Version = AssemblyHelper.GetAssemblyVersion(); } + public PlexApi (IApiRequest api) + { + Api = api; + } + + private IApiRequest Api { get; } + + private const string SignInUri = "https://plex.tv/users/sign_in.json"; + private const string FriendsUri = "https://plex.tv/pms/friends/all"; + private const string GetAccountUri = "https://plex.tv/users/account"; + private static Logger Log = LogManager.GetCurrentClassLogger(); private static string Version { get; } @@ -69,15 +80,11 @@ namespace PlexRequests.Api request.AddJsonBody(userModel); - var api = new ApiRequest(); - - var policy = RetryHandler.RetryAndWaitPolicy (new TimeSpan[] { - TimeSpan.FromSeconds (2), - TimeSpan.FromSeconds(5), - TimeSpan.FromSeconds(10) - }, (exception, timespan) => Log.Error (exception, "Exception when calling SignIn for Plex, Retrying {0}", timespan)); - - return (PlexAuthentication)policy.Execute(() => api.Execute(request, new Uri("https://plex.tv/users/sign_in.json"))); + var obj = RetryHandler.Execute(() => Api.Execute (request, new Uri(SignInUri)), + null, + (exception, timespan) => Log.Error (exception, "Exception when calling SignIn for Plex, Retrying {0}", timespan)); + + return obj; } public PlexFriends GetUsers(string authToken) @@ -89,14 +96,10 @@ namespace PlexRequests.Api AddHeaders(ref request, authToken); - var api = new ApiRequest(); - var policy = RetryHandler.RetryAndWaitPolicy (new TimeSpan[] { - TimeSpan.FromSeconds (2), - TimeSpan.FromSeconds(5), - TimeSpan.FromSeconds(10) - }, (exception, timespan) => Log.Error (exception, "Exception when calling GetUsers for Plex, Retrying {0}", timespan)); - - var users = (PlexFriends)policy.Execute(() =>api.ExecuteXml(request, new Uri("https://plex.tv/pms/friends/all"))); + var users = RetryHandler.Execute(() => Api.Execute (request, new Uri(FriendsUri)), + null, + (exception, timespan) => Log.Error (exception, "Exception when calling GetUsers for Plex, Retrying {0}", timespan)); + return users; } @@ -119,14 +122,9 @@ namespace PlexRequests.Api request.AddUrlSegment("searchTerm", searchTerm); AddHeaders(ref request, authToken); - var api = new ApiRequest(); - var policy = RetryHandler.RetryAndWaitPolicy (new TimeSpan[] { - TimeSpan.FromSeconds (2), - TimeSpan.FromSeconds(5), - TimeSpan.FromSeconds(10) - }, (exception, timespan) => Log.Error (exception, "Exception when calling SearchContent for Plex, Retrying {0}", timespan)); - - var search = (PlexSearch)policy.Execute(() => api.ExecuteXml(request, plexFullHost)); + var search = RetryHandler.Execute(() => Api.ExecuteXml (request, plexFullHost), + null, + (exception, timespan) => Log.Error (exception, "Exception when calling SearchContent for Plex, Retrying {0}", timespan)); return search; } @@ -139,15 +137,11 @@ namespace PlexRequests.Api }; AddHeaders(ref request, authToken); - var policy = RetryHandler.RetryAndWaitPolicy (new TimeSpan[] { - TimeSpan.FromSeconds (2), - TimeSpan.FromSeconds(5), - TimeSpan.FromSeconds(10) - }, (exception, timespan) => Log.Error (exception, "Exception when calling GetStatus for Plex, Retrying {0}", timespan)); - - var api = new ApiRequest(); - var users = (PlexStatus)policy.Execute(() => api.ExecuteXml(request, uri)); + var users = RetryHandler.Execute(() => Api.ExecuteXml (request, uri), + null, + (exception, timespan) => Log.Error (exception, "Exception when calling GetStatus for Plex, Retrying {0}", timespan)); + return users; } @@ -160,16 +154,10 @@ namespace PlexRequests.Api AddHeaders(ref request, authToken); - var policy = RetryHandler.RetryAndWaitPolicy (new TimeSpan[] { - TimeSpan.FromSeconds (2), - TimeSpan.FromSeconds(5), - TimeSpan.FromSeconds(10) - }, (exception, timespan) => Log.Error (exception, "Exception when calling GetAccount for Plex, Retrying: {0}", timespan)); - - - var api = new ApiRequest(); - var account = (PlexAccount)policy.Execute(() => api.ExecuteXml(request, new Uri("https://plex.tv/users/account"))); - + var account = RetryHandler.Execute(() => Api.ExecuteXml (request, new Uri(GetAccountUri)), + null, + (exception, timespan) => Log.Error (exception, "Exception when calling GetAccount for Plex, Retrying {0}", timespan)); + return account; } @@ -183,16 +171,17 @@ namespace PlexRequests.Api AddHeaders(ref request, authToken); - var api = new ApiRequest(); try { - var policy = RetryHandler.RetryAndWaitPolicy (new TimeSpan[] { - TimeSpan.FromSeconds (5), - TimeSpan.FromSeconds(10), - TimeSpan.FromSeconds(30) - }, (exception, timespan) => Log.Error (exception, "Exception when calling GetLibrarySections for Plex, Retrying {0}", timespan)); + var lib = RetryHandler.Execute(() => Api.ExecuteXml (request, plexFullHost), + new TimeSpan[] { + TimeSpan.FromSeconds (5), + TimeSpan.FromSeconds(10), + TimeSpan.FromSeconds(30) + }, + (exception, timespan) => Log.Error (exception, "Exception when calling GetLibrarySections for Plex, Retrying {0}", timespan)); - return (PlexLibraries)policy.Execute(() => api.ExecuteXml(request, plexFullHost)); + return lib; } catch (Exception e) { @@ -212,16 +201,17 @@ namespace PlexRequests.Api request.AddUrlSegment("libraryId", libraryId); AddHeaders(ref request, authToken); - var api = new ApiRequest(); try { - var policy = RetryHandler.RetryAndWaitPolicy (new TimeSpan[] { - TimeSpan.FromSeconds (5), - TimeSpan.FromSeconds(10), - TimeSpan.FromSeconds(30) - }, (exception, timespan) => Log.Error (exception, "Exception when calling GetLibrary for Plex, Retrying {0}", timespan)); + var lib = RetryHandler.Execute(() => Api.ExecuteXml (request, plexFullHost), + new TimeSpan[] { + TimeSpan.FromSeconds (5), + TimeSpan.FromSeconds(10), + TimeSpan.FromSeconds(30) + }, + (exception, timespan) => Log.Error (exception, "Exception when calling GetLibrary for Plex, Retrying {0}", timespan)); - return (PlexSearch)policy.Execute(() => api.ExecuteXml(request, plexFullHost)); + return lib; } catch (Exception e) { diff --git a/PlexRequests.Api/RetryHandler.cs b/PlexRequests.Api/RetryHandler.cs index 20267b228..c9e336f9e 100644 --- a/PlexRequests.Api/RetryHandler.cs +++ b/PlexRequests.Api/RetryHandler.cs @@ -1,34 +1,71 @@ using System; using Polly.Retry; using Polly; +using System.Threading.Tasks; namespace PlexRequests.Api { public static class RetryHandler { - public static RetryPolicy RetryAndWaitPolicy(TimeSpan[] TimeSpan, Action action) + + private static TimeSpan[] DefaultTime = new TimeSpan[] { + TimeSpan.FromSeconds (2), + TimeSpan.FromSeconds(5), + TimeSpan.FromSeconds(10)}; + + public static RetryPolicy RetryAndWaitPolicy(TimeSpan[] timeSpan, Action action) { + if(timeSpan == null) + { + timeSpan = DefaultTime; + } var policy = Policy.Handle () - .WaitAndRetry(TimeSpan, (exception, timeSpan) => action()); + .WaitAndRetry(timeSpan, (e, ts) => action()); return policy; } - public static RetryPolicy RetryAndWaitPolicy(TimeSpan[] TimeSpan) + public static RetryPolicy RetryAndWaitPolicy(TimeSpan[] timeSpan) { + if(timeSpan == null) + { + timeSpan = DefaultTime; + } var policy = Policy.Handle () - .WaitAndRetry(TimeSpan); + .WaitAndRetry(timeSpan); return policy; } - public static RetryPolicy RetryAndWaitPolicy(TimeSpan[] TimeSpan, Action action) + public static RetryPolicy RetryAndWaitPolicy(TimeSpan[] timeSpan, Action action) { + if(timeSpan == null) + { + timeSpan = DefaultTime; + } var policy = Policy.Handle () - .WaitAndRetry(TimeSpan, (exception, timeSpan) => action(exception, timeSpan)); + .WaitAndRetry(timeSpan, (exception, ts) => action(exception, ts)); return policy; } + + public static T Execute(Func action, TimeSpan[] timeSpan) + { + var policy = RetryAndWaitPolicy (timeSpan); + + return policy.Execute (action); + } + + public static T Execute(Func func, TimeSpan[] timeSpan, Action action) + { + if(timeSpan == null) + { + timeSpan = DefaultTime; + } + var policy = RetryAndWaitPolicy (timeSpan, action); + + return policy.Execute (func); + } } } diff --git a/PlexRequests.Api/SickrageApi.cs b/PlexRequests.Api/SickrageApi.cs index 72dc9d468..9ecc535c2 100644 --- a/PlexRequests.Api/SickrageApi.cs +++ b/PlexRequests.Api/SickrageApi.cs @@ -83,11 +83,7 @@ namespace PlexRequests.Api request.AddQueryParameter("initial", quality); } - var policy = RetryHandler.RetryAndWaitPolicy (new TimeSpan[] { - TimeSpan.FromSeconds (2), - TimeSpan.FromSeconds(5), - TimeSpan.FromSeconds(10) - }, (exception, timespan) => Log.Error (exception, "Exception when calling AddSeries for SR, Retrying {0}", timespan)); + var policy = RetryHandler.RetryAndWaitPolicy (null,(exception, timespan) => Log.Error (exception, "Exception when calling AddSeries for SR, Retrying {0}", timespan)); var obj = policy.Execute( () => Api.Execute(request, baseUrl)); @@ -164,11 +160,7 @@ namespace PlexRequests.Api }; - var policy = RetryHandler.RetryAndWaitPolicy (new TimeSpan[] { - TimeSpan.FromSeconds (2), - TimeSpan.FromSeconds(5), - TimeSpan.FromSeconds(10) - }, (exception, timespan) => Log.Error (exception, "Exception when calling Ping for SR, Retrying {0}", timespan)); + var policy = RetryHandler.RetryAndWaitPolicy (null,(exception, timespan) => Log.Error (exception, "Exception when calling Ping for SR, Retrying {0}", timespan)); @@ -191,11 +183,8 @@ namespace PlexRequests.Api try { - var policy = RetryHandler.RetryAndWaitPolicy (new TimeSpan[] { - TimeSpan.FromSeconds (2), - TimeSpan.FromSeconds(5), - TimeSpan.FromSeconds(10) - }, (exception, timespan) => Log.Error (exception, "Exception when calling VerifyShowHasLoaded for SR, Retrying {0}", timespan)); + var policy = RetryHandler.RetryAndWaitPolicy (null,(exception, timespan) => + Log.Error (exception, "Exception when calling VerifyShowHasLoaded for SR, Retrying {0}", timespan)); var obj = policy.Execute( () => Api.ExecuteJson(request, baseUrl)); @@ -224,12 +213,8 @@ namespace PlexRequests.Api return await Task.Run(() => { - var policy = RetryHandler.RetryAndWaitPolicy (new TimeSpan[] { - TimeSpan.FromSeconds (2), - TimeSpan.FromSeconds(5), - TimeSpan.FromSeconds(10) - }, (exception, timespan) => Log.Error (exception, "Exception when calling AddSeason for SR, Retrying {0}", timespan)); - + var policy = RetryHandler.RetryAndWaitPolicy (null,(exception, timespan) => + Log.Error (exception, "Exception when calling AddSeason for SR, Retrying {0}", timespan)); var result = policy.Execute(() => Api.Execute(request, baseUrl)); @@ -255,8 +240,8 @@ namespace PlexRequests.Api TimeSpan.FromSeconds (5), TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(30) - }, (exception, timespan) => Log.Error (exception, "Exception when calling GetShows for SR, Retrying {0}", timespan)); - + }, (exception, timespan) => + Log.Error (exception, "Exception when calling GetShows for SR, Retrying {0}", timespan)); return policy.Execute(() => Api.Execute(request, baseUrl)); diff --git a/PlexRequests.Api/SonarrApi.cs b/PlexRequests.Api/SonarrApi.cs index 7d26a8d5e..b56e969a7 100644 --- a/PlexRequests.Api/SonarrApi.cs +++ b/PlexRequests.Api/SonarrApi.cs @@ -157,10 +157,10 @@ namespace PlexRequests.Api return policy.Execute(() => Api.ExecuteJson>(request, baseUrl)); } - catch (ApiRequestException) + catch (Exception e) { - Log.Error("There has been an API exception when getting the Sonarr Series"); - return null; + Log.Error(e, "There has been an API exception when getting the Sonarr Series"); + return null; } } } diff --git a/PlexRequests.UI/Modules/AdminModule.cs b/PlexRequests.UI/Modules/AdminModule.cs index a738e9951..36549b4d4 100644 --- a/PlexRequests.UI/Modules/AdminModule.cs +++ b/PlexRequests.UI/Modules/AdminModule.cs @@ -23,6 +23,10 @@ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // ************************************************************************/ +using System.Net; +using PlexRequests.Helpers.Exceptions; + + #endregion using System.Collections.Generic; @@ -273,18 +277,27 @@ namespace PlexRequests.UI.Modules return Response.AsJson(string.Empty); } - var users = PlexApi.GetUsers(token); - if (users == null) - { - return Response.AsJson(string.Empty); - } - if (users.User == null || users.User?.Length == 0) - { - return Response.AsJson(string.Empty); - } + try { + var users = PlexApi.GetUsers(token); + if (users == null) + { + return Response.AsJson(string.Empty); + } + if (users.User == null || users.User?.Length == 0) + { + return Response.AsJson(string.Empty); + } - var usernames = users.User.Select(x => x.Title); - return Response.AsJson(usernames); + var usernames = users.User.Select(x => x.Title); + return Response.AsJson(new {Result = true, Users = usernames}); + } catch (Exception ex) { + Log.Error (ex); + if (ex is WebException || ex is ApiRequestException) { + return Response.AsJson (new { Result = false, Message ="Could not load the user list! We have connectivity problems connecting to Plex, Please ensure we can access Plex.Tv, The error has been logged." }); + } + + return Response.AsJson (new { Result = false, Message = ex.Message}); + } } private Negotiator CouchPotato() diff --git a/PlexRequests.UI/Modules/ApplicationTesterModule.cs b/PlexRequests.UI/Modules/ApplicationTesterModule.cs index 69e2df08d..55df3310f 100644 --- a/PlexRequests.UI/Modules/ApplicationTesterModule.cs +++ b/PlexRequests.UI/Modules/ApplicationTesterModule.cs @@ -87,7 +87,7 @@ namespace PlexRequests.UI.Modules : Response.AsJson(new JsonResponseModel { Result = false, Message = "Could not connect to CouchPotato, please check your settings." }); } - catch (ApplicationException e) // Exceptions are expected if we cannot connect so we will just log and swallow them. + catch (Exception e) // Exceptions are expected if we cannot connect so we will just log and swallow them. { Log.Warn("Exception thrown when attempting to get CP's status: "); Log.Warn(e); @@ -116,7 +116,7 @@ namespace PlexRequests.UI.Modules : Response.AsJson(new JsonResponseModel { Result = false, Message = "Could not connect to Sonarr, please check your settings." }); } - catch (ApplicationException e) // Exceptions are expected, if we cannot connect so we will just log and swallow them. + catch (Exception e) // Exceptions are expected, if we cannot connect so we will just log and swallow them. { Log.Warn("Exception thrown when attempting to get Sonarr's status: "); Log.Warn(e); @@ -150,7 +150,7 @@ namespace PlexRequests.UI.Modules : Response.AsJson(new JsonResponseModel { Result = false, Message = "Could not connect to Plex, please check your settings." }); } - catch (ApplicationException e) // Exceptions are expected, if we cannot connect so we will just log and swallow them. + catch (Exception e) // Exceptions are expected, if we cannot connect so we will just log and swallow them. { Log.Warn("Exception thrown when attempting to get Plex's status: "); Log.Warn(e); @@ -179,7 +179,7 @@ namespace PlexRequests.UI.Modules : Response.AsJson(new JsonResponseModel { Result = false, Message = "Could not connect to SickRage, please check your settings." }); } - catch (ApplicationException e) // Exceptions are expected, if we cannot connect so we will just log and swallow them. + catch (Exception e) // Exceptions are expected, if we cannot connect so we will just log and swallow them. { Log.Warn("Exception thrown when attempting to get SickRage's status: "); Log.Warn(e); @@ -214,7 +214,7 @@ namespace PlexRequests.UI.Modules } return Response.AsJson(new JsonResponseModel { Result = false, Message = "Could not connect to Headphones, please check your settings." }); } - catch (ApplicationException e) + catch (Exception e) { Log.Warn("Exception thrown when attempting to get Headphones's status: "); Log.Warn(e); diff --git a/PlexRequests.UI/Modules/LoginModule.cs b/PlexRequests.UI/Modules/LoginModule.cs index 83616a80b..a8a02baba 100644 --- a/PlexRequests.UI/Modules/LoginModule.cs +++ b/PlexRequests.UI/Modules/LoginModule.cs @@ -46,6 +46,7 @@ namespace PlexRequests.UI.Modules { { dynamic model = new ExpandoObject(); + model.Redirect = Request.Query.redirect.Value ?? string.Empty; model.Errored = Request.Query.error.HasValue; var adminCreated = UserMapper.DoUsersExist(); model.AdminExists = adminCreated; @@ -61,6 +62,7 @@ namespace PlexRequests.UI.Modules var username = (string)Request.Form.Username; var password = (string)Request.Form.Password; var dtOffset = (int)Request.Form.DateTimeOffset; + var redirect = (string)Request.Form.Redirect; var userId = UserMapper.ValidateUser(username, password); @@ -75,12 +77,8 @@ namespace PlexRequests.UI.Modules } Session[SessionKeys.UsernameKey] = username; Session[SessionKeys.ClientDateTimeOffsetKey] = dtOffset; - if (!string.IsNullOrEmpty(BaseUrl)) - { - return this.LoginAndRedirect(userId.Value, expiry, $"/{BaseUrl}"); - } - return this.LoginAndRedirect(userId.Value, expiry); + return this.LoginAndRedirect(userId.Value, expiry, redirect); }; Get["/register"] = x => diff --git a/PlexRequests.UI/Views/Admin/Authentication.cshtml b/PlexRequests.UI/Views/Admin/Authentication.cshtml index d223db1b8..1abbd3788 100644 --- a/PlexRequests.UI/Views/Admin/Authentication.cshtml +++ b/PlexRequests.UI/Views/Admin/Authentication.cshtml @@ -153,10 +153,17 @@ url = createBaseUrl(base, url); $.ajax({ type: "Get", - url: "getusers", + url: url, dataType: "json", success: function (response) { - if (response.length > 1) { + + $('#users').html(""); + if(!response.result){ + generateNotify(response.message,"danger"); + $('#users').append(""); + return; + } + if (response.users.length > 1) { $(response).each(function () { $('#users').append(""); }); @@ -167,6 +174,8 @@ error: function (e) { console.log(e); generateNotify("Something went wrong!", "danger"); + $('#users').html(""); + $('#users').append(""); } }); diff --git a/PlexRequests.UI/Views/Admin/CouchPotato.cshtml b/PlexRequests.UI/Views/Admin/CouchPotato.cshtml index fb54a6c33..0e52986e2 100644 --- a/PlexRequests.UI/Views/Admin/CouchPotato.cshtml +++ b/PlexRequests.UI/Views/Admin/CouchPotato.cshtml @@ -88,7 +88,7 @@
    - +
    @@ -170,6 +170,8 @@ e.preventDefault(); var $form = $("#mainForm"); var url = createBaseUrl(baseUrl,"/test/cp"); + $('#spinner').attr("class", "fa fa-spinner fa-spin"); + $.ajax({ type: $form.prop("method"), url: url, @@ -178,15 +180,18 @@ success: function (response) { console.log(response); if (response.result === true) { + $('#spinner').attr("class", "fa fa-check"); generateNotify(response.message, "success"); $('#authToken').val(response.authToken); } else { generateNotify(response.message, "warning"); + $('#spinner').attr("class", "fa fa-times"); } }, error: function (e) { console.log(e); - generateNotify("Something went wrong!", "danger"); + generateNotify("Something went wrong!", "danger"); + $('#spinner').attr("class", "fa fa-times"); } }); }); diff --git a/PlexRequests.UI/Views/Admin/EmailNotifications.cshtml b/PlexRequests.UI/Views/Admin/EmailNotifications.cshtml index 978283d2c..ef12311e3 100644 --- a/PlexRequests.UI/Views/Admin/EmailNotifications.cshtml +++ b/PlexRequests.UI/Views/Admin/EmailNotifications.cshtml @@ -106,7 +106,7 @@
    - +
    @@ -150,7 +150,8 @@ }); }); - $('#testEmail').click(function (e) { + $('#testEmail').click(function (e) { + $('#spinner').attr("class", "fa fa-spinner fa-spin"); var url = createBaseUrl(base, '/admin/testemailnotification'); e.preventDefault(); @@ -167,13 +168,16 @@ dataType: "json", success: function (response) { if (response.result === true) { + $('#spinner').attr("class", "fa fa-check"); generateNotify(response.message, "success"); } else { + $('#spinner').attr("class", "fa fa-times"); generateNotify(response.message, "warning"); } }, error: function (e) { console.log(e); + $('#spinner').attr("class", "fa fa-times"); generateNotify("Something went wrong!", "danger"); } }); diff --git a/PlexRequests.UI/Views/Admin/Headphones.cshtml b/PlexRequests.UI/Views/Admin/Headphones.cshtml index 47629aebc..4145b53bf 100644 --- a/PlexRequests.UI/Views/Admin/Headphones.cshtml +++ b/PlexRequests.UI/Views/Admin/Headphones.cshtml @@ -75,7 +75,7 @@
    - +
    @@ -96,6 +96,8 @@ var base = '@Html.GetBaseUrl()'; $('#testHeadphones').click(function (e) { + + $('#spinner').attr("class", "fa fa-spinner fa-spin"); e.preventDefault(); var $form = $("#mainForm"); var url = createBaseUrl(base, '/test/headphones'); @@ -107,13 +109,16 @@ success: function (response) { console.log(response); if (response.result === true) { + $('#spinner').attr("class", "fa fa-check"); generateNotify(response.message, "success"); } else { + $('#spinner').attr("class", "fa fa-times"); generateNotify(response.message, "warning"); } }, error: function (e) { console.log(e); + $('#spinner').attr("class", "fa fa-times"); generateNotify("Something went wrong!", "danger"); } }); diff --git a/PlexRequests.UI/Views/Admin/Plex.cshtml b/PlexRequests.UI/Views/Admin/Plex.cshtml index 7282218d1..a4b9a2787 100644 --- a/PlexRequests.UI/Views/Admin/Plex.cshtml +++ b/PlexRequests.UI/Views/Admin/Plex.cshtml @@ -52,7 +52,7 @@
    - +
    @@ -69,25 +69,34 @@ \ No newline at end of file diff --git a/PlexRequests.UI/Views/UserManagement/Index.cshtml b/PlexRequests.UI/Views/UserManagement/Index.cshtml new file mode 100644 index 000000000..f2e3b9d54 --- /dev/null +++ b/PlexRequests.UI/Views/UserManagement/Index.cshtml @@ -0,0 +1,14 @@ +@using PlexRequests.UI.Helpers +@{ + var baseUrl = Html.GetBaseUrl().ToHtmlString(); + var url = string.Empty; + if (!string.IsNullOrEmpty(baseUrl)) + { + url = "/" + baseUrl; + } +} + + +

    User Management

    + + \ No newline at end of file From 96a40a2aaaaa27cddfd2bc3e393478438fe34eac Mon Sep 17 00:00:00 2001 From: TidusJar Date: Sun, 15 May 2016 22:16:40 -0400 Subject: [PATCH 41/76] Added the updater to the soloution and did a bit of starting code. --- .../PlexRequests.Core.Tests.csproj | 2 +- .../PlexRequests.Services.Tests.csproj | 2 +- .../PlexRequests.UI.Tests.csproj | 2 +- .../PlexRequests.Updater.csproj | 51 +++++++++++++++++++ PlexRequests.Updater/Program.cs | 13 +++++ .../Properties/AssemblyInfo.cs | 27 ++++++++++ PlexRequests.Updater/Updater.cs | 42 +++++++++++++++ PlexRequests.sln | 50 ++++++++++-------- 8 files changed, 165 insertions(+), 24 deletions(-) create mode 100644 PlexRequests.Updater/PlexRequests.Updater.csproj create mode 100644 PlexRequests.Updater/Program.cs create mode 100644 PlexRequests.Updater/Properties/AssemblyInfo.cs create mode 100644 PlexRequests.Updater/Updater.cs diff --git a/PlexRequests.Core.Tests/PlexRequests.Core.Tests.csproj b/PlexRequests.Core.Tests/PlexRequests.Core.Tests.csproj index 31f058854..b0d9a25ef 100644 --- a/PlexRequests.Core.Tests/PlexRequests.Core.Tests.csproj +++ b/PlexRequests.Core.Tests/PlexRequests.Core.Tests.csproj @@ -1,5 +1,5 @@  - + Debug AnyCPU diff --git a/PlexRequests.Services.Tests/PlexRequests.Services.Tests.csproj b/PlexRequests.Services.Tests/PlexRequests.Services.Tests.csproj index efa0af5e2..b6a3c6dc0 100644 --- a/PlexRequests.Services.Tests/PlexRequests.Services.Tests.csproj +++ b/PlexRequests.Services.Tests/PlexRequests.Services.Tests.csproj @@ -1,5 +1,5 @@  - + Debug AnyCPU diff --git a/PlexRequests.UI.Tests/PlexRequests.UI.Tests.csproj b/PlexRequests.UI.Tests/PlexRequests.UI.Tests.csproj index 12950fc2c..21deb5eff 100644 --- a/PlexRequests.UI.Tests/PlexRequests.UI.Tests.csproj +++ b/PlexRequests.UI.Tests/PlexRequests.UI.Tests.csproj @@ -1,5 +1,5 @@  - + Debug AnyCPU diff --git a/PlexRequests.Updater/PlexRequests.Updater.csproj b/PlexRequests.Updater/PlexRequests.Updater.csproj new file mode 100644 index 000000000..c7a2ef5a5 --- /dev/null +++ b/PlexRequests.Updater/PlexRequests.Updater.csproj @@ -0,0 +1,51 @@ + + + + Debug + AnyCPU + {EBE6FC1C-7B4B-47E9-AF54-0EE0604A2BE5} + Exe + PlexRequests.Updater + PlexRequests.Updater + v4.5 + + + true + full + false + bin\Debug + DEBUG; + prompt + 4 + true + + + full + true + bin\Release + prompt + 4 + true + + + + + + + + + + + + + + + {DD7DC444-D3BF-4027-8AB9-EFC71F5EC581} + PlexRequests.Core + + + {1252336D-42A3-482A-804C-836E60173DFA} + PlexRequests.Helpers + + + \ No newline at end of file diff --git a/PlexRequests.Updater/Program.cs b/PlexRequests.Updater/Program.cs new file mode 100644 index 000000000..5703d1f84 --- /dev/null +++ b/PlexRequests.Updater/Program.cs @@ -0,0 +1,13 @@ +using System; + +namespace PlexRequests.Updater +{ + class MainClass + { + public static void Main (string[] args) + { + Console.WriteLine ("Starting PlexRequests .Net updater"); + + } + } +} diff --git a/PlexRequests.Updater/Properties/AssemblyInfo.cs b/PlexRequests.Updater/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..c1c129870 --- /dev/null +++ b/PlexRequests.Updater/Properties/AssemblyInfo.cs @@ -0,0 +1,27 @@ +using System.Reflection; +using System.Runtime.CompilerServices; + +// Information about this assembly is defined by the following attributes. +// Change them to the values specific to your project. + +[assembly: AssemblyTitle ("PlexRequests.Updater")] +[assembly: AssemblyDescription ("")] +[assembly: AssemblyConfiguration ("")] +[assembly: AssemblyCompany ("")] +[assembly: AssemblyProduct ("")] +[assembly: AssemblyCopyright ("TidusJar")] +[assembly: AssemblyTrademark ("")] +[assembly: AssemblyCulture ("")] + +// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". +// The form "{Major}.{Minor}.*" will automatically update the build and revision, +// and "{Major}.{Minor}.{Build}.*" will update just the revision. + +[assembly: AssemblyVersion ("1.0.*")] + +// The following attributes are used to specify the signing key for the assembly, +// if desired. See the Mono documentation for more information about signing. + +//[assembly: AssemblyDelaySign(false)] +//[assembly: AssemblyKeyFile("")] + diff --git a/PlexRequests.Updater/Updater.cs b/PlexRequests.Updater/Updater.cs new file mode 100644 index 000000000..d1b72b59c --- /dev/null +++ b/PlexRequests.Updater/Updater.cs @@ -0,0 +1,42 @@ +using System; +using PlexRequests.Core; +using System.Net; +using System.IO; +using System.IO.Compression; + +namespace PlexRequests.Updater +{ + public class Updater + { + public void Start(){ + var c = new StatusChecker (); + + try { + + var release = c.GetStatus (); + if(!release.UpdateAvailable) + { + Console.WriteLine ("No Update availble, shutting down"); + } + + using(var client = new WebClient()) + using(var ms = new MemoryStream(client.DownloadData(release.DownloadUri), false)) + using(var gz = new GZipStream(ms, CompressionLevel.Optimal)) + { + // TODO decompress stream + } + + + } catch (Exception ex) { + + Console.WriteLine (ex.Message); + Console.WriteLine ("Oops... Looks like we cannot update!"); + Console.ReadLine (); + } + } + + + + } +} + diff --git a/PlexRequests.sln b/PlexRequests.sln index e3feb5b1d..77eb7e978 100644 --- a/PlexRequests.sln +++ b/PlexRequests.sln @@ -1,6 +1,6 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 14 +# Visual Studio 2012 VisualStudioVersion = 14.0.25123.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PlexRequests.UI", "PlexRequests.UI\PlexRequests.UI.csproj", "{68F5F5F3-B8BB-4911-875F-6F00AAE04EA6}" @@ -33,12 +33,22 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PlexRequests.Api.Models", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PlexRequests.Services.Tests", "PlexRequests.Services.Tests\PlexRequests.Services.Tests.csproj", "{EAADB4AC-064F-4D3A-AFF9-64A33131A9A7}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PlexRequests.Updater", "PlexRequests.Updater\PlexRequests.Updater.csproj", "{EBE6FC1C-7B4B-47E9-AF54-0EE0604A2BE5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1252336D-42A3-482A-804C-836E60173DFA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1252336D-42A3-482A-804C-836E60173DFA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1252336D-42A3-482A-804C-836E60173DFA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1252336D-42A3-482A-804C-836E60173DFA}.Release|Any CPU.Build.0 = Release|Any CPU + {566EFA49-68F8-4716-9693-A6B3F2624DEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {566EFA49-68F8-4716-9693-A6B3F2624DEA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {566EFA49-68F8-4716-9693-A6B3F2624DEA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {566EFA49-68F8-4716-9693-A6B3F2624DEA}.Release|Any CPU.Build.0 = Release|Any CPU {68F5F5F3-B8BB-4911-875F-6F00AAE04EA6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {68F5F5F3-B8BB-4911-875F-6F00AAE04EA6}.Debug|Any CPU.Build.0 = Debug|Any CPU {68F5F5F3-B8BB-4911-875F-6F00AAE04EA6}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -47,42 +57,40 @@ Global {8CB8D235-2674-442D-9C6A-35FCAEEB160D}.Debug|Any CPU.Build.0 = Debug|Any CPU {8CB8D235-2674-442D-9C6A-35FCAEEB160D}.Release|Any CPU.ActiveCfg = Release|Any CPU {8CB8D235-2674-442D-9C6A-35FCAEEB160D}.Release|Any CPU.Build.0 = Release|Any CPU - {95834072-A675-415D-AA8F-877C91623810}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {95834072-A675-415D-AA8F-877C91623810}.Debug|Any CPU.Build.0 = Debug|Any CPU - {95834072-A675-415D-AA8F-877C91623810}.Release|Any CPU.ActiveCfg = Release|Any CPU - {95834072-A675-415D-AA8F-877C91623810}.Release|Any CPU.Build.0 = Release|Any CPU - {DD7DC444-D3BF-4027-8AB9-EFC71F5EC581}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DD7DC444-D3BF-4027-8AB9-EFC71F5EC581}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DD7DC444-D3BF-4027-8AB9-EFC71F5EC581}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DD7DC444-D3BF-4027-8AB9-EFC71F5EC581}.Release|Any CPU.Build.0 = Release|Any CPU {92433867-2B7B-477B-A566-96C382427525}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {92433867-2B7B-477B-A566-96C382427525}.Debug|Any CPU.Build.0 = Debug|Any CPU {92433867-2B7B-477B-A566-96C382427525}.Release|Any CPU.ActiveCfg = Release|Any CPU {92433867-2B7B-477B-A566-96C382427525}.Release|Any CPU.Build.0 = Release|Any CPU - {1252336D-42A3-482A-804C-836E60173DFA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1252336D-42A3-482A-804C-836E60173DFA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1252336D-42A3-482A-804C-836E60173DFA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1252336D-42A3-482A-804C-836E60173DFA}.Release|Any CPU.Build.0 = Release|Any CPU + {95834072-A675-415D-AA8F-877C91623810}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {95834072-A675-415D-AA8F-877C91623810}.Debug|Any CPU.Build.0 = Debug|Any CPU + {95834072-A675-415D-AA8F-877C91623810}.Release|Any CPU.ActiveCfg = Release|Any CPU + {95834072-A675-415D-AA8F-877C91623810}.Release|Any CPU.Build.0 = Release|Any CPU {A930E2CF-79E2-45F9-B06A-9A719A254CE4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A930E2CF-79E2-45F9-B06A-9A719A254CE4}.Debug|Any CPU.Build.0 = Debug|Any CPU {A930E2CF-79E2-45F9-B06A-9A719A254CE4}.Release|Any CPU.ActiveCfg = Release|Any CPU {A930E2CF-79E2-45F9-B06A-9A719A254CE4}.Release|Any CPU.Build.0 = Release|Any CPU - {FCFECD5D-47F6-454D-8692-E27A921BE655}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FCFECD5D-47F6-454D-8692-E27A921BE655}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FCFECD5D-47F6-454D-8692-E27A921BE655}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FCFECD5D-47F6-454D-8692-E27A921BE655}.Release|Any CPU.Build.0 = Release|Any CPU - {566EFA49-68F8-4716-9693-A6B3F2624DEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {566EFA49-68F8-4716-9693-A6B3F2624DEA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {566EFA49-68F8-4716-9693-A6B3F2624DEA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {566EFA49-68F8-4716-9693-A6B3F2624DEA}.Release|Any CPU.Build.0 = Release|Any CPU {CB37A5F8-6DFC-4554-99D3-A42B502E4591}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CB37A5F8-6DFC-4554-99D3-A42B502E4591}.Debug|Any CPU.Build.0 = Debug|Any CPU {CB37A5F8-6DFC-4554-99D3-A42B502E4591}.Release|Any CPU.ActiveCfg = Release|Any CPU {CB37A5F8-6DFC-4554-99D3-A42B502E4591}.Release|Any CPU.Build.0 = Release|Any CPU + {DD7DC444-D3BF-4027-8AB9-EFC71F5EC581}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DD7DC444-D3BF-4027-8AB9-EFC71F5EC581}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DD7DC444-D3BF-4027-8AB9-EFC71F5EC581}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DD7DC444-D3BF-4027-8AB9-EFC71F5EC581}.Release|Any CPU.Build.0 = Release|Any CPU {EAADB4AC-064F-4D3A-AFF9-64A33131A9A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EAADB4AC-064F-4D3A-AFF9-64A33131A9A7}.Debug|Any CPU.Build.0 = Debug|Any CPU {EAADB4AC-064F-4D3A-AFF9-64A33131A9A7}.Release|Any CPU.ActiveCfg = Release|Any CPU {EAADB4AC-064F-4D3A-AFF9-64A33131A9A7}.Release|Any CPU.Build.0 = Release|Any CPU + {EBE6FC1C-7B4B-47E9-AF54-0EE0604A2BE5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EBE6FC1C-7B4B-47E9-AF54-0EE0604A2BE5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EBE6FC1C-7B4B-47E9-AF54-0EE0604A2BE5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EBE6FC1C-7B4B-47E9-AF54-0EE0604A2BE5}.Release|Any CPU.Build.0 = Release|Any CPU + {FCFECD5D-47F6-454D-8692-E27A921BE655}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FCFECD5D-47F6-454D-8692-E27A921BE655}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FCFECD5D-47F6-454D-8692-E27A921BE655}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FCFECD5D-47F6-454D-8692-E27A921BE655}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From d44612217a589298e6269830d73db19619d8e5ba Mon Sep 17 00:00:00 2001 From: TidusJar Date: Sun, 15 May 2016 22:30:47 -0400 Subject: [PATCH 42/76] - Notifications will no longer be send to the admins if they request something. - Looks like we missed out adding the notifications to Music requests, so I added that in --- PlexRequests.UI/Modules/SearchModule.cs | 88 +++++++++++++++++++------ 1 file changed, 67 insertions(+), 21 deletions(-) diff --git a/PlexRequests.UI/Modules/SearchModule.cs b/PlexRequests.UI/Modules/SearchModule.cs index 92bcb1f38..39b58dd59 100644 --- a/PlexRequests.UI/Modules/SearchModule.cs +++ b/PlexRequests.UI/Modules/SearchModule.cs @@ -499,15 +499,16 @@ namespace PlexRequests.UI.Modules Log.Info("Adding movie to database (No approval required)"); RequestService.AddRequest(model); - var notificationModel = new NotificationModel - { - Title = model.Title, - User = Username, - DateTime = DateTime.Now, - NotificationType = NotificationType.NewRequest - }; - NotificationService.Publish(notificationModel); + if (ShouldSendNotification) { + var notificationModel = new NotificationModel { + Title = model.Title, + User = Username, + DateTime = DateTime.Now, + NotificationType = NotificationType.NewRequest + }; + NotificationService.Publish (notificationModel); + } return Response.AsJson(new JsonResponseModel { Result = true, Message = $"{fullMovieName} was successfully added!" }); } return @@ -524,14 +525,15 @@ namespace PlexRequests.UI.Modules Log.Info("Adding movie to database (No approval required)"); RequestService.AddRequest(model); - var notificationModel = new NotificationModel - { - Title = model.Title, - User = Username, - DateTime = DateTime.Now, - NotificationType = NotificationType.NewRequest - }; - NotificationService.Publish(notificationModel); + if (ShouldSendNotification) { + var notificationModel = new NotificationModel { + Title = model.Title, + User = Username, + DateTime = DateTime.Now, + NotificationType = NotificationType.NewRequest + }; + NotificationService.Publish (notificationModel); + } return Response.AsJson(new JsonResponseModel { Result = true, Message = $"{fullMovieName} was successfully added!" }); } @@ -657,9 +659,16 @@ namespace PlexRequests.UI.Modules model.Approved = true; Log.Debug("Adding tv to database requests (No approval required & Sonarr)"); RequestService.AddRequest(model); - var notify1 = new NotificationModel { Title = model.Title, User = Username, DateTime = DateTime.Now, NotificationType = NotificationType.NewRequest }; - NotificationService.Publish(notify1); + if (ShouldSendNotification) { + var notify1 = new NotificationModel { + Title = model.Title, + User = Username, + DateTime = DateTime.Now, + NotificationType = NotificationType.NewRequest + }; + NotificationService.Publish (notify1); + } return Response.AsJson(new JsonResponseModel { Result = true, Message = $"{fullShowName} was successfully added!" }); } @@ -677,9 +686,15 @@ namespace PlexRequests.UI.Modules model.Approved = true; Log.Debug("Adding tv to database requests (No approval required & SickRage)"); RequestService.AddRequest(model); - - var notify2 = new NotificationModel { Title = model.Title, User = Username, DateTime = DateTime.Now, NotificationType = NotificationType.NewRequest }; - NotificationService.Publish(notify2); + if (ShouldSendNotification) { + var notify2 = new NotificationModel { + Title = model.Title, + User = Username, + DateTime = DateTime.Now, + NotificationType = NotificationType.NewRequest + }; + NotificationService.Publish (notify2); + } return Response.AsJson(new JsonResponseModel { Result = true, Message = $"{fullShowName} was successfully added!" }); } @@ -698,6 +713,18 @@ namespace PlexRequests.UI.Modules return Response.AsJson(new JsonResponseModel { Result = true, Message = $"{fullShowName} was successfully added!" }); } + private bool ShouldSendNotification(){ + var sendNotification = true; + var claims = Context.CurrentUser?.Claims; + if (claims != null) { + if (claims.Contains (UserClaims.Admin) || claims.Contains (UserClaims.PowerUser)) { + sendNotification = false; // Don't bother sending a notification if the user is an admin + } + } + return sendNotification; + } + + private Response RequestAlbum(string releaseId, bool notify) { var settings = PrService.GetSettings(); @@ -798,6 +825,16 @@ namespace PlexRequests.UI.Modules model.Approved = true; RequestService.AddRequest(model); + if (ShouldSendNotification ()) { + var notify2 = new NotificationModel { + Title = model.Title, + User = Username, + DateTime = DateTime.Now, + NotificationType = NotificationType.NewRequest + }; + NotificationService.Publish (notify2); + } + return Response.AsJson(new JsonResponseModel { @@ -806,6 +843,15 @@ namespace PlexRequests.UI.Modules }); } + if (ShouldSendNotification ()) { + var notify2 = new NotificationModel { + Title = model.Title, + User = Username, + DateTime = DateTime.Now, + NotificationType = NotificationType.NewRequest + }; + NotificationService.Publish (notify2); + } var result = RequestService.AddRequest(model); return Response.AsJson(new JsonResponseModel { From 15fae2639724f90bbc5f6f867dd6619fb7579e03 Mon Sep 17 00:00:00 2001 From: TidusJar Date: Sun, 15 May 2016 23:01:17 -0400 Subject: [PATCH 43/76] More work on the user management --- PlexRequests.UI/Helpers/BaseUrlHelper.cs | 2 +- .../Models/UserManagementUsersViewModel.cs | 20 +++++++++++ PlexRequests.UI/Modules/ApprovalModule.cs | 16 ++------- PlexRequests.UI/Modules/SearchModule.cs | 8 ++--- .../Modules/UserManagementModule.cs | 11 ++++++ PlexRequests.UI/PlexRequests.UI.csproj | 1 + PlexRequests.UI/Views/Admin/Logs.cshtml | 2 +- .../Views/UserManagement/Index.cshtml | 36 ++++++++++++++++++- 8 files changed, 75 insertions(+), 21 deletions(-) create mode 100644 PlexRequests.UI/Models/UserManagementUsersViewModel.cs diff --git a/PlexRequests.UI/Helpers/BaseUrlHelper.cs b/PlexRequests.UI/Helpers/BaseUrlHelper.cs index c9a3e30f2..4071f00d8 100644 --- a/PlexRequests.UI/Helpers/BaseUrlHelper.cs +++ b/PlexRequests.UI/Helpers/BaseUrlHelper.cs @@ -95,7 +95,7 @@ namespace PlexRequests.UI.Helpers return helper.Raw(sb.ToString()); } - public static IHtmlString LoadLogsAssets(this HtmlHelpers helper) + public static IHtmlString LoadTableAssets(this HtmlHelpers helper) { var sb = new StringBuilder(); var assetLocation = GetBaseUrl(); diff --git a/PlexRequests.UI/Models/UserManagementUsersViewModel.cs b/PlexRequests.UI/Models/UserManagementUsersViewModel.cs new file mode 100644 index 000000000..66bc9974e --- /dev/null +++ b/PlexRequests.UI/Models/UserManagementUsersViewModel.cs @@ -0,0 +1,20 @@ +using System; + +namespace PlexRequests.UI +{ + public class UserManagementUsersViewModel + { + public string Username{get;set;} + public string Claims{get;set;} + public int Id {get;set;} + public string Alias {get;set;} + public UserType Type { get; set;} + } + + public enum UserType + { + PlexUser, + LocalUser + } +} + diff --git a/PlexRequests.UI/Modules/ApprovalModule.cs b/PlexRequests.UI/Modules/ApprovalModule.cs index e6cb97c06..d74431eb8 100644 --- a/PlexRequests.UI/Modules/ApprovalModule.cs +++ b/PlexRequests.UI/Modules/ApprovalModule.cs @@ -50,7 +50,7 @@ namespace PlexRequests.UI.Modules ISettingsService sonarrSettings, ISickRageApi srApi, ISettingsService srSettings, ISettingsService hpSettings, IHeadphonesApi hpApi) : base("approval") { - this.RequiresAuthentication(); + this.RequiresClaims(UserClaims.Admin, UserClaims.PowerUser); Service = service; CpService = cpService; @@ -88,10 +88,7 @@ namespace PlexRequests.UI.Modules private Response Approve(int requestId, string qualityId) { Log.Info("approving request {0}", requestId); - if (!Context.CurrentUser.IsAuthenticated()) - { - return Response.AsJson(new JsonResponseModel { Result = false, Message = "You are not an Admin, so you cannot approve any requests." }); - } + // Get the request from the DB var request = Service.Get(requestId); @@ -258,10 +255,6 @@ namespace PlexRequests.UI.Modules private Response ApproveAllMovies() { - if (!Context.CurrentUser.IsAuthenticated()) - { - return Response.AsJson(new JsonResponseModel { Result = false, Message = "You are not an Admin, so you cannot approve any requests." }); - } var requests = Service.GetAll().Where(x => x.CanApprove && x.Type == RequestType.Movie); var requestedModels = requests as RequestedModel[] ?? requests.ToArray(); @@ -312,11 +305,6 @@ namespace PlexRequests.UI.Modules /// private Response ApproveAll() { - if (!Context.CurrentUser.IsAuthenticated()) - { - return Response.AsJson(new JsonResponseModel { Result = false, Message = "You are not an Admin, so you cannot approve any requests." }); - } - var requests = Service.GetAll().Where(x => x.CanApprove); var requestedModels = requests as RequestedModel[] ?? requests.ToArray(); if (!requestedModels.Any()) diff --git a/PlexRequests.UI/Modules/SearchModule.cs b/PlexRequests.UI/Modules/SearchModule.cs index 39b58dd59..4ef08002b 100644 --- a/PlexRequests.UI/Modules/SearchModule.cs +++ b/PlexRequests.UI/Modules/SearchModule.cs @@ -500,7 +500,7 @@ namespace PlexRequests.UI.Modules RequestService.AddRequest(model); - if (ShouldSendNotification) { + if (ShouldSendNotification()) { var notificationModel = new NotificationModel { Title = model.Title, User = Username, @@ -525,7 +525,7 @@ namespace PlexRequests.UI.Modules Log.Info("Adding movie to database (No approval required)"); RequestService.AddRequest(model); - if (ShouldSendNotification) { + if (ShouldSendNotification()) { var notificationModel = new NotificationModel { Title = model.Title, User = Username, @@ -660,7 +660,7 @@ namespace PlexRequests.UI.Modules Log.Debug("Adding tv to database requests (No approval required & Sonarr)"); RequestService.AddRequest(model); - if (ShouldSendNotification) { + if (ShouldSendNotification()) { var notify1 = new NotificationModel { Title = model.Title, User = Username, @@ -686,7 +686,7 @@ namespace PlexRequests.UI.Modules model.Approved = true; Log.Debug("Adding tv to database requests (No approval required & SickRage)"); RequestService.AddRequest(model); - if (ShouldSendNotification) { + if (ShouldSendNotification()) { var notify2 = new NotificationModel { Title = model.Title, User = Username, diff --git a/PlexRequests.UI/Modules/UserManagementModule.cs b/PlexRequests.UI/Modules/UserManagementModule.cs index 0e7bd1c02..eeed0db69 100644 --- a/PlexRequests.UI/Modules/UserManagementModule.cs +++ b/PlexRequests.UI/Modules/UserManagementModule.cs @@ -10,6 +10,7 @@ using PlexRequests.Core; using PlexRequests.UI.Models; using PlexRequests.UI.Modules; using PlexRequests.Helpers; +using System.Collections.Generic; namespace PlexRequests.UI @@ -32,6 +33,16 @@ namespace PlexRequests.UI public Response LoadUsers() { var users = UserMapper.GetUsers (); + var model = new List(); + foreach (var user in users) { + model.Add (new UserManagementUsersViewModel { + //Claims = ByteConverterHelper.ReturnObject(user.Claims), + Claims = "test", + Id = user.Id, + Username = user.UserName, + //Type = UserType.LocalUser + }); + } return Response.AsJson (users); } } diff --git a/PlexRequests.UI/PlexRequests.UI.csproj b/PlexRequests.UI/PlexRequests.UI.csproj index f0683968d..d78195670 100644 --- a/PlexRequests.UI/PlexRequests.UI.csproj +++ b/PlexRequests.UI/PlexRequests.UI.csproj @@ -201,6 +201,7 @@ +
    diff --git a/PlexRequests.UI/Views/Admin/Logs.cshtml b/PlexRequests.UI/Views/Admin/Logs.cshtml index ed32d6552..9fc58d202 100644 --- a/PlexRequests.UI/Views/Admin/Logs.cshtml +++ b/PlexRequests.UI/Views/Admin/Logs.cshtml @@ -1,6 +1,6 @@ @using PlexRequests.UI.Helpers @Html.Partial("_Sidebar") -@Html.LoadLogsAssets() +@Html.LoadTableAssets() @{ var baseUrl = Html.GetBaseUrl(); diff --git a/PlexRequests.UI/Views/UserManagement/Index.cshtml b/PlexRequests.UI/Views/UserManagement/Index.cshtml index f2e3b9d54..3df2a104d 100644 --- a/PlexRequests.UI/Views/UserManagement/Index.cshtml +++ b/PlexRequests.UI/Views/UserManagement/Index.cshtml @@ -1,4 +1,5 @@ @using PlexRequests.UI.Helpers +@Html.LoadTableAssets() @{ var baseUrl = Html.GetBaseUrl().ToHtmlString(); var url = string.Empty; @@ -11,4 +12,37 @@

    User Management

    - \ No newline at end of file + +
    +
    +
    + + + + + + + + + +
    IdUsernamePermissions
    +
    + + \ No newline at end of file From 809b2bf0a87702952ee18172d68f436799f1c7b6 Mon Sep 17 00:00:00 2001 From: TidusJar Date: Sun, 15 May 2016 23:39:40 -0400 Subject: [PATCH 44/76] Made the store backup clean up some of the older backups (> 7 days). --- PlexRequests.Services/Jobs/StoreBackup.cs | 57 ++++++++++++++++++++++- PlexRequests.Store/DbConfiguration.cs | 3 ++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/PlexRequests.Services/Jobs/StoreBackup.cs b/PlexRequests.Services/Jobs/StoreBackup.cs index 64a1fbc57..f64a7361e 100644 --- a/PlexRequests.Services/Jobs/StoreBackup.cs +++ b/PlexRequests.Services/Jobs/StoreBackup.cs @@ -27,6 +27,7 @@ using System; using System.IO; using System.Linq; +using System.Globalization; using NLog; @@ -37,7 +38,6 @@ using PlexRequests.Store.Repository; using Quartz; -using Directory = System.IO.Directory; namespace PlexRequests.Services.Jobs { @@ -57,6 +57,7 @@ namespace PlexRequests.Services.Jobs public void Execute(IJobExecutionContext context) { TakeBackup(); + Cleanup (); } private void TakeBackup() @@ -80,8 +81,11 @@ namespace PlexRequests.Services.Jobs try { + if(DoWeNeedToBackUp(backupDir.FullName)) + { File.Copy(dbPath, Path.Combine(backupDir.FullName, $"PlexRequests.sqlite_{DateTime.Now.ToString("yyyy-MM-dd hh.mm.ss")}.bak")); - } + } + } catch (Exception e) { Log.Warn(e); @@ -93,5 +97,54 @@ namespace PlexRequests.Services.Jobs } } + + private void Cleanup() + { + Log.Trace("Starting DB Cleanup"); + var dbPath = Sql.CurrentPath; + var dir = Path.GetDirectoryName(dbPath); + if (dir == null) + { + Log.Warn("We couldn't find the DB path. We cannot backup."); + return; + } + var backupDir = Directory.CreateDirectory(Path.Combine(dir, "Backup")); + + var files = backupDir.GetFiles(); + + foreach (var file in files) { + var dt = ParseName(file.Name); + if(dt < DateTime.Now.AddDays(-7)){ + try { + + File.Delete(file.FullName); + } catch (Exception ex) { + Log.Error(ex); + } + } + } + + } + + private bool DoWeNeedToBackup(string backupPath) + { + var files = Directory.GetFiles(backupPath); + //TODO Get the latest file and if it's within an hour of DateTime.Now then don't bother backing up. + return true; + } + + private DateTime ParseName(string fileName) + { + var names = fileName.Split(new []{'_','.',' '}, StringSplitOptions.RemoveEmptyEntries); + if(names.Count() > 1) + { + DateTime parsed; + //DateTime.TryParseExcat(names[1], "yyyy-MM-dd hh.mm.ss",CultureInfo.CurrentUICulture, DateTimeStyles.None, out parsed); + DateTime.TryParse(names[2], out parsed); + return parsed; + + } + return DateTime.MinValue; + } } } \ No newline at end of file diff --git a/PlexRequests.Store/DbConfiguration.cs b/PlexRequests.Store/DbConfiguration.cs index 27e7f20f9..eb8458d02 100644 --- a/PlexRequests.Store/DbConfiguration.cs +++ b/PlexRequests.Store/DbConfiguration.cs @@ -23,6 +23,9 @@ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // *********************************************************************** +using System.Globalization; + + #endregion using System; using System.Data; From 8a14427cea4e0f17e28d8d654e0a017eda18c282 Mon Sep 17 00:00:00 2001 From: TidusJar Date: Sun, 15 May 2016 23:55:55 -0400 Subject: [PATCH 45/76] A bit more work on switching to using user claims so we can support multiple users --- PlexRequests.UI/Modules/BaseAuthModule.cs | 10 ++++++- PlexRequests.UI/Modules/RequestsModule.cs | 35 +++++++++-------------- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/PlexRequests.UI/Modules/BaseAuthModule.cs b/PlexRequests.UI/Modules/BaseAuthModule.cs index 1cb5fe225..45e55fc81 100644 --- a/PlexRequests.UI/Modules/BaseAuthModule.cs +++ b/PlexRequests.UI/Modules/BaseAuthModule.cs @@ -23,6 +23,9 @@ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // ************************************************************************/ +using System.Linq; + + #endregion using Nancy; @@ -55,7 +58,12 @@ namespace PlexRequests.UI.Modules } } - protected bool IsAdmin => Context.CurrentUser.IsAuthenticated(); + protected bool IsAdmin { get { + var claims = Context.CurrentUser.Claims.ToList(); + if(claims.Contains(UserClaims.Admin) || claims.Contains(UserClaims.PowerUser)){ + return true;} + return false; + } } protected int DateTimeOffset { diff --git a/PlexRequests.UI/Modules/RequestsModule.cs b/PlexRequests.UI/Modules/RequestsModule.cs index bb3bd7b49..c01d6afd0 100644 --- a/PlexRequests.UI/Modules/RequestsModule.cs +++ b/PlexRequests.UI/Modules/RequestsModule.cs @@ -108,7 +108,6 @@ namespace PlexRequests.UI.Modules private Response GetMovies() // TODO: async await the API calls { var settings = PrSettings.GetSettings(); - var isAdmin = Context.CurrentUser.IsAuthenticated(); List taskList = new List(); @@ -130,7 +129,7 @@ namespace PlexRequests.UI.Modules List qualities = new List(); - if (isAdmin) + if (IsAdmin) { var cpSettings = CpSettings.GetSettings(); if (cpSettings.Enabled) @@ -185,7 +184,6 @@ namespace PlexRequests.UI.Modules private Response GetTvShows() // TODO: async await the API calls { var settings = PrSettings.GetSettings(); - var isAdmin = Context.CurrentUser.IsAuthenticated(); List taskList = new List(); @@ -198,14 +196,14 @@ namespace PlexRequests.UI.Modules { dbTv = t.Result.ToList(); - if (settings.UsersCanViewOnlyOwnRequests && !isAdmin) + if (settings.UsersCanViewOnlyOwnRequests && !IsAdmin) { dbTv = dbTv.Where(x => x.UserHasRequested(Username)).ToList(); } })); List qualities = new List(); - if (isAdmin) + if (IsAdmin) { var sonarrSettings = SonarrSettings.GetSettings(); if (sonarrSettings.Enabled) @@ -251,10 +249,10 @@ namespace PlexRequests.UI.Modules Approved = tv.Available || tv.Approved, Title = tv.Title, Overview = tv.Overview, - RequestedUsers = isAdmin ? tv.AllUsers.ToArray() : new string[] { }, + RequestedUsers = IsAdmin ? tv.AllUsers.ToArray() : new string[] { }, ReleaseYear = tv.ReleaseDate.Year.ToString(), Available = tv.Available, - Admin = isAdmin, + Admin = IsAdmin, Issues = tv.Issues.ToString().CamelCaseToWords(), OtherMessage = tv.OtherMessage, AdminNotes = tv.AdminNote, @@ -269,7 +267,6 @@ namespace PlexRequests.UI.Modules private Response GetAlbumRequests() { var settings = PrSettings.GetSettings(); - var isAdmin = Context.CurrentUser.IsAuthenticated(); var dbAlbum = Service.GetAll().Where(x => x.Type == RequestType.Album); if (settings.UsersCanViewOnlyOwnRequests && !isAdmin) { @@ -294,10 +291,10 @@ namespace PlexRequests.UI.Modules Approved = album.Available || album.Approved, Title = album.Title, Overview = album.Overview, - RequestedUsers = isAdmin ? album.AllUsers.ToArray() : new string[] { }, + RequestedUsers = IsAdmin ? album.AllUsers.ToArray() : new string[] { }, ReleaseYear = album.ReleaseDate.Year.ToString(), Available = album.Available, - Admin = isAdmin, + Admin = IsAdmin, Issues = album.Issues.ToString().CamelCaseToWords(), OtherMessage = album.OtherMessage, AdminNotes = album.AdminNote, @@ -312,11 +309,8 @@ namespace PlexRequests.UI.Modules } private Response DeleteRequest(int requestid) - { - if (!Context.CurrentUser.IsAuthenticated()) - { - return Response.AsJson(new JsonResponseModel { Result = false, Message = "You are not an Admin, so you cannot delete any requests." }); - } + { + this.RequiresClaims (UserClaims.PowerUser, UserClaims.Admin); var currentEntity = Service.Get(requestid); Service.DeleteRequest(currentEntity); @@ -363,10 +357,7 @@ namespace PlexRequests.UI.Modules private Response ClearIssue(int requestId) { - if (!Context.CurrentUser.IsAuthenticated()) - { - return Response.AsJson(new JsonResponseModel { Result = false, Message = "You are not an Admin, so you cannot clear any issues." }); - } + this.RequiresClaims (UserClaims.PowerUser, UserClaims.Admin); var originalRequest = Service.Get(requestId); if (originalRequest == null) @@ -383,7 +374,8 @@ namespace PlexRequests.UI.Modules } private Response ChangeRequestAvailability(int requestId, bool available) - { + { + this.RequiresClaims (UserClaims.PowerUser, UserClaims.Admin); var originalRequest = Service.Get(requestId); if (originalRequest == null) { @@ -399,7 +391,8 @@ namespace PlexRequests.UI.Modules } private Response AddNote(int requestId, string noteArea) - { + { + this.RequiresClaims (UserClaims.PowerUser, UserClaims.Admin); var originalRequest = Service.Get(requestId); if (originalRequest == null) { From db6f28f9804ed8e97fa23139585ba82a2f319e2e Mon Sep 17 00:00:00 2001 From: TidusJar Date: Sun, 15 May 2016 23:56:19 -0400 Subject: [PATCH 46/76] missed some files --- PlexRequests.UI/Modules/ApprovalModule.cs | 5 ----- .../Modules/UserManagementModule.cs | 21 ++++++++++++++++--- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/PlexRequests.UI/Modules/ApprovalModule.cs b/PlexRequests.UI/Modules/ApprovalModule.cs index d74431eb8..d57f31a1f 100644 --- a/PlexRequests.UI/Modules/ApprovalModule.cs +++ b/PlexRequests.UI/Modules/ApprovalModule.cs @@ -276,11 +276,6 @@ namespace PlexRequests.UI.Modules private Response ApproveAllTVShows() { - if (!Context.CurrentUser.IsAuthenticated()) - { - return Response.AsJson(new JsonResponseModel { Result = false, Message = "You are not an Admin, so you cannot approve any requests." }); - } - var requests = Service.GetAll().Where(x => x.CanApprove && x.Type == RequestType.TvShow); var requestedModels = requests as RequestedModel[] ?? requests.ToArray(); if (!requestedModels.Any()) diff --git a/PlexRequests.UI/Modules/UserManagementModule.cs b/PlexRequests.UI/Modules/UserManagementModule.cs index eeed0db69..d0eff824d 100644 --- a/PlexRequests.UI/Modules/UserManagementModule.cs +++ b/PlexRequests.UI/Modules/UserManagementModule.cs @@ -25,12 +25,12 @@ namespace PlexRequests.UI Get ["/users"] = x => LoadUsers (); } - public Negotiator Load() + private Negotiator Load() { return View ["Index"]; } - public Response LoadUsers() + private Response LoadUsers() { var users = UserMapper.GetUsers (); var model = new List(); @@ -45,6 +45,21 @@ namespace PlexRequests.UI } return Response.AsJson (users); } - } + + private Response CreateUser(string username, string password, string claims) + { + if (string.IsNullOrWhiteSpace (username) || string.IsNullOrWhiteSpace (password)) { + return Response.AsJson (new JsonResponseModel { + Result = true, + Message = "Please enter in a valid Username and Password" + }); + } + var user = UserMapper.CreateUser (username, password, new string[]{ claims }); + if(user.HasValue){ + return Response.AsJson(new JsonResponseModel{ Result = true}); + } + + return Response.AsJson(new JsonResponseModel{ Result = false, Message = "Could not save user"}); + } } From 8dcb8022abfa162d2e1857ca60aab38dbb687da2 Mon Sep 17 00:00:00 2001 From: TidusJar Date: Sun, 15 May 2016 23:56:52 -0400 Subject: [PATCH 47/76] and some more... --- PlexRequests.UI/Views/Requests/Index.cshtml | 2 +- PlexRequests.UI/Views/Shared/_Layout.cshtml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/PlexRequests.UI/Views/Requests/Index.cshtml b/PlexRequests.UI/Views/Requests/Index.cshtml index 27565156e..c8061e369 100644 --- a/PlexRequests.UI/Views/Requests/Index.cshtml +++ b/PlexRequests.UI/Views/Requests/Index.cshtml @@ -36,7 +36,7 @@
    - @if (Context.CurrentUser.IsAuthenticated()) + @if (Context.CurrentUser.IsAuthenticated()) //TODO replace with IsAdmin { @if (Model.SearchForMovies) { diff --git a/PlexRequests.UI/Views/Shared/_Layout.cshtml b/PlexRequests.UI/Views/Shared/_Layout.cshtml index dc666d79b..80f146912 100644 --- a/PlexRequests.UI/Views/Shared/_Layout.cshtml +++ b/PlexRequests.UI/Views/Shared/_Layout.cshtml @@ -43,7 +43,7 @@ @@ -70,14 +72,14 @@ -
    - @RenderBody() -
    -
    - +
    + @RenderBody() +
    +
    + -
    +
    + + + + + + + + + + + + + + + + + + + + + + + +
     
    +
    + + diff --git a/PlexRequests.UI/Views/ApiDocs/o2c.html b/PlexRequests.UI/Views/ApiDocs/o2c.html new file mode 100644 index 000000000..88e8bf114 --- /dev/null +++ b/PlexRequests.UI/Views/ApiDocs/o2c.html @@ -0,0 +1,20 @@ + \ No newline at end of file diff --git a/PlexRequests.UI/Views/Shared/_Layout.cshtml b/PlexRequests.UI/Views/Shared/_Layout.cshtml index 5ad52e036..7e071b494 100644 --- a/PlexRequests.UI/Views/Shared/_Layout.cshtml +++ b/PlexRequests.UI/Views/Shared/_Layout.cshtml @@ -56,7 +56,7 @@
  • Settings
  • Change password
  • -
  • Donate!
  • +
  • Donate!
  • Logout
  • diff --git a/PlexRequests.UI/packages.config b/PlexRequests.UI/packages.config index 79419632f..ddcf218f0 100644 --- a/PlexRequests.UI/packages.config +++ b/PlexRequests.UI/packages.config @@ -16,7 +16,10 @@ + + + @@ -26,6 +29,7 @@ + From 187a59261ab056dd969670f9a206e20dfa121bf4 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Thu, 19 May 2016 14:29:41 +0100 Subject: [PATCH 57/76] More work on the api and documentation #222 #205 --- PlexRequests.UI.Tests/ApiModuleTests.cs | 84 +++++++++++++++++++ .../PlexRequests.UI.Tests.csproj | 1 + .../RequestedModelDataProvider.cs | 4 + PlexRequests.UI/Modules/ApiMetadataModule.cs | 16 +++- PlexRequests.UI/Modules/ApiModule.cs | 54 ++++++++---- PlexRequests.UI/PlexRequests.UI.csproj | 1 + .../Validators/RequestedModelValidator.cs | 45 ++++++++++ 7 files changed, 186 insertions(+), 19 deletions(-) create mode 100644 PlexRequests.UI.Tests/ApiModuleTests.cs create mode 100644 PlexRequests.UI/Validators/RequestedModelValidator.cs diff --git a/PlexRequests.UI.Tests/ApiModuleTests.cs b/PlexRequests.UI.Tests/ApiModuleTests.cs new file mode 100644 index 000000000..7208b8bfb --- /dev/null +++ b/PlexRequests.UI.Tests/ApiModuleTests.cs @@ -0,0 +1,84 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: ApiModuleTests.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 Moq; + +using Nancy; +using Nancy.Testing; + +using NUnit.Framework; + +using PlexRequests.Core; +using PlexRequests.Core.SettingModels; +using PlexRequests.UI.Helpers; +using PlexRequests.UI.Modules; + +namespace PlexRequests.UI.Tests +{ + [TestFixture] + [Ignore("Locator :(")] + public class ApiModuleTests + { + private ConfigurableBootstrapper Bootstrapper { get; set; } + + [SetUp] + public void Setup() + { + var requestMock = new Mock(); + var settingsMock = new Mock>(); + Bootstrapper = new ConfigurableBootstrapper(with => + { + with.Module(); + with.Dependency(requestMock.Object); + with.Dependency(settingsMock.Object); + with.ApplicationStartup( + (c, a) => + { + var loc = ServiceLocator.Instance; + loc.SetContainer(c); + }); + }); + + } + + [Test] + public void GetAllRequests() + { + + var browser = new Browser(Bootstrapper); + + var result = browser.Post("/api/requests", with => + { + with.HttpRequest(); + with.Header("Accept", "application/json"); + with.Query("apikey","a"); + + }); + + Assert.That(HttpStatusCode.OK, Is.EqualTo(result.StatusCode)); + } + } +} \ No newline at end of file diff --git a/PlexRequests.UI.Tests/PlexRequests.UI.Tests.csproj b/PlexRequests.UI.Tests/PlexRequests.UI.Tests.csproj index 21deb5eff..3c7db1543 100644 --- a/PlexRequests.UI.Tests/PlexRequests.UI.Tests.csproj +++ b/PlexRequests.UI.Tests/PlexRequests.UI.Tests.csproj @@ -90,6 +90,7 @@ + diff --git a/PlexRequests.UI/ModelDataProviders/RequestedModelDataProvider.cs b/PlexRequests.UI/ModelDataProviders/RequestedModelDataProvider.cs index a838416c2..fe75957d8 100644 --- a/PlexRequests.UI/ModelDataProviders/RequestedModelDataProvider.cs +++ b/PlexRequests.UI/ModelDataProviders/RequestedModelDataProvider.cs @@ -34,6 +34,10 @@ namespace PlexRequests.UI.ModelDataProviders { public class RequestedModelDataProvider : ISwaggerModelDataProvider { + /// + /// Gets the model data for the api documentation. + /// + /// public SwaggerModelData GetModelData() { return SwaggerModelData.ForType(with => diff --git a/PlexRequests.UI/Modules/ApiMetadataModule.cs b/PlexRequests.UI/Modules/ApiMetadataModule.cs index 18e764621..eb8ae6a52 100644 --- a/PlexRequests.UI/Modules/ApiMetadataModule.cs +++ b/PlexRequests.UI/Modules/ApiMetadataModule.cs @@ -42,11 +42,23 @@ namespace PlexRequests.UI.Modules with.ResourcePath("/requests"); with.Summary("The list of requests"); - with.Notes("This returns a list of users from our awesome app"); + with.Notes("This returns a list of requests"); with.QueryParam("apikey", "The Api Key found in the settings", true); with.Model>>(); }); + Describe["GetRequest"] = description => description.AsSwagger(with => + { + with.ResourcePath("/requests/{id}"); + with.Summary("Get's a single request"); + + with.Notes("This returns a single request"); + with.QueryParam("apikey", "The Api Key found in the settings", true); + //with.QueryParam("id", "The request id to return", true); + with.PathParam("id"); + with.Model>>(); + }); + Describe["PostRequests"] = description => description.AsSwagger(with => { with.ResourcePath("/requests"); @@ -72,7 +84,7 @@ namespace PlexRequests.UI.Modules with.ResourcePath("/requests"); with.Summary("Deletes an existing request"); with.Model>(); - with.BodyParam("The request", true); + with.BodyParam("The request ID to delete", true); with.QueryParam("apikey", "The Api Key found in the settings", true); with.Notes("Deletes an existing request. If the request doesn't exist we will return an error."); }); diff --git a/PlexRequests.UI/Modules/ApiModule.cs b/PlexRequests.UI/Modules/ApiModule.cs index 3b0e7b40f..eb4f34f84 100644 --- a/PlexRequests.UI/Modules/ApiModule.cs +++ b/PlexRequests.UI/Modules/ApiModule.cs @@ -29,8 +29,6 @@ using System.Collections.Generic; using Nancy; using Nancy.ModelBinding; -using Nancy.Responses.Negotiation; -using Nancy.Validation; using PlexRequests.Core; using PlexRequests.Store; @@ -42,6 +40,7 @@ namespace PlexRequests.UI.Modules public ApiModule(IRequestService service) : base("api") { Get["GetRequests","/requests"] = x => GetRequests(); + Get["GetRequest","/requests/{id}"] = x => GetSingleRequests(x); Post["PostRequests", "/requests"] = x => CreateRequest(); Put["PutRequests", "/requests"] = x => UpdateRequest(); Delete["DeleteRequests", "/requests"] = x => DeleteRequest(); @@ -61,15 +60,33 @@ namespace PlexRequests.UI.Modules return ReturnReponse(apiModel); } + public Response GetSingleRequests(dynamic x) + { + var id = (int)x.id; + var apiModel = new ApiModel> { Data = new List() }; + + var requests = RequestService.Get(id); + if (string.IsNullOrEmpty(requests.Title)) + { + apiModel.Error = true; + apiModel.ErrorMessage = "Request does not exist"; + return ReturnReponse(apiModel); + } + apiModel.Data.Add(requests); + + return ReturnReponse(apiModel); + } + public Response CreateRequest() { - var request = this.Bind(); - var valid = this.Validate(request); - if (!valid.IsValid) + var request = this.BindAndValidate(); + + if (!ModelValidationResult.IsValid) { - return ReturnValidationReponse(valid); + return ReturnValidationReponse(ModelValidationResult); } + var apiModel = new ApiModel(); var result = RequestService.AddRequest(request); @@ -87,11 +104,11 @@ namespace PlexRequests.UI.Modules public Response UpdateRequest() { - var request = this.Bind(); - var valid = this.Validate(request); - if (!valid.IsValid) + var request = this.BindAndValidate(); + + if (!ModelValidationResult.IsValid) { - return ReturnValidationReponse(valid); + return ReturnValidationReponse(ModelValidationResult); } @@ -112,17 +129,20 @@ namespace PlexRequests.UI.Modules public Response DeleteRequest() { - var request = this.Bind(); - var valid = this.Validate(request); - if (!valid.IsValid) - { - return ReturnValidationReponse(valid); - } + var id = this.Bind(); + var apiModel = new ApiModel(); try { - RequestService.DeleteRequest(request); + var exisitingRequest = RequestService.Get(id); + if (exisitingRequest == null) + { + apiModel.Error = true; + apiModel.ErrorMessage = $"The request id {id} does not exist"; + return ReturnReponse(apiModel); + } + RequestService.DeleteRequest(exisitingRequest); apiModel.Data = true; return ReturnReponse(apiModel); diff --git a/PlexRequests.UI/PlexRequests.UI.csproj b/PlexRequests.UI/PlexRequests.UI.csproj index a2609cb6b..e74ce7fc1 100644 --- a/PlexRequests.UI/PlexRequests.UI.csproj +++ b/PlexRequests.UI/PlexRequests.UI.csproj @@ -185,6 +185,7 @@ + diff --git a/PlexRequests.UI/Validators/RequestedModelValidator.cs b/PlexRequests.UI/Validators/RequestedModelValidator.cs new file mode 100644 index 000000000..1f6f1d171 --- /dev/null +++ b/PlexRequests.UI/Validators/RequestedModelValidator.cs @@ -0,0 +1,45 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: RequestedModelValidator.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 FluentValidation; + +using PlexRequests.Store; + +namespace PlexRequests.UI.Validators +{ + public class RequestedModelValidator : AbstractValidator + { + public RequestedModelValidator() + { + RuleFor(x => x.Title).NotNull(); + RuleFor(x => x.ProviderId).NotNull().WithMessage("'ProviderId' must not be empty. Please use either TVMaze Id or TheMovieDb Id"); + RuleFor(x => x.PosterPath).NotNull(); + RuleFor(x => x.ReleaseDate).NotNull(); + RuleFor(x => x.Type).NotNull(); + RuleFor(x => x.RequestedUsers).NotNull(); + } + } +} \ No newline at end of file From 84dc4515fdc09f7a11371fec356bdb5326eb5487 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Thu, 19 May 2016 17:02:46 +0100 Subject: [PATCH 58/76] Removed the service locator from the base classes and added in some Api tests added all the tests back in! --- PlexRequests.UI.Tests/AdminModuleTests.cs | 26 +-- PlexRequests.UI.Tests/ApiModuleTests.cs | 160 ++++++++++++++++-- .../PlexRequests.UI.Tests.csproj | 9 + PlexRequests.UI.Tests/UserLoginModuleTests.cs | 138 ++++----------- PlexRequests.UI.Tests/packages.config | 2 + PlexRequests.UI/Modules/AdminModule.cs | 5 +- PlexRequests.UI/Modules/ApiDocsModule.cs | 5 +- PlexRequests.UI/Modules/ApiMetadataModule.cs | 5 +- PlexRequests.UI/Modules/ApiModule.cs | 13 +- .../Modules/ApplicationTesterModule.cs | 2 +- PlexRequests.UI/Modules/ApprovalModule.cs | 2 +- PlexRequests.UI/Modules/BaseApiModule.cs | 9 +- PlexRequests.UI/Modules/BaseAuthModule.cs | 9 +- PlexRequests.UI/Modules/BaseModule.cs | 11 +- PlexRequests.UI/Modules/IndexModule.cs | 5 +- PlexRequests.UI/Modules/LoginModule.cs | 3 +- PlexRequests.UI/Modules/RequestsModule.cs | 2 +- PlexRequests.UI/Modules/SearchModule.cs | 2 +- .../Modules/UpdateCheckerModule.cs | 3 +- PlexRequests.UI/Modules/UserLoginModule.cs | 2 +- .../Modules/UserManagementModule.cs | 14 +- 21 files changed, 251 insertions(+), 176 deletions(-) diff --git a/PlexRequests.UI.Tests/AdminModuleTests.cs b/PlexRequests.UI.Tests/AdminModuleTests.cs index 77d316deb..441e6fe53 100644 --- a/PlexRequests.UI.Tests/AdminModuleTests.cs +++ b/PlexRequests.UI.Tests/AdminModuleTests.cs @@ -25,6 +25,7 @@ // ************************************************************************/ #endregion using System.Collections.Generic; +using System.Linq; using Moq; @@ -32,6 +33,7 @@ using Nancy; using Nancy.Testing; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using NUnit.Framework; @@ -50,7 +52,6 @@ using PlexRequests.UI.Helpers; namespace PlexRequests.UI.Tests { [TestFixture] - [Ignore("Needs rework")] public class AdminModuleTests { private Mock> PlexRequestMock { get; set; } @@ -71,6 +72,7 @@ namespace PlexRequests.UI.Tests private Mock> LogRepo { get; set; } private Mock NotificationService { get; set; } private Mock Cache { get; set; } + private Mock> Log { get; set; } private ConfigurableBootstrapper Bootstrapper { get; set; } @@ -83,9 +85,10 @@ namespace PlexRequests.UI.Tests PlexMock = new Mock(); PlexMock.Setup(x => x.SignIn("Username1", "Password1")) - .Returns(new PlexAuthentication { user = new User { authentication_token = "abc", username = "Username1" } }); + .Returns(new PlexAuthentication { user = new User { authentication_token = "abc", title = "Username1" } }); PlexRequestMock = new Mock>(); + PlexRequestMock.Setup(x => x.GetSettings()).Returns(new PlexRequestSettings()); CpMock = new Mock>(); PlexSettingsMock = new Mock>(); SonarrApiMock = new Mock(); @@ -101,6 +104,7 @@ namespace PlexRequests.UI.Tests NotificationService = new Mock(); HeadphonesSettings = new Mock>(); Cache = new Mock(); + Log = new Mock>(); Bootstrapper = new ConfigurableBootstrapper(with => { @@ -123,16 +127,11 @@ namespace PlexRequests.UI.Tests with.Dependency(NotificationService.Object); with.Dependency(HeadphonesSettings.Object); with.Dependency(Cache.Object); - with.ApplicationStartup( - (container, pipelines) => - { - var loc = ServiceLocator.Instance; - loc.SetContainer(container); - }); + with.Dependency(Log.Object); with.RootPathProvider(); with.RequestStartup((container, pipelines, context) => { - context.CurrentUser = new UserIdentity { UserName = "user" }; + context.CurrentUser = new UserIdentity { UserName = "user", Claims = new List {"Admin"} }; }); }); @@ -240,7 +239,7 @@ namespace PlexRequests.UI.Tests [Test] public void GetUsersSuccessfully() { - var users = new PlexFriends { User = new[] { new UserFriends { Username = "abc2" }, } }; + var users = new PlexFriends { User = new[] { new UserFriends { Title = "abc2" }, } }; PlexMock.Setup(x => x.GetUsers(It.IsAny())).Returns(users); var browser = new Browser(Bootstrapper); @@ -255,9 +254,11 @@ namespace PlexRequests.UI.Tests Assert.That(HttpStatusCode.OK, Is.EqualTo(result.StatusCode)); - var body = result.Body.AsString(); + + var body = JsonConvert.DeserializeObject(result.Body.AsString()); + var user = body["users"]; Assert.That(body, Is.Not.Null); - Assert.That(body, Contains.Substring("abc2")); + Assert.That(user.ToString().Contains("abc"), Is.True); PlexMock.Verify(x => x.GetUsers(It.IsAny()), Times.Once); AuthMock.Verify(x => x.GetSettings(), Times.Once); @@ -276,6 +277,7 @@ namespace PlexRequests.UI.Tests with.Header("Accept", "application/json"); with.FormValue("username", "Username1"); with.FormValue("password", "Password1"); + }); diff --git a/PlexRequests.UI.Tests/ApiModuleTests.cs b/PlexRequests.UI.Tests/ApiModuleTests.cs index 7208b8bfb..28ed4b52a 100644 --- a/PlexRequests.UI.Tests/ApiModuleTests.cs +++ b/PlexRequests.UI.Tests/ApiModuleTests.cs @@ -24,22 +24,37 @@ // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // ************************************************************************/ #endregion +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +using FluentValidation; + using Moq; using Nancy; +using Nancy.Bootstrapper; using Nancy.Testing; +using Nancy.Validation; +using Nancy.Validation.FluentValidation; + +using Newtonsoft.Json; using NUnit.Framework; using PlexRequests.Core; using PlexRequests.Core.SettingModels; -using PlexRequests.UI.Helpers; +using PlexRequests.Store; +using PlexRequests.UI.Models; using PlexRequests.UI.Modules; +using PlexRequests.UI.Validators; + +using Ploeh.AutoFixture; namespace PlexRequests.UI.Tests { [TestFixture] - [Ignore("Locator :(")] public class ApiModuleTests { private ConfigurableBootstrapper Bootstrapper { get; set; } @@ -47,38 +62,147 @@ namespace PlexRequests.UI.Tests [SetUp] public void Setup() { + var requests = new Fixture().CreateMany(); var requestMock = new Mock(); var settingsMock = new Mock>(); + settingsMock.Setup(x => x.GetSettings()).Returns(new PlexRequestSettings {ApiKey = "api"}); + requestMock.Setup(x => x.GetAll()).Returns(requests); + requestMock.Setup(x => x.Get(1)).Returns(requests.FirstOrDefault()); + requestMock.Setup(x => x.Get(99)).Returns(new RequestedModel()); + requestMock.Setup(x => x.DeleteRequest(It.IsAny())); + Bootstrapper = new ConfigurableBootstrapper(with => { with.Module(); with.Dependency(requestMock.Object); with.Dependency(settingsMock.Object); - with.ApplicationStartup( - (c, a) => - { - var loc = ServiceLocator.Instance; - loc.SetContainer(c); - }); + with.RootPathProvider(); + with.ModelValidatorLocator( + new DefaultValidatorLocator( + new List() + { + new FluentValidationValidatorFactory( + new DefaultFluentAdapterFactory(new List()), + new List { new RequestedModelValidator() }) + })); }); - + + } + + private Action GetBrowser() + { + return with => + { + with.HttpRequest(); + with.Header("Accept", "application/json"); + with.Query("apikey", "api"); + }; + } + + [Test] + public void InvalidApiKey() + { + var browser = new Browser(Bootstrapper); + + var result = browser.Get("/api/requests", with => + { + with.HttpRequest(); + with.Header("Accept", "application/json"); + with.Query("apikey","a"); + }); + + Assert.That(HttpStatusCode.OK, Is.EqualTo(result.StatusCode)); + + var body = JsonConvert.DeserializeObject>>(result.Body.AsString()); + Assert.That(body.Error, Is.True); + Assert.That(body.ErrorMessage, Is.Not.Empty); } [Test] public void GetAllRequests() { - var browser = new Browser(Bootstrapper); - var result = browser.Post("/api/requests", with => - { - with.HttpRequest(); - with.Header("Accept", "application/json"); - with.Query("apikey","a"); - - }); - + var result = browser.Get("/api/requests", GetBrowser()); Assert.That(HttpStatusCode.OK, Is.EqualTo(result.StatusCode)); + + var body = JsonConvert.DeserializeObject>>(result.Body.AsString()); + Assert.That(body.Data, Is.Not.Null); + Assert.That(body.Data.Count, Is.GreaterThan(0)); + Assert.That(body.Error, Is.False); + Assert.That(body.ErrorMessage, Is.Null.Or.Empty); + } + + [Test] + public void GetSingleRequest() + { + var browser = new Browser(Bootstrapper); + + var result = browser.Get("/api/requests/1", GetBrowser()); + Assert.That(HttpStatusCode.OK, Is.EqualTo(result.StatusCode)); + + var body = JsonConvert.DeserializeObject>>(result.Body.AsString()); + Assert.That(body.Data, Is.Not.Null); + Assert.That(body.Data.Count, Is.EqualTo(1)); + Assert.That(body.Error, Is.False); + Assert.That(body.ErrorMessage, Is.Null.Or.Empty); + } + + [Test] + public void GetSingleRequestThatDoesntExist() + { + var browser = new Browser(Bootstrapper); + + var result = browser.Get("/api/requests/99", GetBrowser()); + Assert.That(HttpStatusCode.OK, Is.EqualTo(result.StatusCode)); + + var body = JsonConvert.DeserializeObject>>(result.Body.AsString()); + Assert.That(body.Data, Is.Not.Null); + Assert.That(body.Data.Count, Is.EqualTo(0)); + Assert.That(body.Error, Is.True); + Assert.That(body.ErrorMessage, Is.Not.Null.Or.Empty); + } + + [Test] + public void DeleteARequest() + { + var browser = new Browser(Bootstrapper); + + var result = browser.Delete("/api/requests/1", GetBrowser()); + Assert.That(HttpStatusCode.OK, Is.EqualTo(result.StatusCode)); + + var body = JsonConvert.DeserializeObject>(result.Body.AsString()); + Assert.That(body.Data, Is.True); + Assert.That(body.Error, Is.False); + Assert.That(body.ErrorMessage, Is.Null.Or.Empty); + } + + [Test] + public void DeleteARequestThatDoesNotExist() + { + var browser = new Browser(Bootstrapper); + + var result = browser.Delete("/api/requests/99", GetBrowser()); + Assert.That(HttpStatusCode.OK, Is.EqualTo(result.StatusCode)); + + var body = JsonConvert.DeserializeObject>(result.Body.AsString()); + Assert.That(body.Data, Is.False); + Assert.That(body.Error, Is.True); + Assert.That(body.ErrorMessage, Is.Not.Null.Or.Empty); + } + + [Test] + public void CreateAEmptyRequest() + { + var browser = new Browser(Bootstrapper); + + var result = browser.Post("/api/requests/", GetBrowser()); + Assert.That(HttpStatusCode.OK, Is.EqualTo(result.StatusCode)); + + var body = JsonConvert.DeserializeObject>(result.Body.AsString()); + Assert.That(body.Data, Is.Not.Null.Or.Empty); + Assert.That(body.Error, Is.True); + Assert.That(body.ErrorMessage, Is.Not.Null.Or.Empty); } } } \ No newline at end of file diff --git a/PlexRequests.UI.Tests/PlexRequests.UI.Tests.csproj b/PlexRequests.UI.Tests/PlexRequests.UI.Tests.csproj index 3c7db1543..083fd2286 100644 --- a/PlexRequests.UI.Tests/PlexRequests.UI.Tests.csproj +++ b/PlexRequests.UI.Tests/PlexRequests.UI.Tests.csproj @@ -46,6 +46,11 @@ ..\packages\FluentScheduler.3.1.46\lib\net40\FluentScheduler.dll True + + ..\packages\FluentValidation.6.2.1.0\lib\Net45\FluentValidation.dll + True + + ..\packages\Moq.4.2.1510.2205\lib\net40\Moq.dll True @@ -58,6 +63,10 @@ ..\packages\Nancy.Testing.1.4.1\lib\net40\Nancy.Testing.dll True + + ..\packages\Nancy.Validation.FluentValidation.1.4.1\lib\net40\Nancy.Validation.FluentValidation.dll + True + ..\packages\Nancy.Viewengines.Razor.1.4.3\lib\net40\Nancy.ViewEngines.Razor.dll True diff --git a/PlexRequests.UI.Tests/UserLoginModuleTests.cs b/PlexRequests.UI.Tests/UserLoginModuleTests.cs index d8e5b3e3a..f38f17ce0 100644 --- a/PlexRequests.UI.Tests/UserLoginModuleTests.cs +++ b/PlexRequests.UI.Tests/UserLoginModuleTests.cs @@ -47,11 +47,12 @@ using PlexRequests.UI.Modules; namespace PlexRequests.UI.Tests { [TestFixture] - [Ignore("Needs some work")] + //[Ignore("Needs some work")] public class UserLoginModuleTests { private Mock> AuthMock { get; set; } private Mock> PlexRequestMock { get; set; } + private ConfigurableBootstrapper Bootstrapper { get; set; } private Mock PlexMock { get; set; } [SetUp] @@ -60,6 +61,15 @@ namespace PlexRequests.UI.Tests AuthMock = new Mock>(); PlexMock = new Mock(); PlexRequestMock = new Mock>(); + PlexRequestMock.Setup(x => x.GetSettings()).Returns(new PlexRequestSettings()); + Bootstrapper = new ConfigurableBootstrapper(with => + { + with.Module(); + with.Dependency(PlexRequestMock.Object); + with.Dependency(AuthMock.Object); + with.Dependency(PlexMock.Object); + with.RootPathProvider(); + }); } [Test] @@ -68,21 +78,11 @@ namespace PlexRequests.UI.Tests var expectedSettings = new AuthenticationSettings { UserAuthentication = false, PlexAuthToken = "abc" }; AuthMock.Setup(x => x.GetSettings()).Returns(expectedSettings); - var bootstrapper = new ConfigurableBootstrapper(with => - { - with.Module(); - with.Dependency(AuthMock.Object); - with.Dependency(PlexMock.Object); - with.Dependency(PlexRequestMock.Object); - with.RootPathProvider(); - }); - var loc = ServiceLocator.Instance; - loc.SetContainer(TinyIoCContainer.Current); - bootstrapper.WithSession(new Dictionary()); + Bootstrapper.WithSession(new Dictionary()); - var browser = new Browser(bootstrapper); + var browser = new Browser(Bootstrapper); var result = browser.Post("/userlogin", with => { with.HttpRequest(); @@ -106,17 +106,10 @@ namespace PlexRequests.UI.Tests var expectedSettings = new AuthenticationSettings { UserAuthentication = false, PlexAuthToken = "abc" }; AuthMock.Setup(x => x.GetSettings()).Returns(expectedSettings); - var bootstrapper = new ConfigurableBootstrapper(with => - { - with.Module(); - with.Dependency(AuthMock.Object); - with.Dependency(PlexMock.Object); - with.RootPathProvider(); - }); - bootstrapper.WithSession(new Dictionary()); + Bootstrapper.WithSession(new Dictionary()); - var browser = new Browser(bootstrapper); + var browser = new Browser(Bootstrapper); var result = browser.Post("/userlogin", with => { with.HttpRequest(); @@ -143,7 +136,7 @@ namespace PlexRequests.UI.Tests { new UserFriends { - Username = "abc", + Title = "abc", }, } }; @@ -152,17 +145,9 @@ namespace PlexRequests.UI.Tests PlexMock.Setup(x => x.GetUsers(It.IsAny())).Returns(plexFriends); PlexMock.Setup(x => x.GetAccount(It.IsAny())).Returns(new PlexAccount()); - var bootstrapper = new ConfigurableBootstrapper(with => - { - with.Module(); - with.Dependency(AuthMock.Object); - with.Dependency(PlexMock.Object); - with.RootPathProvider(); - }); + Bootstrapper.WithSession(new Dictionary()); - bootstrapper.WithSession(new Dictionary()); - - var browser = new Browser(bootstrapper); + var browser = new Browser(Bootstrapper); var result = browser.Post("/userlogin", with => { with.HttpRequest(); @@ -199,17 +184,9 @@ namespace PlexRequests.UI.Tests PlexMock.Setup(x => x.GetUsers(It.IsAny())).Returns(plexFriends); PlexMock.Setup(x => x.GetAccount(It.IsAny())).Returns(new PlexAccount()); - var bootstrapper = new ConfigurableBootstrapper(with => - { - with.Module(); - with.Dependency(AuthMock.Object); - with.Dependency(PlexMock.Object); - with.RootPathProvider(); - }); + Bootstrapper.WithSession(new Dictionary()); - bootstrapper.WithSession(new Dictionary()); - - var browser = new Browser(bootstrapper); + var browser = new Browser(Bootstrapper); var result = browser.Post("/userlogin", with => { @@ -240,7 +217,7 @@ namespace PlexRequests.UI.Tests { new UserFriends { - Username = "abc", + Title = "abc", } } }; @@ -257,17 +234,9 @@ namespace PlexRequests.UI.Tests PlexMock.Setup(x => x.SignIn(It.IsAny(), It.IsAny())).Returns(plexAuth); PlexMock.Setup(x => x.GetAccount(It.IsAny())).Returns(new PlexAccount()); - var bootstrapper = new ConfigurableBootstrapper(with => - { - with.Module(); - with.Dependency(AuthMock.Object); - with.Dependency(PlexMock.Object); - with.RootPathProvider(); - }); + Bootstrapper.WithSession(new Dictionary()); - bootstrapper.WithSession(new Dictionary()); - - var browser = new Browser(bootstrapper); + var browser = new Browser(Bootstrapper); var result = browser.Post("/userlogin", with => { with.HttpRequest(); @@ -310,17 +279,10 @@ namespace PlexRequests.UI.Tests PlexMock.Setup(x => x.GetUsers(It.IsAny())).Returns(plexFriends); PlexMock.Setup(x => x.SignIn(It.IsAny(), It.IsAny())).Returns(plexAuth); - var bootstrapper = new ConfigurableBootstrapper(with => - { - with.Module(); - with.Dependency(AuthMock.Object); - with.Dependency(PlexMock.Object); - with.RootPathProvider(); - }); + + Bootstrapper.WithSession(new Dictionary()); - bootstrapper.WithSession(new Dictionary()); - - var browser = new Browser(bootstrapper); + var browser = new Browser(Bootstrapper); var result = browser.Post("/userlogin", with => { with.HttpRequest(); @@ -347,17 +309,9 @@ namespace PlexRequests.UI.Tests var expectedSettings = new AuthenticationSettings { UserAuthentication = false, DeniedUsers = "abc", PlexAuthToken = "abc" }; AuthMock.Setup(x => x.GetSettings()).Returns(expectedSettings); - var bootstrapper = new ConfigurableBootstrapper(with => - { - with.Module(); - with.Dependency(AuthMock.Object); - with.Dependency(PlexMock.Object); - with.RootPathProvider(); - }); + Bootstrapper.WithSession(new Dictionary()); - bootstrapper.WithSession(new Dictionary()); - - var browser = new Browser(bootstrapper); + var browser = new Browser(Bootstrapper); var result = browser.Post("/userlogin", with => { with.HttpRequest(); @@ -379,17 +333,9 @@ namespace PlexRequests.UI.Tests [Test] public void Logout() { - var bootstrapper = new ConfigurableBootstrapper(with => - { - with.Module(); - with.Dependency(AuthMock.Object); - with.Dependency(PlexMock.Object); - with.RootPathProvider(); - }); + Bootstrapper.WithSession(new Dictionary { { SessionKeys.UsernameKey, "abc" } }); - bootstrapper.WithSession(new Dictionary { { SessionKeys.UsernameKey, "abc" } }); - - var browser = new Browser(bootstrapper); + var browser = new Browser(Bootstrapper); var result = browser.Get("/userlogin/logout", with => { with.HttpRequest(); @@ -418,17 +364,9 @@ namespace PlexRequests.UI.Tests PlexMock.Setup(x => x.GetAccount(It.IsAny())).Returns(account); PlexMock.Setup(x => x.SignIn(It.IsAny(), It.IsAny())).Returns(new PlexAuthentication { user = new User { username = "Jamie" } }); - var bootstrapper = new ConfigurableBootstrapper(with => - { - with.Module(); - with.Dependency(AuthMock.Object); - with.Dependency(PlexMock.Object); - with.RootPathProvider(); - }); + Bootstrapper.WithSession(new Dictionary()); - bootstrapper.WithSession(new Dictionary()); - - var browser = new Browser(bootstrapper); + var browser = new Browser(Bootstrapper); var result = browser.Post("/userlogin", with => { with.HttpRequest(); @@ -473,17 +411,9 @@ namespace PlexRequests.UI.Tests PlexMock.Setup(x => x.SignIn(It.IsAny(), It.IsAny())).Returns(plexAuth); PlexMock.Setup(x => x.GetAccount(It.IsAny())).Returns(account); - var bootstrapper = new ConfigurableBootstrapper(with => - { - with.Module(); - with.Dependency(AuthMock.Object); - with.Dependency(PlexMock.Object); - with.RootPathProvider(); - }); + Bootstrapper.WithSession(new Dictionary()); - bootstrapper.WithSession(new Dictionary()); - - var browser = new Browser(bootstrapper); + var browser = new Browser(Bootstrapper); var result = browser.Post("/userlogin", with => { with.HttpRequest(); diff --git a/PlexRequests.UI.Tests/packages.config b/PlexRequests.UI.Tests/packages.config index 7de66863b..d4b45a263 100644 --- a/PlexRequests.UI.Tests/packages.config +++ b/PlexRequests.UI.Tests/packages.config @@ -3,10 +3,12 @@ + + diff --git a/PlexRequests.UI/Modules/AdminModule.cs b/PlexRequests.UI/Modules/AdminModule.cs index 38b830ead..56bc64df8 100644 --- a/PlexRequests.UI/Modules/AdminModule.cs +++ b/PlexRequests.UI/Modules/AdminModule.cs @@ -101,7 +101,7 @@ namespace PlexRequests.UI.Modules INotificationService notify, ISettingsService headphones, ISettingsService logs, - ICacheProvider cache) : base("admin") + ICacheProvider cache) : base("admin", prService) { PrService = prService; CpService = cpService; @@ -123,9 +123,8 @@ namespace PlexRequests.UI.Modules LogService = logs; Cache = cache; - #if DEBUG this.RequiresClaims(UserClaims.Admin); - #endif + Get["/"] = _ => Admin(); Get["/authentication"] = _ => Authentication(); diff --git a/PlexRequests.UI/Modules/ApiDocsModule.cs b/PlexRequests.UI/Modules/ApiDocsModule.cs index 61d4dde6f..94dbaeda5 100644 --- a/PlexRequests.UI/Modules/ApiDocsModule.cs +++ b/PlexRequests.UI/Modules/ApiDocsModule.cs @@ -27,11 +27,14 @@ using Nancy; using Nancy.Responses.Negotiation; +using PlexRequests.Core; +using PlexRequests.Core.SettingModels; + namespace PlexRequests.UI.Modules { public class ApiDocsModule : BaseModule { - public ApiDocsModule() : base("apidocs") + public ApiDocsModule(ISettingsService pr) : base("apidocs", pr) { Get["/"] = x => Documentation(); } diff --git a/PlexRequests.UI/Modules/ApiMetadataModule.cs b/PlexRequests.UI/Modules/ApiMetadataModule.cs index eb8ae6a52..6ea6ea8a5 100644 --- a/PlexRequests.UI/Modules/ApiMetadataModule.cs +++ b/PlexRequests.UI/Modules/ApiMetadataModule.cs @@ -54,7 +54,6 @@ namespace PlexRequests.UI.Modules with.Notes("This returns a single request"); with.QueryParam("apikey", "The Api Key found in the settings", true); - //with.QueryParam("id", "The request id to return", true); with.PathParam("id"); with.Model>>(); }); @@ -81,10 +80,10 @@ namespace PlexRequests.UI.Modules Describe["DeleteRequests"] = description => description.AsSwagger(with => { - with.ResourcePath("/requests"); + with.ResourcePath("/requests/{id}"); with.Summary("Deletes an existing request"); with.Model>(); - with.BodyParam("The request ID to delete", true); + with.PathParam("id"); with.QueryParam("apikey", "The Api Key found in the settings", true); with.Notes("Deletes an existing request. If the request doesn't exist we will return an error."); }); diff --git a/PlexRequests.UI/Modules/ApiModule.cs b/PlexRequests.UI/Modules/ApiModule.cs index eb4f34f84..e45981a9f 100644 --- a/PlexRequests.UI/Modules/ApiModule.cs +++ b/PlexRequests.UI/Modules/ApiModule.cs @@ -26,24 +26,26 @@ #endregion using System; using System.Collections.Generic; +using System.Diagnostics; using Nancy; using Nancy.ModelBinding; using PlexRequests.Core; +using PlexRequests.Core.SettingModels; using PlexRequests.Store; namespace PlexRequests.UI.Modules { public class ApiModule : BaseApiModule { - public ApiModule(IRequestService service) : base("api") + public ApiModule(IRequestService service, ISettingsService pr) : base("api", pr) { Get["GetRequests","/requests"] = x => GetRequests(); Get["GetRequest","/requests/{id}"] = x => GetSingleRequests(x); Post["PostRequests", "/requests"] = x => CreateRequest(); Put["PutRequests", "/requests"] = x => UpdateRequest(); - Delete["DeleteRequests", "/requests"] = x => DeleteRequest(); + Delete["DeleteRequests", "/requests/{id}"] = x => DeleteRequest(x); RequestService = service; } @@ -127,16 +129,15 @@ namespace PlexRequests.UI.Modules return ReturnReponse(apiModel); } - public Response DeleteRequest() + public Response DeleteRequest(dynamic x) { - var id = this.Bind(); - + var id = (int)x.id; var apiModel = new ApiModel(); try { var exisitingRequest = RequestService.Get(id); - if (exisitingRequest == null) + if (string.IsNullOrEmpty(exisitingRequest.Title)) { apiModel.Error = true; apiModel.ErrorMessage = $"The request id {id} does not exist"; diff --git a/PlexRequests.UI/Modules/ApplicationTesterModule.cs b/PlexRequests.UI/Modules/ApplicationTesterModule.cs index 55df3310f..69cf1575c 100644 --- a/PlexRequests.UI/Modules/ApplicationTesterModule.cs +++ b/PlexRequests.UI/Modules/ApplicationTesterModule.cs @@ -44,7 +44,7 @@ namespace PlexRequests.UI.Modules { public ApplicationTesterModule(ICouchPotatoApi cpApi, ISonarrApi sonarrApi, IPlexApi plexApi, - ISettingsService authSettings, ISickRageApi srApi, IHeadphonesApi hpApi) : base("test") + ISettingsService authSettings, ISickRageApi srApi, IHeadphonesApi hpApi, ISettingsService pr) : base("test", pr) { this.RequiresAuthentication(); diff --git a/PlexRequests.UI/Modules/ApprovalModule.cs b/PlexRequests.UI/Modules/ApprovalModule.cs index d57f31a1f..c0546a70c 100644 --- a/PlexRequests.UI/Modules/ApprovalModule.cs +++ b/PlexRequests.UI/Modules/ApprovalModule.cs @@ -48,7 +48,7 @@ namespace PlexRequests.UI.Modules public ApprovalModule(IRequestService service, ISettingsService cpService, ICouchPotatoApi cpApi, ISonarrApi sonarrApi, ISettingsService sonarrSettings, ISickRageApi srApi, ISettingsService srSettings, - ISettingsService hpSettings, IHeadphonesApi hpApi) : base("approval") + ISettingsService hpSettings, IHeadphonesApi hpApi, ISettingsService pr) : base("approval", pr) { this.RequiresClaims(UserClaims.Admin, UserClaims.PowerUser); diff --git a/PlexRequests.UI/Modules/BaseApiModule.cs b/PlexRequests.UI/Modules/BaseApiModule.cs index 9d2d023e3..4ed4b8031 100644 --- a/PlexRequests.UI/Modules/BaseApiModule.cs +++ b/PlexRequests.UI/Modules/BaseApiModule.cs @@ -30,6 +30,7 @@ using System.Linq; using Nancy; using Nancy.Validation; +using PlexRequests.Core; using PlexRequests.Core.SettingModels; using PlexRequests.Store; @@ -37,16 +38,20 @@ namespace PlexRequests.UI.Modules { public abstract class BaseApiModule : BaseModule { - protected BaseApiModule() + protected BaseApiModule(ISettingsService s) : base(s) { + Settings = s; Before += (ctx) => CheckAuth(); } - protected BaseApiModule(string modulePath) : base(modulePath) + protected BaseApiModule(string modulePath, ISettingsService s) : base(modulePath, s) { + Settings = s; Before += (ctx) => CheckAuth(); } + private ISettingsService Settings { get; } + protected Response ReturnReponse(object result) { var queryString = (DynamicDictionary)Context.Request.Query; diff --git a/PlexRequests.UI/Modules/BaseAuthModule.cs b/PlexRequests.UI/Modules/BaseAuthModule.cs index d640d4b21..0bbad4a02 100644 --- a/PlexRequests.UI/Modules/BaseAuthModule.cs +++ b/PlexRequests.UI/Modules/BaseAuthModule.cs @@ -75,20 +75,23 @@ namespace PlexRequests.UI.Modules } } - protected BaseAuthModule() + protected BaseAuthModule(ISettingsService pr) : base(pr) { + Service = pr; Before += (ctx) => CheckAuth(); } - protected BaseAuthModule(string modulePath) : base(modulePath) + protected BaseAuthModule(string modulePath, ISettingsService pr) : base(modulePath, pr) { + Service = pr; Before += (ctx) => CheckAuth(); } + private ISettingsService Service { get; } private Response CheckAuth() { - var settings = Locator.Resolve>().GetSettings(); + var settings = Service.GetSettings(); var baseUrl = settings.BaseUrl; var redirectPath = string.IsNullOrEmpty(baseUrl) ? "~/userlogin" : $"~/{baseUrl}/userlogin"; diff --git a/PlexRequests.UI/Modules/BaseModule.cs b/PlexRequests.UI/Modules/BaseModule.cs index 5d289a578..335fc16f5 100644 --- a/PlexRequests.UI/Modules/BaseModule.cs +++ b/PlexRequests.UI/Modules/BaseModule.cs @@ -28,19 +28,16 @@ using Nancy; using PlexRequests.Core; using PlexRequests.Core.SettingModels; -using PlexRequests.UI.Helpers; namespace PlexRequests.UI.Modules { public abstract class BaseModule : NancyModule { - protected ServiceLocator Locator => ServiceLocator.Instance; - protected ISettingsService Settings => Locator.Resolve>(); protected string BaseUrl { get; set; } - protected BaseModule() + protected BaseModule(ISettingsService settingsService) { - var settings = Settings.GetSettings(); + var settings = settingsService.GetSettings(); var baseUrl = settings.BaseUrl; BaseUrl = baseUrl; @@ -49,9 +46,9 @@ namespace PlexRequests.UI.Modules ModulePath = modulePath; } - protected BaseModule(string modulePath) + protected BaseModule(string modulePath, ISettingsService settingsService) { - var settings = Settings.GetSettings(); + var settings = settingsService.GetSettings(); var baseUrl = settings.BaseUrl; BaseUrl = baseUrl; diff --git a/PlexRequests.UI/Modules/IndexModule.cs b/PlexRequests.UI/Modules/IndexModule.cs index 46b54e098..81cdfa7e2 100644 --- a/PlexRequests.UI/Modules/IndexModule.cs +++ b/PlexRequests.UI/Modules/IndexModule.cs @@ -27,11 +27,14 @@ using Nancy; using Nancy.Extensions; +using PlexRequests.Core; +using PlexRequests.Core.SettingModels; + namespace PlexRequests.UI.Modules { public class IndexModule : BaseAuthModule { - public IndexModule() + public IndexModule(ISettingsService pr) : base(pr) { Get["/"] = parameters => Context.GetRedirect(!string.IsNullOrEmpty(BaseUrl) ? $"~/{BaseUrl}/search" : "~/search"); diff --git a/PlexRequests.UI/Modules/LoginModule.cs b/PlexRequests.UI/Modules/LoginModule.cs index 416291f48..14fbf49d0 100644 --- a/PlexRequests.UI/Modules/LoginModule.cs +++ b/PlexRequests.UI/Modules/LoginModule.cs @@ -34,13 +34,14 @@ using Nancy.Responses.Negotiation; using Nancy.Security; using PlexRequests.Core; +using PlexRequests.Core.SettingModels; using PlexRequests.UI.Models; namespace PlexRequests.UI.Modules { public class LoginModule : BaseModule { - public LoginModule() + public LoginModule(ISettingsService pr) : base(pr) { Get["/login"] = _ => { diff --git a/PlexRequests.UI/Modules/RequestsModule.cs b/PlexRequests.UI/Modules/RequestsModule.cs index 3886649e7..94e01a90b 100644 --- a/PlexRequests.UI/Modules/RequestsModule.cs +++ b/PlexRequests.UI/Modules/RequestsModule.cs @@ -59,7 +59,7 @@ namespace PlexRequests.UI.Modules ICouchPotatoApi cpApi, ISonarrApi sonarrApi, ISickRageApi sickRageApi, - ICacheProvider cache) : base("requests") + ICacheProvider cache) : base("requests", prSettings) { Service = service; PrSettings = prSettings; diff --git a/PlexRequests.UI/Modules/SearchModule.cs b/PlexRequests.UI/Modules/SearchModule.cs index 4ef08002b..62601aee8 100644 --- a/PlexRequests.UI/Modules/SearchModule.cs +++ b/PlexRequests.UI/Modules/SearchModule.cs @@ -62,7 +62,7 @@ namespace PlexRequests.UI.Modules ISettingsService sickRageService, ICouchPotatoApi cpApi, ISickRageApi srApi, INotificationService notify, IMusicBrainzApi mbApi, IHeadphonesApi hpApi, ISettingsService hpService, ICouchPotatoCacher cpCacher, ISonarrCacher sonarrCacher, ISickRageCacher sickRageCacher, IPlexApi plexApi, - ISettingsService plexService, ISettingsService auth) : base("search") + ISettingsService plexService, ISettingsService auth) : base("search", prSettings) { Auth = auth; PlexService = plexService; diff --git a/PlexRequests.UI/Modules/UpdateCheckerModule.cs b/PlexRequests.UI/Modules/UpdateCheckerModule.cs index 3d89ff2ff..f1b46a805 100644 --- a/PlexRequests.UI/Modules/UpdateCheckerModule.cs +++ b/PlexRequests.UI/Modules/UpdateCheckerModule.cs @@ -31,6 +31,7 @@ using Nancy; using NLog; using PlexRequests.Core; +using PlexRequests.Core.SettingModels; using PlexRequests.Helpers; using PlexRequests.UI.Models; @@ -38,7 +39,7 @@ namespace PlexRequests.UI.Modules { public class UpdateCheckerModule : BaseAuthModule { - public UpdateCheckerModule(ICacheProvider provider) : base("updatechecker") + public UpdateCheckerModule(ICacheProvider provider, ISettingsService pr) : base("updatechecker", pr) { Cache = provider; diff --git a/PlexRequests.UI/Modules/UserLoginModule.cs b/PlexRequests.UI/Modules/UserLoginModule.cs index a543287c1..d6acb5343 100644 --- a/PlexRequests.UI/Modules/UserLoginModule.cs +++ b/PlexRequests.UI/Modules/UserLoginModule.cs @@ -46,7 +46,7 @@ namespace PlexRequests.UI.Modules { public class UserLoginModule : BaseModule { - public UserLoginModule(ISettingsService auth, IPlexApi api) : base("userlogin") + public UserLoginModule(ISettingsService auth, IPlexApi api, ISettingsService pr) : base("userlogin", pr) { AuthService = auth; Api = api; diff --git a/PlexRequests.UI/Modules/UserManagementModule.cs b/PlexRequests.UI/Modules/UserManagementModule.cs index 2ea43e79e..b2d8a6a2d 100644 --- a/PlexRequests.UI/Modules/UserManagementModule.cs +++ b/PlexRequests.UI/Modules/UserManagementModule.cs @@ -1,23 +1,19 @@ -using System; +using System.Collections.Generic; using Nancy; -using Nancy.Authentication.Forms; -using Nancy.Extensions; using Nancy.Responses.Negotiation; using Nancy.Security; using PlexRequests.Core; -using PlexRequests.UI.Models; -using PlexRequests.UI.Modules; +using PlexRequests.Core.SettingModels; using PlexRequests.Helpers; -using System.Collections.Generic; +using PlexRequests.UI.Models; - -namespace PlexRequests.UI +namespace PlexRequests.UI.Modules { public class UserManagementModule : BaseModule { - public UserManagementModule() : base("usermanagement") + public UserManagementModule(ISettingsService pr) : base("usermanagement",pr) { this.RequiresClaims(UserClaims.Admin); Get["/"] = x => Load(); From cbfe88cd6ddb970978cf7a99ba9fce0c9e3c80eb Mon Sep 17 00:00:00 2001 From: tidusjar Date: Fri, 20 May 2016 10:44:02 +0100 Subject: [PATCH 59/76] Added the ability to get the apikey from the api if you provide a correct username and password. Added more unit tests Added the ability to change a users password using the api refactored the Usermapper and made it unit testsable. Also api documentation for the new endpoints too. #222 #205 --- PlexRequests.Core/UserMapper.cs | 52 +++--- PlexRequests.UI.Tests/ApiModuleTests.cs | 170 +++++++++++++++++- PlexRequests.UI.Tests/UserLoginModuleTests.cs | 2 - PlexRequests.UI/Bootstrapper.cs | 2 + .../ModelDataProviders/UserUpdateViewModel.cs | 55 ++++++ PlexRequests.UI/Models/UserUpdateViewModel.cs | 34 ++++ PlexRequests.UI/Modules/AdminModule.cs | 13 +- PlexRequests.UI/Modules/ApiMetadataModule.cs | 24 ++- PlexRequests.UI/Modules/ApiModule.cs | 60 ++++++- PlexRequests.UI/Modules/BaseApiModule.cs | 4 + PlexRequests.UI/Modules/LoginModule.cs | 4 +- .../Modules/UserManagementModule.cs | 4 +- PlexRequests.UI/PlexRequests.UI.csproj | 3 + .../Validators/UserViewModelValidator.cs | 41 +++++ 14 files changed, 424 insertions(+), 44 deletions(-) create mode 100644 PlexRequests.UI/ModelDataProviders/UserUpdateViewModel.cs create mode 100644 PlexRequests.UI/Models/UserUpdateViewModel.cs create mode 100644 PlexRequests.UI/Validators/UserViewModelValidator.cs diff --git a/PlexRequests.Core/UserMapper.cs b/PlexRequests.Core/UserMapper.cs index a846652c5..eb66cbea3 100644 --- a/PlexRequests.Core/UserMapper.cs +++ b/PlexRequests.Core/UserMapper.cs @@ -36,21 +36,20 @@ using Nancy.Security; using PlexRequests.Core.Models; using PlexRequests.Helpers; using PlexRequests.Store; +using PlexRequests.Store.Repository; namespace PlexRequests.Core { - public class UserMapper : IUserMapper + public class UserMapper : IUserMapper, ICustomUserMapper { - public UserMapper(ISqliteConfiguration db) + public UserMapper(IRepository repo) { - Db = db; + Repo = repo; } - private static ISqliteConfiguration Db { get; set; } + private static IRepository Repo { get; set; } public IUserIdentity GetUserFromIdentifier(Guid identifier, NancyContext context) { - var repo = new UserRepository(Db); - - var user = repo.Get(identifier.ToString()); + var user = Repo.Get(identifier.ToString()); if (user == null) { @@ -64,10 +63,9 @@ namespace PlexRequests.Core }; } - public static Guid? ValidateUser(string username, string password) + public Guid? ValidateUser(string username, string password) { - var repo = new UserRepository(Db); - var users = repo.GetAll(); + var users = Repo.GetAll(); foreach (var u in users) { @@ -83,17 +81,15 @@ namespace PlexRequests.Core return null; } - public static bool DoUsersExist() + public bool DoUsersExist() { - var repo = new UserRepository(Db); - var users = repo.GetAll(); + var users = Repo.GetAll(); return users.Any(); } - public static Guid? CreateUser(string username, string password, string[] claims = default(string[])) + public Guid? CreateUser(string username, string password, string[] claims = default(string[])) { - var repo = new UserRepository(Db); var salt = PasswordHasher.GenerateSalt(); var userModel = new UsersModel @@ -105,17 +101,16 @@ namespace PlexRequests.Core Claims = ByteConverterHelper.ReturnBytes(claims), UserProperties = ByteConverterHelper.ReturnBytes(new UserProperties()) }; - repo.Insert(userModel); + Repo.Insert(userModel); - var userRecord = repo.Get(userModel.UserGuid); + var userRecord = Repo.Get(userModel.UserGuid); return new Guid(userRecord.UserGuid); } - public static bool UpdatePassword(string username, string oldPassword, string newPassword) + public bool UpdatePassword(string username, string oldPassword, string newPassword) { - var repo = new UserRepository(Db); - var users = repo.GetAll(); + var users = Repo.GetAll(); var userToChange = users.FirstOrDefault(x => x.UserName == username); if (userToChange == null) return false; @@ -132,13 +127,22 @@ namespace PlexRequests.Core userToChange.Hash = newHash; userToChange.Salt = newSalt; - return repo.Update(userToChange); + return Repo.Update(userToChange); } - public static IEnumerable GetUsers() + public IEnumerable GetUsers() { - var repo = new UserRepository(Db); - return repo.GetAll(); + return Repo.GetAll(); } } + + public interface ICustomUserMapper + { + IEnumerable GetUsers(); + Guid? CreateUser(string username, string password, string[] claims = default(string[])); + bool DoUsersExist(); + Guid? ValidateUser(string username, string password); + bool UpdatePassword(string username, string oldPassword, string newPassword); + + } } diff --git a/PlexRequests.UI.Tests/ApiModuleTests.cs b/PlexRequests.UI.Tests/ApiModuleTests.cs index 28ed4b52a..26d7dca52 100644 --- a/PlexRequests.UI.Tests/ApiModuleTests.cs +++ b/PlexRequests.UI.Tests/ApiModuleTests.cs @@ -34,7 +34,6 @@ using FluentValidation; using Moq; using Nancy; -using Nancy.Bootstrapper; using Nancy.Testing; using Nancy.Validation; using Nancy.Validation.FluentValidation; @@ -46,6 +45,7 @@ using NUnit.Framework; using PlexRequests.Core; using PlexRequests.Core.SettingModels; using PlexRequests.Store; +using PlexRequests.Store.Repository; using PlexRequests.UI.Models; using PlexRequests.UI.Modules; using PlexRequests.UI.Validators; @@ -62,31 +62,48 @@ namespace PlexRequests.UI.Tests [SetUp] public void Setup() { - var requests = new Fixture().CreateMany(); + var fixture = new Fixture(); + var requests = fixture.CreateMany(); var requestMock = new Mock(); var settingsMock = new Mock>(); + var userRepoMock = new Mock>(); + var mapperMock = new Mock(); + + var userModels = fixture.CreateMany().ToList(); + userModels.Add(new UsersModel + { + UserName = "user1" + }); + settingsMock.Setup(x => x.GetSettings()).Returns(new PlexRequestSettings {ApiKey = "api"}); requestMock.Setup(x => x.GetAll()).Returns(requests); requestMock.Setup(x => x.Get(1)).Returns(requests.FirstOrDefault()); requestMock.Setup(x => x.Get(99)).Returns(new RequestedModel()); requestMock.Setup(x => x.DeleteRequest(It.IsAny())); + userRepoMock.Setup(x => x.GetAll()).Returns(userModels); + userRepoMock.Setup(x => x.Update(It.IsAny())).Returns(true); + + mapperMock.Setup(x => x.ValidateUser("user1", It.IsAny())).Returns(Guid.NewGuid()); + mapperMock.Setup(x => x.UpdatePassword("user1", "password", "newpassword")).Returns(true); + Bootstrapper = new ConfigurableBootstrapper(with => { with.Module(); with.Dependency(requestMock.Object); with.Dependency(settingsMock.Object); + with.Dependency(userRepoMock.Object); + with.Dependency(mapperMock.Object); with.RootPathProvider(); with.ModelValidatorLocator( new DefaultValidatorLocator( - new List() + new List { new FluentValidationValidatorFactory( new DefaultFluentAdapterFactory(new List()), - new List { new RequestedModelValidator() }) + new List { new RequestedModelValidator(), new UserViewModelValidator() }) })); }); - } private Action GetBrowser() @@ -192,6 +209,7 @@ namespace PlexRequests.UI.Tests } [Test] + [Description("Should file the validation")] public void CreateAEmptyRequest() { var browser = new Browser(Bootstrapper); @@ -204,5 +222,147 @@ namespace PlexRequests.UI.Tests Assert.That(body.Error, Is.True); Assert.That(body.ErrorMessage, Is.Not.Null.Or.Empty); } + + [Test] + public void UpdateUsersPassword() + { + var model = new UserUpdateViewModel + { + CurrentPassword = "password", + NewPassword = "newpassword" + }; + var browser = new Browser(Bootstrapper); + var result = browser.Put("/api/credentials/user1", with => + { + with.HttpRequest(); + with.Header("Accept", "application/json"); + with.Query("apikey", "api"); + with.JsonBody(model); + }); + + Assert.That(HttpStatusCode.OK, Is.EqualTo(result.StatusCode)); + + var body = JsonConvert.DeserializeObject>(result.Body.AsString()); + Assert.That(body.Data, Is.Not.Null.Or.Empty); + Assert.That(body.Error, Is.False); + Assert.That(body.ErrorMessage, Is.Null.Or.Empty); + } + + [Test] + public void UpdateInvalidUsersPassword() + { + var model = new UserUpdateViewModel + { + CurrentPassword = "password", + NewPassword = "newpassword" + }; + var browser = new Browser(Bootstrapper); + var result = browser.Put("/api/credentials/user99", with => + { + with.HttpRequest(); + with.Header("Accept", "application/json"); + with.Query("apikey", "api"); + with.JsonBody(model); + }); + + Assert.That(HttpStatusCode.OK, Is.EqualTo(result.StatusCode)); + + var body = JsonConvert.DeserializeObject>(result.Body.AsString()); + Assert.That(body.Data, Is.Null.Or.Empty); + Assert.That(body.Error, Is.True); + Assert.That(body.ErrorMessage, Is.Not.Null.Or.Empty); + } + + [Test] + public void UpdateUsersInvalidPassword() + { + var model = new UserUpdateViewModel + { + CurrentPassword = "password", + NewPassword = "password2" + }; + var browser = new Browser(Bootstrapper); + var result = browser.Put("/api/credentials/user1", with => + { + with.HttpRequest(); + with.Header("Accept", "application/json"); + with.Query("apikey", "api"); + with.JsonBody(model); + }); + + Assert.That(HttpStatusCode.OK, Is.EqualTo(result.StatusCode)); + + var body = JsonConvert.DeserializeObject>(result.Body.AsString()); + Assert.That(body.Data, Is.Null.Or.Empty); + Assert.That(body.Error, Is.True); + Assert.That(body.ErrorMessage, Is.Not.Null.Or.Empty); + } + + [Test] + public void UpdateUsersWithBadModel() + { + var model = new UserUpdateViewModel + { + CurrentPassword = null, + NewPassword = "password2" + }; + var browser = new Browser(Bootstrapper); + var result = browser.Put("/api/credentials/user1", with => + { + with.HttpRequest(); + with.Header("Accept", "application/json"); + with.Query("apikey", "api"); + with.JsonBody(model); + }); + + Assert.That(HttpStatusCode.OK, Is.EqualTo(result.StatusCode)); + + var body = JsonConvert.DeserializeObject>(result.Body.AsString()); + Assert.That(body.Data.Length, Is.GreaterThan(0)); + Assert.That(body.Error, Is.True); + Assert.That(body.ErrorMessage, Is.Not.Null.Or.Empty); + } + + [Test] + public void GetApiKey() + { + var browser = new Browser(Bootstrapper); + var result = browser.Get("/api/apikey", with => + { + with.HttpRequest(); + with.Header("Accept", "application/json"); + with.Query("apikey", "api"); + with.Query("username","user1"); + with.Query("password","password"); + }); + + Assert.That(HttpStatusCode.OK, Is.EqualTo(result.StatusCode)); + + var body = JsonConvert.DeserializeObject>(result.Body.AsString()); + Assert.That(body.Data, Is.Not.Null.Or.Empty); + Assert.That(body.Error, Is.False); + Assert.That(body.ErrorMessage, Is.Null.Or.Empty); + } + + [Test] + public void GetApiKeyWithBadCredentials() + { + var browser = new Browser(Bootstrapper); + var result = browser.Get("/api/apikey", with => + { + with.HttpRequest(); + with.Header("Accept", "application/json"); + with.Query("apikey", "api"); + with.Query("username", "user"); + with.Query("password", "password"); + }); + + Assert.That(HttpStatusCode.OK, Is.EqualTo(result.StatusCode)); + + var body = JsonConvert.DeserializeObject>(result.Body.AsString()); + Assert.That(body.Data, Is.Null.Or.Empty); + Assert.That(body.Error, Is.True); + Assert.That(body.ErrorMessage, Is.Not.Null.Or.Empty); + } } } \ No newline at end of file diff --git a/PlexRequests.UI.Tests/UserLoginModuleTests.cs b/PlexRequests.UI.Tests/UserLoginModuleTests.cs index f38f17ce0..cc4db6843 100644 --- a/PlexRequests.UI.Tests/UserLoginModuleTests.cs +++ b/PlexRequests.UI.Tests/UserLoginModuleTests.cs @@ -30,7 +30,6 @@ using Moq; using Nancy; using Nancy.Testing; -using Nancy.TinyIoc; using Newtonsoft.Json; @@ -40,7 +39,6 @@ using PlexRequests.Api.Interfaces; using PlexRequests.Api.Models.Plex; using PlexRequests.Core; using PlexRequests.Core.SettingModels; -using PlexRequests.UI.Helpers; using PlexRequests.UI.Models; using PlexRequests.UI.Modules; diff --git a/PlexRequests.UI/Bootstrapper.cs b/PlexRequests.UI/Bootstrapper.cs index 28d8905da..c2e31d5dd 100644 --- a/PlexRequests.UI/Bootstrapper.cs +++ b/PlexRequests.UI/Bootstrapper.cs @@ -69,6 +69,7 @@ namespace PlexRequests.UI protected override void ConfigureRequestContainer(TinyIoCContainer container, NancyContext context) { container.Register(); + container.Register(); container.Register(new DbConfiguration(new SqliteFactory())); container.Register().AsSingleton(); @@ -87,6 +88,7 @@ namespace PlexRequests.UI // Repo's container.Register, GenericRepository>(); + container.Register, UserRepository>(); container.Register, GenericRepository>(); container.Register(); container.Register(); diff --git a/PlexRequests.UI/ModelDataProviders/UserUpdateViewModel.cs b/PlexRequests.UI/ModelDataProviders/UserUpdateViewModel.cs new file mode 100644 index 000000000..4c626a555 --- /dev/null +++ b/PlexRequests.UI/ModelDataProviders/UserUpdateViewModel.cs @@ -0,0 +1,55 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: RequestedModelDataProvider.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 Nancy.Swagger; +using Nancy.Swagger.Services; + +using PlexRequests.Store; + +namespace PlexRequests.UI.ModelDataProviders +{ + public class UserUpdateViewModel : ISwaggerModelDataProvider + { + /// + /// Gets the model data for the api documentation. + /// + /// + public SwaggerModelData GetModelData() + { + return SwaggerModelData.ForType(with => + { + with.Property(x => x.CurrentPassword) + .Description("The users current password") + .Required(true); + + with.Property(x => x.NewPassword) + .Description("The users new password that we will change it to") + .Required(true); + }); + } + } +} \ No newline at end of file diff --git a/PlexRequests.UI/Models/UserUpdateViewModel.cs b/PlexRequests.UI/Models/UserUpdateViewModel.cs new file mode 100644 index 000000000..550c2381b --- /dev/null +++ b/PlexRequests.UI/Models/UserUpdateViewModel.cs @@ -0,0 +1,34 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: UserUpdateViewModel.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 PlexRequests.UI.Models +{ + public class UserUpdateViewModel + { + public string CurrentPassword { get; set; } + public string NewPassword { get; set; } + } +} \ No newline at end of file diff --git a/PlexRequests.UI/Modules/AdminModule.cs b/PlexRequests.UI/Modules/AdminModule.cs index 56bc64df8..62348a269 100644 --- a/PlexRequests.UI/Modules/AdminModule.cs +++ b/PlexRequests.UI/Modules/AdminModule.cs @@ -709,17 +709,14 @@ namespace PlexRequests.UI.Modules private Response CreateApiKey() { - this.RequiresClaims (UserClaims.Admin); - - var apiKey = Guid.NewGuid ().ToString ("N"); - - var settings = PrService.GetSettings (); + this.RequiresClaims(UserClaims.Admin); + var apiKey = Guid.NewGuid().ToString("N"); + var settings = PrService.GetSettings(); settings.ApiKey = apiKey; + PrService.SaveSettings(settings); - PrService.SaveSettings (settings); - - return Response.AsJson (apiKey); + return Response.AsJson(apiKey); } } } \ No newline at end of file diff --git a/PlexRequests.UI/Modules/ApiMetadataModule.cs b/PlexRequests.UI/Modules/ApiMetadataModule.cs index 6ea6ea8a5..6418ca158 100644 --- a/PlexRequests.UI/Modules/ApiMetadataModule.cs +++ b/PlexRequests.UI/Modules/ApiMetadataModule.cs @@ -30,6 +30,7 @@ using Nancy.Metadata.Modules; using Nancy.Swagger; using PlexRequests.Store; +using PlexRequests.UI.Models; namespace PlexRequests.UI.Modules { @@ -83,11 +84,32 @@ namespace PlexRequests.UI.Modules with.ResourcePath("/requests/{id}"); with.Summary("Deletes an existing request"); with.Model>(); - with.PathParam("id"); + with.PathParam("id", required:true); with.QueryParam("apikey", "The Api Key found in the settings", true); with.Notes("Deletes an existing request. If the request doesn't exist we will return an error."); }); + Describe["GetApiKey"] = description => description.AsSwagger(with => + { + with.ResourcePath("/apikey"); + with.Summary("Gets the Api Key for Plex Requests"); + with.Model>(); + with.QueryParam("username", required:true ); + with.QueryParam("password", required: true ); + with.Notes("Get's the current api key for the application"); + }); + + Describe["PutCredentials"] = description => description.AsSwagger(with => + { + with.ResourcePath("/credentials/{username}"); + with.Summary("Sets a new password for the user"); + with.Model>(); + with.PathParam("username", required:true); + with.QueryParam("apikey", "The Api Key found in the settings", true); + with.BodyParam("User update view model", true); + with.Notes("Sets a new password for the user"); + }); + } } } \ No newline at end of file diff --git a/PlexRequests.UI/Modules/ApiModule.cs b/PlexRequests.UI/Modules/ApiModule.cs index e45981a9f..441d32485 100644 --- a/PlexRequests.UI/Modules/ApiModule.cs +++ b/PlexRequests.UI/Modules/ApiModule.cs @@ -26,7 +26,6 @@ #endregion using System; using System.Collections.Generic; -using System.Diagnostics; using Nancy; using Nancy.ModelBinding; @@ -34,12 +33,13 @@ using Nancy.ModelBinding; using PlexRequests.Core; using PlexRequests.Core.SettingModels; using PlexRequests.Store; +using PlexRequests.UI.Models; namespace PlexRequests.UI.Modules { public class ApiModule : BaseApiModule { - public ApiModule(IRequestService service, ISettingsService pr) : base("api", pr) + public ApiModule(IRequestService service, ISettingsService pr, ICustomUserMapper m) : base("api", pr) { Get["GetRequests","/requests"] = x => GetRequests(); Get["GetRequest","/requests/{id}"] = x => GetSingleRequests(x); @@ -47,10 +47,18 @@ namespace PlexRequests.UI.Modules Put["PutRequests", "/requests"] = x => UpdateRequest(); Delete["DeleteRequests", "/requests/{id}"] = x => DeleteRequest(x); + Get["GetApiKey", "/apikey"] = x => GetApiKey(); + + Put["PutCredentials", "/credentials/{username}"] = x => ChangePassword(x); + RequestService = service; + SettingsService = pr; + UserMapper = m; } private IRequestService RequestService { get; } + private ISettingsService SettingsService { get; } + private ICustomUserMapper UserMapper { get; } public Response GetRequests() { @@ -156,6 +164,54 @@ namespace PlexRequests.UI.Modules } } + public Response GetApiKey() + { + var user = Request.Query["username"]; + var password = Request.Query["password"]; + var result = UserMapper.ValidateUser(user, password); + var model = new ApiModel(); + if (result == null) + { + model.Error = true; + model.ErrorMessage = "Incorrect username or password"; + return ReturnReponse(model); + } + + var settings = SettingsService.GetSettings(); + model.Data = settings.ApiKey; + + return ReturnReponse(model); + } + + public Response ChangePassword(dynamic x) + { + var username = (string)x.username; + var userModel = this.BindAndValidate(); + + if (!ModelValidationResult.IsValid) + { + return ReturnValidationReponse(ModelValidationResult); + } + + var valid = UserMapper.ValidateUser(username, userModel.CurrentPassword); + if (valid == null) + { + var errorModel = new ApiModel { Error = true, ErrorMessage = "Incorrect username or password" }; + return ReturnReponse(errorModel); + } + var result = UserMapper.UpdatePassword(username, userModel.CurrentPassword, userModel.NewPassword); + + if (!result) + { + var errorModel = new ApiModel { Error = true, ErrorMessage = "Could not update the password. " }; + return ReturnReponse(errorModel); + } + + + var model = new ApiModel { Data = "Successfully updated the password"}; + return ReturnReponse(model); + } + } } \ No newline at end of file diff --git a/PlexRequests.UI/Modules/BaseApiModule.cs b/PlexRequests.UI/Modules/BaseApiModule.cs index 4ed4b8031..c7c1f4fc8 100644 --- a/PlexRequests.UI/Modules/BaseApiModule.cs +++ b/PlexRequests.UI/Modules/BaseApiModule.cs @@ -86,6 +86,10 @@ namespace PlexRequests.UI.Modules private Response CheckAuth() { + if (Request.Path.Contains("api/apikey")) // We do not need the apikey for this call + { + return null; + } var settings = Settings.GetSettings(); var apiModel = new ApiModel> { Data = new List() }; if (!Authenticated(settings)) diff --git a/PlexRequests.UI/Modules/LoginModule.cs b/PlexRequests.UI/Modules/LoginModule.cs index 14fbf49d0..326b7ec13 100644 --- a/PlexRequests.UI/Modules/LoginModule.cs +++ b/PlexRequests.UI/Modules/LoginModule.cs @@ -41,8 +41,9 @@ namespace PlexRequests.UI.Modules { public class LoginModule : BaseModule { - public LoginModule(ISettingsService pr) : base(pr) + public LoginModule(ISettingsService pr, ICustomUserMapper m) : base(pr) { + UserMapper = m; Get["/login"] = _ => { { @@ -110,6 +111,7 @@ namespace PlexRequests.UI.Modules Get["/changepassword"] = _ => ChangePassword(); Post["/changepassword"] = _ => ChangePasswordPost(); } + private ICustomUserMapper UserMapper { get; } private Negotiator ChangePassword() { diff --git a/PlexRequests.UI/Modules/UserManagementModule.cs b/PlexRequests.UI/Modules/UserManagementModule.cs index b2d8a6a2d..c93977cd7 100644 --- a/PlexRequests.UI/Modules/UserManagementModule.cs +++ b/PlexRequests.UI/Modules/UserManagementModule.cs @@ -13,13 +13,15 @@ namespace PlexRequests.UI.Modules { public class UserManagementModule : BaseModule { - public UserManagementModule(ISettingsService pr) : base("usermanagement",pr) + public UserManagementModule(ISettingsService pr, ICustomUserMapper m) : base("usermanagement",pr) { this.RequiresClaims(UserClaims.Admin); Get["/"] = x => Load(); Get["/users"] = x => LoadUsers(); + UserMapper = m; } + private ICustomUserMapper UserMapper { get; } private Negotiator Load() { diff --git a/PlexRequests.UI/PlexRequests.UI.csproj b/PlexRequests.UI/PlexRequests.UI.csproj index e74ce7fc1..67e2135b3 100644 --- a/PlexRequests.UI/PlexRequests.UI.csproj +++ b/PlexRequests.UI/PlexRequests.UI.csproj @@ -164,6 +164,7 @@ + @@ -172,6 +173,7 @@ + @@ -179,6 +181,7 @@ + diff --git a/PlexRequests.UI/Validators/UserViewModelValidator.cs b/PlexRequests.UI/Validators/UserViewModelValidator.cs new file mode 100644 index 000000000..0e4388adc --- /dev/null +++ b/PlexRequests.UI/Validators/UserViewModelValidator.cs @@ -0,0 +1,41 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: SonarrValidator.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 FluentValidation; + +using PlexRequests.UI.Models; + +namespace PlexRequests.UI.Validators +{ + public class UserViewModelValidator : AbstractValidator + { + public UserViewModelValidator() + { + RuleFor(request => request.CurrentPassword).NotEmpty().NotNull(); + RuleFor(request => request.NewPassword).NotEmpty().NotNull(); + } + } +} \ No newline at end of file From 915459a1412d7a605c340ddcfee12a903a6e2cb4 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Fri, 20 May 2016 11:52:52 +0100 Subject: [PATCH 60/76] #222 #205 more ! Started getting the settings out --- PlexRequests.UI.Tests/ApiModuleTests.cs | 72 +++++++++++- ...aModule.cs => ApiRequestMetadataModule.cs} | 6 +- .../{ApiModule.cs => ApiRequestModule.cs} | 61 +--------- .../Modules/ApiSettingsMetadataModule.cs | 60 ++++++++++ PlexRequests.UI/Modules/ApiSettingsModule.cs | 79 +++++++++++++ .../Modules/ApiUserMetadataModule.cs | 61 ++++++++++ PlexRequests.UI/Modules/ApiUserModule.cs | 104 ++++++++++++++++++ PlexRequests.UI/PlexRequests.UI.csproj | 8 +- 8 files changed, 388 insertions(+), 63 deletions(-) rename PlexRequests.UI/Modules/{ApiMetadataModule.cs => ApiRequestMetadataModule.cs} (97%) rename PlexRequests.UI/Modules/{ApiModule.cs => ApiRequestModule.cs} (71%) create mode 100644 PlexRequests.UI/Modules/ApiSettingsMetadataModule.cs create mode 100644 PlexRequests.UI/Modules/ApiSettingsModule.cs create mode 100644 PlexRequests.UI/Modules/ApiUserMetadataModule.cs create mode 100644 PlexRequests.UI/Modules/ApiUserModule.cs diff --git a/PlexRequests.UI.Tests/ApiModuleTests.cs b/PlexRequests.UI.Tests/ApiModuleTests.cs index 26d7dca52..05b443162 100644 --- a/PlexRequests.UI.Tests/ApiModuleTests.cs +++ b/PlexRequests.UI.Tests/ApiModuleTests.cs @@ -28,6 +28,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Runtime.InteropServices; using FluentValidation; @@ -44,6 +45,7 @@ using NUnit.Framework; using PlexRequests.Core; using PlexRequests.Core.SettingModels; +using PlexRequests.Helpers; using PlexRequests.Store; using PlexRequests.Store.Repository; using PlexRequests.UI.Models; @@ -68,6 +70,7 @@ namespace PlexRequests.UI.Tests var settingsMock = new Mock>(); var userRepoMock = new Mock>(); var mapperMock = new Mock(); + var authSettingsMock = new Mock>(); var userModels = fixture.CreateMany().ToList(); userModels.Add(new UsersModel @@ -87,13 +90,21 @@ namespace PlexRequests.UI.Tests mapperMock.Setup(x => x.ValidateUser("user1", It.IsAny())).Returns(Guid.NewGuid()); mapperMock.Setup(x => x.UpdatePassword("user1", "password", "newpassword")).Returns(true); + authSettingsMock.Setup(x => x.SaveSettings(It.Is(c => c.PlexAuthToken.Equals("abc")))).Returns(true); + Bootstrapper = new ConfigurableBootstrapper(with => { - with.Module(); + with.Module(); + with.Module(); + with.Module(); + with.Dependency(requestMock.Object); with.Dependency(settingsMock.Object); with.Dependency(userRepoMock.Object); with.Dependency(mapperMock.Object); + with.Dependency(authSettingsMock.Object); + + with.RootPathProvider(); with.ModelValidatorLocator( new DefaultValidatorLocator( @@ -364,5 +375,64 @@ namespace PlexRequests.UI.Tests Assert.That(body.Error, Is.True); Assert.That(body.ErrorMessage, Is.Not.Null.Or.Empty); } + + + [Test] + public void SaveNewAuthSettings() + { + var model = new AuthenticationSettings + { + Id = 1, + PlexAuthToken = "abc", + DeniedUsers = "abc", + UsePassword = false, + UserAuthentication = true + }; + var browser = new Browser(Bootstrapper); + var result = browser.Post("api/settings/authentication", with => + { + with.HttpRequest(); + with.Header("Accept", "application/json"); + with.Query("apikey", "api"); + with.JsonBody(model); + }); + + Assert.That(HttpStatusCode.OK, Is.EqualTo(result.StatusCode)); + + var body = JsonConvert.DeserializeObject>(result.Body.AsString()); + Assert.That(body.Data, Is.Not.Null.Or.Empty); + Assert.That(body.Error, Is.False); + Assert.That(body.ErrorMessage, Is.Null.Or.Empty); + } + + [TestCaseSource(nameof(AuthSettingsData))] + public object SaveNewAuthSettings(object model) + { + + var browser = new Browser(Bootstrapper); + var result = browser.Post("api/settings/authentication", with => + { + with.HttpRequest(); + with.Header("Accept", "application/json"); + with.Query("apikey", "api"); + with.JsonBody(model); + }); + + Assert.That(HttpStatusCode.OK, Is.EqualTo(result.StatusCode)); + var body = JsonConvert.DeserializeObject>(result.Body.AsString()); + + var retVal = new List { body.ErrorMessage, body.Error.ToString(), body.Data.ToString() }; + return retVal; + } + + private static IEnumerable AuthSettingsData + { + get + { + yield return + new TestCaseData(new AuthenticationSettings { Id = 1, PlexAuthToken = "abc", DeniedUsers = "abc", UsePassword = false, UserAuthentication = true }) + .Returns(new List { null, false.ToString(), true.ToString() }); + } + } } } \ No newline at end of file diff --git a/PlexRequests.UI/Modules/ApiMetadataModule.cs b/PlexRequests.UI/Modules/ApiRequestMetadataModule.cs similarity index 97% rename from PlexRequests.UI/Modules/ApiMetadataModule.cs rename to PlexRequests.UI/Modules/ApiRequestMetadataModule.cs index 6418ca158..732ad356e 100644 --- a/PlexRequests.UI/Modules/ApiMetadataModule.cs +++ b/PlexRequests.UI/Modules/ApiRequestMetadataModule.cs @@ -1,7 +1,7 @@ #region Copyright // /************************************************************************ // Copyright (c) 2016 Jamie Rees -// File: ApiMetadataModule.cs +// File: ApiRequestMetadataModule.cs // Created By: Jamie Rees // // Permission is hereby granted, free of charge, to any person obtaining @@ -34,9 +34,9 @@ using PlexRequests.UI.Models; namespace PlexRequests.UI.Modules { - public class ApiMetadataModule: MetadataModule + public class ApiRequestMetadataModule: MetadataModule { - public ApiMetadataModule() + public ApiRequestMetadataModule() { Describe["GetRequests"] = description => description.AsSwagger(with => { diff --git a/PlexRequests.UI/Modules/ApiModule.cs b/PlexRequests.UI/Modules/ApiRequestModule.cs similarity index 71% rename from PlexRequests.UI/Modules/ApiModule.cs rename to PlexRequests.UI/Modules/ApiRequestModule.cs index 441d32485..199cb9814 100644 --- a/PlexRequests.UI/Modules/ApiModule.cs +++ b/PlexRequests.UI/Modules/ApiRequestModule.cs @@ -1,7 +1,7 @@ #region Copyright // /************************************************************************ // Copyright (c) 2016 Jamie Rees -// File: ApiModule.cs +// File: ApiRequestModule.cs // Created By: Jamie Rees // // Permission is hereby granted, free of charge, to any person obtaining @@ -37,9 +37,9 @@ using PlexRequests.UI.Models; namespace PlexRequests.UI.Modules { - public class ApiModule : BaseApiModule + public class ApiRequestModule : BaseApiModule { - public ApiModule(IRequestService service, ISettingsService pr, ICustomUserMapper m) : base("api", pr) + public ApiRequestModule(IRequestService service, ISettingsService pr) : base("api", pr) { Get["GetRequests","/requests"] = x => GetRequests(); Get["GetRequest","/requests/{id}"] = x => GetSingleRequests(x); @@ -47,18 +47,13 @@ namespace PlexRequests.UI.Modules Put["PutRequests", "/requests"] = x => UpdateRequest(); Delete["DeleteRequests", "/requests/{id}"] = x => DeleteRequest(x); - Get["GetApiKey", "/apikey"] = x => GetApiKey(); - - Put["PutCredentials", "/credentials/{username}"] = x => ChangePassword(x); - + RequestService = service; SettingsService = pr; - UserMapper = m; } private IRequestService RequestService { get; } private ISettingsService SettingsService { get; } - private ICustomUserMapper UserMapper { get; } public Response GetRequests() { @@ -164,54 +159,6 @@ namespace PlexRequests.UI.Modules } } - public Response GetApiKey() - { - var user = Request.Query["username"]; - var password = Request.Query["password"]; - var result = UserMapper.ValidateUser(user, password); - var model = new ApiModel(); - if (result == null) - { - model.Error = true; - model.ErrorMessage = "Incorrect username or password"; - return ReturnReponse(model); - } - - var settings = SettingsService.GetSettings(); - model.Data = settings.ApiKey; - - return ReturnReponse(model); - } - - public Response ChangePassword(dynamic x) - { - var username = (string)x.username; - var userModel = this.BindAndValidate(); - - if (!ModelValidationResult.IsValid) - { - return ReturnValidationReponse(ModelValidationResult); - } - - var valid = UserMapper.ValidateUser(username, userModel.CurrentPassword); - if (valid == null) - { - var errorModel = new ApiModel { Error = true, ErrorMessage = "Incorrect username or password" }; - return ReturnReponse(errorModel); - } - var result = UserMapper.UpdatePassword(username, userModel.CurrentPassword, userModel.NewPassword); - - if (!result) - { - var errorModel = new ApiModel { Error = true, ErrorMessage = "Could not update the password. " }; - return ReturnReponse(errorModel); - } - - - var model = new ApiModel { Data = "Successfully updated the password"}; - return ReturnReponse(model); - } - } } \ No newline at end of file diff --git a/PlexRequests.UI/Modules/ApiSettingsMetadataModule.cs b/PlexRequests.UI/Modules/ApiSettingsMetadataModule.cs new file mode 100644 index 000000000..ee12514a8 --- /dev/null +++ b/PlexRequests.UI/Modules/ApiSettingsMetadataModule.cs @@ -0,0 +1,60 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: ApiSettingsMetadataModule.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 Nancy.Metadata.Modules; +using Nancy.Swagger; + +using PlexRequests.Core.SettingModels; + +namespace PlexRequests.UI.Modules +{ + public class ApiSettingsMetadataModule: MetadataModule + { + public ApiSettingsMetadataModule() + { + Describe["GetAuthSettings"] = description => description.AsSwagger(with => + { + with.ResourcePath("/settings/authentication"); + with.Summary("Gets the authentication settings saved in the application"); + with.Model>(); + with.Notes("Gets the authentication settings saved in the application"); + + with.QueryParam("apikey", "The Api Key found in the settings", true); + }); + + Describe["PostAuthSettings"] = description => description.AsSwagger(with => + { + with.ResourcePath("/settings/authentication"); + with.Summary("Saves the authentication settings saved in the application"); + with.Model>(); + with.QueryParam("apikey", "The Api Key found in the settings", true); + with.BodyParam("Authentication settings", true); + with.Notes("Saves the authentication settings saved in the application"); + }); + + } + } +} \ No newline at end of file diff --git a/PlexRequests.UI/Modules/ApiSettingsModule.cs b/PlexRequests.UI/Modules/ApiSettingsModule.cs new file mode 100644 index 000000000..f0f50eb07 --- /dev/null +++ b/PlexRequests.UI/Modules/ApiSettingsModule.cs @@ -0,0 +1,79 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: ApiModule.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 Nancy; +using Nancy.ModelBinding; + +using PlexRequests.Core; +using PlexRequests.Core.SettingModels; + +namespace PlexRequests.UI.Modules +{ + public class ApiSettingsModule : BaseApiModule + { + public ApiSettingsModule(ISettingsService pr, ISettingsService auth) : base("api", pr) + { + Get["GetAuthSettings","/settings/authentication"] = x => GetAuthSettings(); + Post["PostAuthSettings","/settings/authentication"] = x => PostAuthSettings(); + + SettingsService = pr; + AuthSettings = auth; + } + + private ISettingsService SettingsService { get; } + private ISettingsService AuthSettings { get; } + + public Response GetAuthSettings() + { + var model = new ApiModel(); + var settings = AuthSettings.GetSettings(); + model.Data = settings; + return ReturnReponse(model); + } + + public Response PostAuthSettings() + { + var newSettings = this.BindAndValidate(); + if (!ModelValidationResult.IsValid) + { + return ReturnValidationReponse(ModelValidationResult); + } + + var model = new ApiModel(); + var settings = AuthSettings.SaveSettings(newSettings); + if (settings) + { + model.Data = true; + return ReturnReponse(model); + } + + model.Error = true; + model.ErrorMessage = "Could not update the settings"; + return ReturnReponse(model); + } + + } +} \ No newline at end of file diff --git a/PlexRequests.UI/Modules/ApiUserMetadataModule.cs b/PlexRequests.UI/Modules/ApiUserMetadataModule.cs new file mode 100644 index 000000000..b581e7bcd --- /dev/null +++ b/PlexRequests.UI/Modules/ApiUserMetadataModule.cs @@ -0,0 +1,61 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: ApiUserMetadataModule.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 Nancy.Metadata.Modules; +using Nancy.Swagger; + +using PlexRequests.UI.Models; + +namespace PlexRequests.UI.Modules +{ + public class ApiUserMetadataModule: MetadataModule + { + public ApiUserMetadataModule() + { + Describe["GetApiKey"] = description => description.AsSwagger(with => + { + with.ResourcePath("/apikey"); + with.Summary("Gets the Api Key for Plex Requests"); + with.Model>(); + with.QueryParam("username", required:true ); + with.QueryParam("password", required: true ); + with.Notes("Get's the current api key for the application"); + }); + + Describe["PutCredentials"] = description => description.AsSwagger(with => + { + with.ResourcePath("/credentials/{username}"); + with.Summary("Sets a new password for the user"); + with.Model>(); + with.PathParam("username", required:true); + with.QueryParam("apikey", "The Api Key found in the settings", true); + with.BodyParam("User update view model", true); + with.Notes("Sets a new password for the user"); + }); + + } + } +} \ No newline at end of file diff --git a/PlexRequests.UI/Modules/ApiUserModule.cs b/PlexRequests.UI/Modules/ApiUserModule.cs new file mode 100644 index 000000000..545558401 --- /dev/null +++ b/PlexRequests.UI/Modules/ApiUserModule.cs @@ -0,0 +1,104 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: ApiModule.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion +using System; +using System.Collections.Generic; + +using Nancy; +using Nancy.ModelBinding; + +using PlexRequests.Core; +using PlexRequests.Core.SettingModels; +using PlexRequests.Store; +using PlexRequests.UI.Models; + +namespace PlexRequests.UI.Modules +{ + public class ApiUserModule : BaseApiModule + { + public ApiUserModule(ISettingsService pr, ICustomUserMapper m) : base("api", pr) + { + + Put["PutCredentials", "/credentials/{username}"] = x => ChangePassword(x); + + Get["GetApiKey", "/apikey"] = x => GetApiKey(); + + SettingsService = pr; + UserMapper = m; + } + + private ISettingsService SettingsService { get; } + private ICustomUserMapper UserMapper { get; } + + public Response ChangePassword(dynamic x) + { + var username = (string)x.username; + var userModel = this.BindAndValidate(); + + if (!ModelValidationResult.IsValid) + { + return ReturnValidationReponse(ModelValidationResult); + } + + var valid = UserMapper.ValidateUser(username, userModel.CurrentPassword); + if (valid == null) + { + var errorModel = new ApiModel { Error = true, ErrorMessage = "Incorrect username or password" }; + return ReturnReponse(errorModel); + } + var result = UserMapper.UpdatePassword(username, userModel.CurrentPassword, userModel.NewPassword); + + if (!result) + { + var errorModel = new ApiModel { Error = true, ErrorMessage = "Could not update the password. " }; + return ReturnReponse(errorModel); + } + + + var model = new ApiModel { Data = "Successfully updated the password"}; + return ReturnReponse(model); + } + public Response GetApiKey() + { + var user = Request.Query["username"]; + var password = Request.Query["password"]; + var result = UserMapper.ValidateUser(user, password); + var model = new ApiModel(); + if (result == null) + { + model.Error = true; + model.ErrorMessage = "Incorrect username or password"; + return ReturnReponse(model); + } + + var settings = SettingsService.GetSettings(); + model.Data = settings.ApiKey; + + return ReturnReponse(model); + } + + } +} \ No newline at end of file diff --git a/PlexRequests.UI/PlexRequests.UI.csproj b/PlexRequests.UI/PlexRequests.UI.csproj index 67e2135b3..bd57c59b5 100644 --- a/PlexRequests.UI/PlexRequests.UI.csproj +++ b/PlexRequests.UI/PlexRequests.UI.csproj @@ -175,7 +175,11 @@ - + + + + + @@ -279,7 +283,7 @@ Always - + From 0601f045824ab76d86fc7cf2614f8a903f901db4 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Fri, 20 May 2016 13:07:24 +0100 Subject: [PATCH 61/76] I think we have finished the main bulk of the auto updater #29 --- PlexRequests.UI/Bootstrapper.cs | 12 +++++--- PlexRequests.UI/PlexRequests.UI.csproj | 1 + PlexRequests.UI/Program.cs | 39 ++++++++++++++++++++++---- PlexRequests.UI/Start/UpdateValue.cs | 8 +++--- 4 files changed, 47 insertions(+), 13 deletions(-) diff --git a/PlexRequests.UI/Bootstrapper.cs b/PlexRequests.UI/Bootstrapper.cs index c2e31d5dd..4966373c3 100644 --- a/PlexRequests.UI/Bootstrapper.cs +++ b/PlexRequests.UI/Bootstrapper.cs @@ -68,9 +68,7 @@ namespace PlexRequests.UI protected override void ConfigureRequestContainer(TinyIoCContainer container, NancyContext context) { - container.Register(); - container.Register(); - container.Register(new DbConfiguration(new SqliteFactory())); + container.Register().AsSingleton(); // Settings @@ -88,7 +86,6 @@ namespace PlexRequests.UI // Repo's container.Register, GenericRepository>(); - container.Register, UserRepository>(); container.Register, GenericRepository>(); container.Register(); container.Register(); @@ -121,9 +118,16 @@ namespace PlexRequests.UI var loc = ServiceLocator.Instance; loc.SetContainer(container); } + protected override void ApplicationStartup(TinyIoCContainer container, IPipelines pipelines) { + container.Register(new DbConfiguration(new SqliteFactory())); + container.Register, UserRepository>(); + container.Register(); + container.Register(); + + CookieBasedSessions.Enable(pipelines, CryptographyConfiguration.Default); StaticConfiguration.DisableErrorTraces = false; diff --git a/PlexRequests.UI/PlexRequests.UI.csproj b/PlexRequests.UI/PlexRequests.UI.csproj index bd57c59b5..f9058cdb2 100644 --- a/PlexRequests.UI/PlexRequests.UI.csproj +++ b/PlexRequests.UI/PlexRequests.UI.csproj @@ -80,6 +80,7 @@ + ..\packages\CommandLineParser.2.0.275-beta\lib\net45\CommandLine.dll diff --git a/PlexRequests.UI/Program.cs b/PlexRequests.UI/Program.cs index fd7688ecc..1bb0da24c 100644 --- a/PlexRequests.UI/Program.cs +++ b/PlexRequests.UI/Program.cs @@ -39,6 +39,9 @@ using PlexRequests.Helpers; using PlexRequests.Store; using PlexRequests.Store.Repository; using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Windows.Forms; using CommandLine; @@ -62,10 +65,10 @@ namespace PlexRequests.UI e => -1); var updated = result.MapResult(x => x.Updated, e => UpdateValue.None); - //TODO - + CheckUpdate(updated); + PrintToConsole("Starting Up! Please wait, this can usually take a few seconds.", ConsoleColor.Yellow); - + Log.Trace("Getting product version"); WriteOutVersion(); @@ -163,11 +166,37 @@ namespace PlexRequests.UI { if (val == UpdateValue.Failed) { - + PrintToConsole("Update Failed", ConsoleColor.Red); } if (val == UpdateValue.Updated) { - // TODO Change the name of PlexRequests.Updater.exe_Updated and delete the old version + PrintToConsole("Finishing Update", ConsoleColor.Yellow); + var applicationPath = Path.Combine(Path.GetDirectoryName(Application.ExecutablePath)); + var files = Directory.GetFiles(applicationPath, "PlexRequests.*", SearchOption.TopDirectoryOnly); + var oldUpdater = files.FirstOrDefault(x => x == $"{applicationPath}\\PlexRequests.Updater.exe"); + var newUpdater = files.FirstOrDefault(x => x == $"{applicationPath}\\PlexRequests.Updater.exe_Updated"); + + if (oldUpdater == null || newUpdater == null) + { + PrintToConsole("Looks like there was nothing to update.", ConsoleColor.Yellow); + return; + } + + try + { + File.Copy(oldUpdater, "PlexRequests.Updater.exe_Old", true); + File.Delete(oldUpdater); + File.Copy(newUpdater, "PlexRequests.Updater.exe", true); + File.Delete(newUpdater); + + File.Delete("PlexRequests.Updater.exe_Old"); // Cleanup + } + catch (Exception e) + { + Console.WriteLine(e.Message); + } + + PrintToConsole("Finished Update!", ConsoleColor.Yellow); } } } diff --git a/PlexRequests.UI/Start/UpdateValue.cs b/PlexRequests.UI/Start/UpdateValue.cs index 5c19d8f7c..0a994ed99 100644 --- a/PlexRequests.UI/Start/UpdateValue.cs +++ b/PlexRequests.UI/Start/UpdateValue.cs @@ -26,10 +26,10 @@ #endregion namespace PlexRequests.UI.Start { - public enum UpdateValue + public enum UpdateValue : int { - None, - Updated, - Failed + None = 0, + Updated = 1, + Failed = 2 } } \ No newline at end of file From aa2949cdd0c4cb7a44f3496d3af33ce5137a074c Mon Sep 17 00:00:00 2001 From: tidusjar Date: Fri, 20 May 2016 13:51:25 +0100 Subject: [PATCH 62/76] I think the auto updater is finished! #29 --- PlexRequests.UI/Modules/AdminModule.cs | 15 ++++++- .../Views/Admin/Authentication.cshtml | 2 +- PlexRequests.UI/Views/Admin/Status.cshtml | 39 +++++++++++++++++-- PlexRequests.Updater/Updater.cs | 6 ++- 4 files changed, 56 insertions(+), 6 deletions(-) diff --git a/PlexRequests.UI/Modules/AdminModule.cs b/PlexRequests.UI/Modules/AdminModule.cs index 62348a269..e1673c5e0 100644 --- a/PlexRequests.UI/Modules/AdminModule.cs +++ b/PlexRequests.UI/Modules/AdminModule.cs @@ -54,6 +54,7 @@ using PlexRequests.Store.Repository; using PlexRequests.UI.Helpers; using PlexRequests.UI.Models; using System; +using System.Diagnostics; using Nancy.Json; using Nancy.Security; @@ -172,7 +173,9 @@ namespace PlexRequests.UI.Modules Get["/headphones"] = _ => Headphones(); Post["/headphones"] = _ => SaveHeadphones(); - Post ["/createapikey"] = x => CreateApiKey (); + Post["/createapikey"] = x => CreateApiKey(); + + Post["/autoupdate"] = x => AutoUpdate(); } private Negotiator Authentication() @@ -496,6 +499,16 @@ namespace PlexRequests.UI.Modules return View["Status", status]; } + private Response AutoUpdate() + { + var url = Request.Form["url"]; + var startInfo = new ProcessStartInfo("PlexRequests.Updater.exe") { Arguments = url}; + Process.Start(startInfo); + + Environment.Exit(0); + return Nancy.Response.NoBody; + } + private Negotiator PushbulletNotifications() { var settings = PushbulletService.GetSettings(); diff --git a/PlexRequests.UI/Views/Admin/Authentication.cshtml b/PlexRequests.UI/Views/Admin/Authentication.cshtml index 1abbd3788..129220cd6 100644 --- a/PlexRequests.UI/Views/Admin/Authentication.cshtml +++ b/PlexRequests.UI/Views/Admin/Authentication.cshtml @@ -126,7 +126,7 @@ var $form = $("#mainForm"); $.ajax({ type: $form.prop("method"), - url: "requestauth", + url: url, data: $form.serialize(), dataType: "json", success: function (response) { diff --git a/PlexRequests.UI/Views/Admin/Status.cshtml b/PlexRequests.UI/Views/Admin/Status.cshtml index 05dbf1faf..eb5f1b743 100644 --- a/PlexRequests.UI/Views/Admin/Status.cshtml +++ b/PlexRequests.UI/Views/Admin/Status.cshtml @@ -1,4 +1,5 @@ -@Html.Partial("_Sidebar") +@using PlexRequests.UI.Helpers +@Html.Partial("_Sidebar")
    @@ -15,6 +16,8 @@ @if (Model.UpdateAvailable) { +
    + } else { @@ -28,11 +31,41 @@

    @Model.ReleaseTitle

    -
    +
    @Html.Raw(Model.ReleaseNotes) }
    -
    \ No newline at end of file +
    + + \ No newline at end of file diff --git a/PlexRequests.Updater/Updater.cs b/PlexRequests.Updater/Updater.cs index fce211c22..43b44007c 100644 --- a/PlexRequests.Updater/Updater.cs +++ b/PlexRequests.Updater/Updater.cs @@ -192,7 +192,11 @@ namespace PlexRequests.Updater private void FinishUpdate() { - var startInfo = new ProcessStartInfo("PlexRequests.exe") { Arguments = Error ? "-u 2" : "-u 1" }; + ProcessStartInfo startInfo; + startInfo = Type.GetType("Mono.Runtime") != null + ? new ProcessStartInfo("mono PlexRequests.exe") { Arguments = Error ? "-u 2" : "-u 1" } + : new ProcessStartInfo("PlexRequests.exe") { Arguments = Error ? "-u 2" : "-u 1" }; + Process.Start(startInfo); Environment.Exit(0); From db35897f050beab01d55fb1d8f8cda87d8cbf38c Mon Sep 17 00:00:00 2001 From: tidusjar Date: Fri, 20 May 2016 16:00:38 +0100 Subject: [PATCH 63/76] Styling for #27 --- .../Content/awesome-bootstrap-checkbox.css | 266 ++++++++++++++++++ .../Content/awesome-bootstrap-checkbox.scss | 250 ++++++++++++++++ PlexRequests.UI/Content/custom.css | 56 ++++ PlexRequests.UI/Content/custom.min.css | 2 +- PlexRequests.UI/Content/custom.scss | 64 ++++- PlexRequests.UI/Helpers/BaseUrlHelper.cs | 1 + PlexRequests.UI/Modules/AdminModule.cs | 2 +- PlexRequests.UI/PlexRequests.UI.csproj | 6 + .../Views/Admin/Authentication.cshtml | 16 +- .../Views/Admin/CouchPotato.cshtml | 16 +- .../Views/Admin/EmailNotifications.cshtml | 24 +- PlexRequests.UI/Views/Admin/Headphones.cshtml | 16 +- PlexRequests.UI/Views/Admin/Plex.cshtml | 8 +- .../Admin/PushbulletNotifications.cshtml | 8 +- .../Views/Admin/PushoverNotifications.cshtml | 8 +- PlexRequests.UI/Views/Admin/Settings.cshtml | 109 ++++--- PlexRequests.UI/Views/Admin/Sickrage.cshtml | 13 +- PlexRequests.UI/Views/Admin/Sonarr.cshtml | 18 +- PlexRequests.UI/Views/Login/Index.cshtml | 17 +- PlexRequests.UI/Views/Search/Index.cshtml | 54 +++- PlexRequests.UI/Views/UserLogin/Index.cshtml | 5 +- PlexRequests.Updater/Updater.cs | 2 +- 22 files changed, 813 insertions(+), 148 deletions(-) create mode 100644 PlexRequests.UI/Content/awesome-bootstrap-checkbox.css create mode 100644 PlexRequests.UI/Content/awesome-bootstrap-checkbox.scss diff --git a/PlexRequests.UI/Content/awesome-bootstrap-checkbox.css b/PlexRequests.UI/Content/awesome-bootstrap-checkbox.css new file mode 100644 index 000000000..b78e37a09 --- /dev/null +++ b/PlexRequests.UI/Content/awesome-bootstrap-checkbox.css @@ -0,0 +1,266 @@ +@charset "UTF-8"; +.abc-checkbox { + padding-left: 20px; } +.abc-checkbox label { + display: inline-block; + vertical-align: middle; + position: relative; + padding-left: 5px; } +.abc-checkbox label::before { + cursor: pointer; + content: ""; + display: inline-block; + position: absolute; + width: 17px; + height: 17px; + left: 0; + margin-left: -20px; + border: 1px solid #ccc; + border-radius: 3px; + background-color: #fff; } +.abc-checkbox label::after { + cursor: pointer; + display: inline-block; + position: absolute; + width: 16px; + height: 16px; + left: 0; + top: 0; + margin-left: -20px; + padding-left: 3px; + padding-top: 1px; + font-size: 11px; + color: #55595c; } +.abc-checkbox input[type="checkbox"], +.abc-checkbox input[type="radio"] { + cursor: pointer; + opacity: 0; + z-index: 1; } +.abc-checkbox input[type="checkbox"]:focus + label::before, +.abc-checkbox input[type="radio"]:focus + label::before { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; } +.abc-checkbox input[type="checkbox"]:checked + label::after, +.abc-checkbox input[type="radio"]:checked + label::after { + font-family: "FontAwesome"; + content: ""; } +.abc-checkbox input[type="checkbox"]:indeterminate + label::after, +.abc-checkbox input[type="radio"]:indeterminate + label::after { + display: block; + content: ""; + width: 10px; + height: 3px; + background-color: #555555; + border-radius: 2px; + margin-left: -16.5px; + margin-top: 7px; } +.abc-checkbox input[type="checkbox"]:disabled + label, +.abc-checkbox input[type="radio"]:disabled + label { + opacity: 0.65; } +.abc-checkbox input[type="checkbox"]:disabled + label::before, +.abc-checkbox input[type="radio"]:disabled + label::before { + background-color: #eceeef; + cursor: not-allowed; } +.abc-checkbox.abc-checkbox-circle label::before { + border-radius: 50%; } +.abc-checkbox.checkbox-inline { + margin-top: 0; } + +.abc-checkbox-primary input[type="checkbox"]:checked + label::before, +.abc-checkbox-primary input[type="radio"]:checked + label::before { + background-color: #0275d8; + border-color: #0275d8; } + +.abc-checkbox-primary input[type="checkbox"]:checked + label::after, +.abc-checkbox-primary input[type="radio"]:checked + label::after { + color: #fff; } + +.abc-checkbox-danger input[type="checkbox"]:checked + label::before, +.abc-checkbox-danger input[type="radio"]:checked + label::before { + background-color: #d9534f; + border-color: #d9534f; } + +.abc-checkbox-danger input[type="checkbox"]:checked + label::after, +.abc-checkbox-danger input[type="radio"]:checked + label::after { + color: #fff; } + +.abc-checkbox-info input[type="checkbox"]:checked + label::before, +.abc-checkbox-info input[type="radio"]:checked + label::before { + background-color: #5bc0de; + border-color: #5bc0de; } + +.abc-checkbox-info input[type="checkbox"]:checked + label::after, +.abc-checkbox-info input[type="radio"]:checked + label::after { + color: #fff; } + +.abc-checkbox-warning input[type="checkbox"]:checked + label::before, +.abc-checkbox-warning input[type="radio"]:checked + label::before { + background-color: #f0ad4e; + border-color: #f0ad4e; } + +.abc-checkbox-warning input[type="checkbox"]:checked + label::after, +.abc-checkbox-warning input[type="radio"]:checked + label::after { + color: #fff; } + +.abc-checkbox-success input[type="checkbox"]:checked + label::before, +.abc-checkbox-success input[type="radio"]:checked + label::before { + background-color: #5cb85c; + border-color: #5cb85c; } + +.abc-checkbox-success input[type="checkbox"]:checked + label::after, +.abc-checkbox-success input[type="radio"]:checked + label::after { + color: #fff; } + +.abc-checkbox-primary input[type="checkbox"]:indeterminate + label::before, +.abc-checkbox-primary input[type="radio"]:indeterminate + label::before { + background-color: #0275d8; + border-color: #0275d8; } + +.abc-checkbox-primary input[type="checkbox"]:indeterminate + label::after, +.abc-checkbox-primary input[type="radio"]:indeterminate + label::after { + background-color: #fff; } + +.abc-checkbox-danger input[type="checkbox"]:indeterminate + label::before, +.abc-checkbox-danger input[type="radio"]:indeterminate + label::before { + background-color: #d9534f; + border-color: #d9534f; } + +.abc-checkbox-danger input[type="checkbox"]:indeterminate + label::after, +.abc-checkbox-danger input[type="radio"]:indeterminate + label::after { + background-color: #fff; } + +.abc-checkbox-info input[type="checkbox"]:indeterminate + label::before, +.abc-checkbox-info input[type="radio"]:indeterminate + label::before { + background-color: #5bc0de; + border-color: #5bc0de; } + +.abc-checkbox-info input[type="checkbox"]:indeterminate + label::after, +.abc-checkbox-info input[type="radio"]:indeterminate + label::after { + background-color: #fff; } + +.abc-checkbox-warning input[type="checkbox"]:indeterminate + label::before, +.abc-checkbox-warning input[type="radio"]:indeterminate + label::before { + background-color: #f0ad4e; + border-color: #f0ad4e; } + +.abc-checkbox-warning input[type="checkbox"]:indeterminate + label::after, +.abc-checkbox-warning input[type="radio"]:indeterminate + label::after { + background-color: #fff; } + +.abc-checkbox-success input[type="checkbox"]:indeterminate + label::before, +.abc-checkbox-success input[type="radio"]:indeterminate + label::before { + background-color: #5cb85c; + border-color: #5cb85c; } + +.abc-checkbox-success input[type="checkbox"]:indeterminate + label::after, +.abc-checkbox-success input[type="radio"]:indeterminate + label::after { + background-color: #fff; } + +.abc-radio { + padding-left: 20px; } +.abc-radio label { + display: inline-block; + vertical-align: middle; + position: relative; + padding-left: 5px; } +.abc-radio label::before { + content: ""; + cursor: pointer; + display: inline-block; + position: absolute; + width: 17px; + height: 17px; + left: 0; + margin-left: -20px; + border: 1px solid #ccc; + border-radius: 50%; + background-color: #fff; } +.abc-radio label::after { + cursor: pointer; + display: inline-block; + position: absolute; + content: " "; + width: 11px; + height: 11px; + left: 3px; + top: 3px; + margin-left: -20px; + border-radius: 50%; + background-color: #55595c; + transform: scale(0, 0); + transition: transform 0.1s cubic-bezier(0.8, -0.33, 0.2, 1.33); } +.abc-radio input[type="radio"] { + cursor: pointer; + opacity: 0; + z-index: 1; } +.abc-radio input[type="radio"]:focus + label::before { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; } +.abc-radio input[type="radio"]:checked + label::after { + transform: scale(1, 1); } +.abc-radio input[type="radio"]:disabled + label { + opacity: 0.65; } +.abc-radio input[type="radio"]:disabled + label::before { + cursor: not-allowed; } +.abc-radio.radio-inline { + margin-top: 0; } + +.abc-radio-primary input[type="radio"] + label::after { + background-color: #0275d8; } + +.abc-radio-primary input[type="radio"]:checked + label::before { + border-color: #0275d8; } + +.abc-radio-primary input[type="radio"]:checked + label::after { + background-color: #0275d8; } + +.abc-radio-danger input[type="radio"] + label::after { + background-color: #d9534f; } + +.abc-radio-danger input[type="radio"]:checked + label::before { + border-color: #d9534f; } + +.abc-radio-danger input[type="radio"]:checked + label::after { + background-color: #d9534f; } + +.abc-radio-info input[type="radio"] + label::after { + background-color: #5bc0de; } + +.abc-radio-info input[type="radio"]:checked + label::before { + border-color: #5bc0de; } + +.abc-radio-info input[type="radio"]:checked + label::after { + background-color: #5bc0de; } + +.abc-radio-warning input[type="radio"] + label::after { + background-color: #f0ad4e; } + +.abc-radio-warning input[type="radio"]:checked + label::before { + border-color: #f0ad4e; } + +.abc-radio-warning input[type="radio"]:checked + label::after { + background-color: #f0ad4e; } + +.abc-radio-success input[type="radio"] + label::after { + background-color: #5cb85c; } + +.abc-radio-success input[type="radio"]:checked + label::before { + border-color: #5cb85c; } + +.abc-radio-success input[type="radio"]:checked + label::after { + background-color: #5cb85c; } + +input[type="checkbox"].styled:checked + label:after, +input[type="radio"].styled:checked + label:after { + font-family: "FontAwesome"; + content: ""; } + +input[type="checkbox"] .styled:checked + label::before, +input[type="radio"] .styled:checked + label::before { + color: #fff; } + +input[type="checkbox"] .styled:checked + label::after, +input[type="radio"] .styled:checked + label::after { + color: #fff; } diff --git a/PlexRequests.UI/Content/awesome-bootstrap-checkbox.scss b/PlexRequests.UI/Content/awesome-bootstrap-checkbox.scss new file mode 100644 index 000000000..464893ddb --- /dev/null +++ b/PlexRequests.UI/Content/awesome-bootstrap-checkbox.scss @@ -0,0 +1,250 @@ + +// +// Checkboxes +// -------------------------------------------------- + + +$font-family-icon: 'FontAwesome' !default; +$fa-var-check: "\f00c" !default; +$check-icon: $fa-var-check !default; + +@mixin checkbox-variant($parent, $color) { + #{$parent} input[type="checkbox"]:checked + label, + #{$parent} input[type="radio"]:checked + label { + &::before { + background-color: $color; + border-color: $color; + } + &::after{ + color: #fff; + } + } +} + +@mixin checkbox-variant-indeterminate($parent, $color) { + #{$parent} input[type="checkbox"]:indeterminate + label, + #{$parent} input[type="radio"]:indeterminate + label { + &::before { + background-color: $color; + border-color: $color; + } + &::after{ + background-color: #fff; + } + } +} + +.abc-checkbox{ + padding-left: 20px; + + label{ + display: inline-block; + vertical-align: middle; + position: relative; + padding-left: 5px; + + &::before{ + cursor: pointer; + content: ""; + display: inline-block; + position: absolute; + width: 17px; + height: 17px; + left: 0; + margin-left: -20px; + border: 1px solid $input-border-color; + border-radius: 3px; + background-color: #fff; + @include transition(border 0.15s ease-in-out, color 0.15s ease-in-out); + } + + &::after{ + cursor: pointer; + display: inline-block; + position: absolute; + width: 16px; + height: 16px; + left: 0; + top: 0; + margin-left: -20px; + padding-left: 3px; + padding-top: 1px; + font-size: 11px; + color: $input-color; + } + } + + input[type="checkbox"], + input[type="radio"] { + cursor: pointer; + opacity: 0; + z-index: 1; + + &:focus + label::before{ + @include tab-focus(); + } + + &:checked + label::after{ + font-family: $font-family-icon; + content: $check-icon; + } + + &:indeterminate + label::after{ + display: block; + content: ""; + width: 10px; + height: 3px; + background-color: #555555; + border-radius: 2px; + margin-left: -16.5px; + margin-top: 7px; + } + + &:disabled + label{ + opacity: 0.65; + + &::before{ + background-color: $input-bg-disabled; + cursor: not-allowed; + } + } + + } + + &.abc-checkbox-circle label::before{ + border-radius: 50%; + } + + &.checkbox-inline{ + margin-top: 0; + } +} + +@include checkbox-variant('.abc-checkbox-primary', $brand-primary); +@include checkbox-variant('.abc-checkbox-danger', $brand-danger); +@include checkbox-variant('.abc-checkbox-info', $brand-info); +@include checkbox-variant('.abc-checkbox-warning', $brand-warning); +@include checkbox-variant('.abc-checkbox-success', $brand-success); + + +@include checkbox-variant-indeterminate('.abc-checkbox-primary', $brand-primary); +@include checkbox-variant-indeterminate('.abc-checkbox-danger', $brand-danger); +@include checkbox-variant-indeterminate('.abc-checkbox-info', $brand-info); +@include checkbox-variant-indeterminate('.abc-checkbox-warning', $brand-warning); +@include checkbox-variant-indeterminate('.abc-checkbox-success', $brand-success); + +// +// Radios +// -------------------------------------------------- + +@mixin radio-variant($parent, $color) { + #{$parent} input[type="radio"]{ + + label{ + &::after{ + background-color: $color; + } + } + &:checked + label{ + &::before { + border-color: $color; + } + &::after{ + background-color: $color; + } + } + } +} + +.abc-radio{ + padding-left: 20px; + + label{ + display: inline-block; + vertical-align: middle; + position: relative; + padding-left: 5px; + + &::before{ + cursor: pointer; + content: ""; + display: inline-block; + position: absolute; + width: 17px; + height: 17px; + left: 0; + margin-left: -20px; + border: 1px solid $input-border-color; + border-radius: 50%; + background-color: #fff; + @include transition(border 0.15s ease-in-out); + } + + &::after{ + cursor: pointer; + display: inline-block; + position: absolute; + content: " "; + width: 11px; + height: 11px; + left: 3px; + top: 3px; + margin-left: -20px; + border-radius: 50%; + background-color: $input-color; + transform: scale(0, 0); + + transition: transform .1s cubic-bezier(.8,-0.33,.2,1.33); + //curve - http://cubic-bezier.com/#.8,-0.33,.2,1.33 + } + } + + input[type="radio"]{ + cursor: pointer; + opacity: 0; + z-index: 1; + + &:focus + label::before{ + @include tab-focus(); + } + + &:checked + label::after{ + transform: scale(1, 1); + } + + &:disabled + label{ + opacity: 0.65; + + &::before{ + cursor: not-allowed; + } + } + + } + + &.radio-inline{ + margin-top: 0; + } +} + +@include radio-variant('.abc-radio-primary', $brand-primary); +@include radio-variant('.abc-radio-danger', $brand-danger); +@include radio-variant('.abc-radio-info', $brand-info); +@include radio-variant('.abc-radio-warning', $brand-warning); +@include radio-variant('.abc-radio-success', $brand-success); + + +input[type="checkbox"], +input[type="radio"] { + &.styled:checked + label:after { + font-family: $font-family-icon; + content: $check-icon; + } + .styled:checked + label { + &::before { + color: #fff; + } + &::after { + color: #fff; + } + } +} diff --git a/PlexRequests.UI/Content/custom.css b/PlexRequests.UI/Content/custom.css index ccdecf826..5d18f3b4c 100644 --- a/PlexRequests.UI/Content/custom.css +++ b/PlexRequests.UI/Content/custom.css @@ -42,11 +42,28 @@ label { margin-bottom: 0.5rem !important; font-size: 16px !important; } +.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%; @@ -225,3 +242,42 @@ label { text-align: center; font-size: 15px; } +.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: "\2713"; + font-size: 13px; + color: #fafafa; + text-align: center; + line-height: 13px; } + +.input-group-sm { + padding-top: 2px; + padding-bottom: 2px; } + +.tab-pane .form-horizontal .form-group { + margin-right: 15px; + margin-left: 15px; } + diff --git a/PlexRequests.UI/Content/custom.min.css b/PlexRequests.UI/Content/custom.min.css index 0a8928887..9c604fa2f 100644 --- a/PlexRequests.UI/Content/custom.min.css +++ b/PlexRequests.UI/Content/custom.min.css @@ -1 +1 @@ -@media(min-width:768px){.row{position:relative;}.bottom-align-text{position:absolute;bottom:0;right:0;}}@media(max-width:48em){.home{padding-top:1rem;}}@media(min-width:48em){.home{padding-top:4rem;}}.btn{border-radius:.25rem !important;}.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;}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{background:#4e5d6c;}.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:13px;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:#ffa400;text-align:center;font-size:15px;} \ No newline at end of file +@media(min-width:768px){.row{position:relative;}.bottom-align-text{position:absolute;bottom:0;right:0;}}@media(max-width:48em){.home{padding-top:1rem;}}@media(min-width:48em){.home{padding-top:4rem;}}.btn{border-radius:.25rem !important;}.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;}.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:13px;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:#ffa400;text-align:center;font-size:15px;}.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;}.input-group-sm{padding-top:2px;padding-bottom:2px;}.tab-pane .form-horizontal .form-group{margin-right:15px;margin-left:15px;} \ No newline at end of file diff --git a/PlexRequests.UI/Content/custom.scss b/PlexRequests.UI/Content/custom.scss index e2fd7c26d..0157bed3e 100644 --- a/PlexRequests.UI/Content/custom.scss +++ b/PlexRequests.UI/Content/custom.scss @@ -69,12 +69,34 @@ label { font-size: 16px $i; } +.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%; @@ -284,4 +306,44 @@ $border-radius: 10px; background-color: rgb(255, 164, 0); text-align: center; font-size: 15px; -} \ No newline at end of file +} + +.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: "\2713"; + font-size: 13px; + color: #fafafa; + text-align: center; + line-height: 13px; } + +.input-group-sm{ + padding-top: 2px; + padding-bottom: 2px; +} + +.tab-pane .form-horizontal .form-group { + margin-right: 15px; + margin-left: 15px; } \ No newline at end of file diff --git a/PlexRequests.UI/Helpers/BaseUrlHelper.cs b/PlexRequests.UI/Helpers/BaseUrlHelper.cs index 4071f00d8..fd14cf23e 100644 --- a/PlexRequests.UI/Helpers/BaseUrlHelper.cs +++ b/PlexRequests.UI/Helpers/BaseUrlHelper.cs @@ -57,6 +57,7 @@ namespace PlexRequests.UI.Helpers sb.AppendLine($""); sb.AppendLine($""); sb.AppendLine($""); + sb.AppendLine($""); sb.AppendLine($""); sb.AppendLine($""); diff --git a/PlexRequests.UI/Modules/AdminModule.cs b/PlexRequests.UI/Modules/AdminModule.cs index e1673c5e0..2e3c4233d 100644 --- a/PlexRequests.UI/Modules/AdminModule.cs +++ b/PlexRequests.UI/Modules/AdminModule.cs @@ -278,7 +278,7 @@ namespace PlexRequests.UI.Modules var token = settings?.PlexAuthToken; if (token == null) { - return Response.AsJson(string.Empty); + return Response.AsJson(new { Result = true, Users = string.Empty }); } try { diff --git a/PlexRequests.UI/PlexRequests.UI.csproj b/PlexRequests.UI/PlexRequests.UI.csproj index f9058cdb2..50c045ad1 100644 --- a/PlexRequests.UI/PlexRequests.UI.csproj +++ b/PlexRequests.UI/PlexRequests.UI.csproj @@ -196,6 +196,9 @@ + + PreserveNewest + PreserveNewest @@ -363,6 +366,9 @@ compilerconfig.json + + PreserveNewest + Always diff --git a/PlexRequests.UI/Views/Admin/Authentication.cshtml b/PlexRequests.UI/Views/Admin/Authentication.cshtml index 129220cd6..471b10903 100644 --- a/PlexRequests.UI/Views/Admin/Authentication.cshtml +++ b/PlexRequests.UI/Views/Admin/Authentication.cshtml @@ -16,35 +16,35 @@
    - +
    - +
    diff --git a/PlexRequests.UI/Views/Admin/CouchPotato.cshtml b/PlexRequests.UI/Views/Admin/CouchPotato.cshtml index 0e52986e2..8558534f2 100644 --- a/PlexRequests.UI/Views/Admin/CouchPotato.cshtml +++ b/PlexRequests.UI/Views/Admin/CouchPotato.cshtml @@ -17,16 +17,16 @@ CouchPotato Settings
    - +
    @@ -53,16 +53,16 @@
    - +
    diff --git a/PlexRequests.UI/Views/Admin/EmailNotifications.cshtml b/PlexRequests.UI/Views/Admin/EmailNotifications.cshtml index ef12311e3..b9b22f5a7 100644 --- a/PlexRequests.UI/Views/Admin/EmailNotifications.cshtml +++ b/PlexRequests.UI/Views/Admin/EmailNotifications.cshtml @@ -18,44 +18,44 @@
    - +
    - +
    - +
    Please note that if user notifications is enabled, the email will get sent with the SMTP set-up below. diff --git a/PlexRequests.UI/Views/Admin/Headphones.cshtml b/PlexRequests.UI/Views/Admin/Headphones.cshtml index 4145b53bf..f95b3ffdf 100644 --- a/PlexRequests.UI/Views/Admin/Headphones.cshtml +++ b/PlexRequests.UI/Views/Admin/Headphones.cshtml @@ -17,30 +17,30 @@ Headphones Settings
    - +
    - +
    diff --git a/PlexRequests.UI/Views/Admin/Plex.cshtml b/PlexRequests.UI/Views/Admin/Plex.cshtml index a4b9a2787..d50364b02 100644 --- a/PlexRequests.UI/Views/Admin/Plex.cshtml +++ b/PlexRequests.UI/Views/Admin/Plex.cshtml @@ -32,16 +32,16 @@
    - +
    diff --git a/PlexRequests.UI/Views/Admin/PushbulletNotifications.cshtml b/PlexRequests.UI/Views/Admin/PushbulletNotifications.cshtml index 9ef592b7a..a4a53a902 100644 --- a/PlexRequests.UI/Views/Admin/PushbulletNotifications.cshtml +++ b/PlexRequests.UI/Views/Admin/PushbulletNotifications.cshtml @@ -8,16 +8,16 @@
    - +
    diff --git a/PlexRequests.UI/Views/Admin/PushoverNotifications.cshtml b/PlexRequests.UI/Views/Admin/PushoverNotifications.cshtml index e67c1b541..8417bb227 100644 --- a/PlexRequests.UI/Views/Admin/PushoverNotifications.cshtml +++ b/PlexRequests.UI/Views/Admin/PushoverNotifications.cshtml @@ -8,16 +8,16 @@
    - +
    diff --git a/PlexRequests.UI/Views/Admin/Settings.cshtml b/PlexRequests.UI/Views/Admin/Settings.cshtml index 67e484970..1c1a42e59 100644 --- a/PlexRequests.UI/Views/Admin/Settings.cshtml +++ b/PlexRequests.UI/Views/Admin/Settings.cshtml @@ -23,7 +23,7 @@
    - Request Plex Settings + Plex Request Settings
    @@ -42,138 +42,137 @@
    You will have to restart after changing the url base. -
    +
    - - + +
    -
    +
    - +
    - +
    - +
    - - -
    +
    - +
    - + + +
    +
    + +
    +
    + + @if (Model.UsersCanViewOnlyOwnRequests) + { + + + } + else + { + + } +

    A comma separated list of users whose requests do not require approval.

    - +
    -
    -
    - - -
    -
    - @*
    - -
    - -
    -
    //TODO: Need to implement this*@ + +
    + +
    +
    //TODO: Need to implement this*@
    diff --git a/PlexRequests.UI/Views/Admin/Sickrage.cshtml b/PlexRequests.UI/Views/Admin/Sickrage.cshtml index 34eb87a42..6181f0b0e 100644 --- a/PlexRequests.UI/Views/Admin/Sickrage.cshtml +++ b/PlexRequests.UI/Views/Admin/Sickrage.cshtml @@ -17,16 +17,14 @@ SickRage Settings
    -
    @@ -53,16 +51,15 @@
    -
    diff --git a/PlexRequests.UI/Views/Admin/Sonarr.cshtml b/PlexRequests.UI/Views/Admin/Sonarr.cshtml index 1607b0d41..56f14cb7e 100644 --- a/PlexRequests.UI/Views/Admin/Sonarr.cshtml +++ b/PlexRequests.UI/Views/Admin/Sonarr.cshtml @@ -17,16 +17,14 @@ Sonarr Settings
    -
    @@ -53,16 +51,14 @@
    -
    @@ -93,7 +89,7 @@
    - +
    +
    diff --git a/PlexRequests.UI/Views/Login/Index.cshtml b/PlexRequests.UI/Views/Login/Index.cshtml index 37a07dc33..bbdf57068 100644 --- a/PlexRequests.UI/Views/Login/Index.cshtml +++ b/PlexRequests.UI/Views/Login/Index.cshtml @@ -8,15 +8,16 @@ } } - Username -
    - Password -
    - Remember Me -

    - + Username +
    + Password +
    + + +
    + - + @if (!Model.AdminExists) { diff --git a/PlexRequests.UI/Views/Search/Index.cshtml b/PlexRequests.UI/Views/Search/Index.cshtml index 1c419cdc8..3d26aab63 100644 --- a/PlexRequests.UI/Views/Search/Index.cshtml +++ b/PlexRequests.UI/Views/Search/Index.cshtml @@ -12,28 +12,35 @@

    Want to watch something that is not currently on Plex?! No problem! Just search for it below and request it!


    -
    -
    - -
    -
    + @@ -102,9 +109,32 @@
    } + + +
    +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + +
    +
    +
    +
    +
    - -
    diff --git a/PlexRequests.UI/Views/UserLogin/Index.cshtml b/PlexRequests.UI/Views/UserLogin/Index.cshtml index aada16203..ebef5b3cb 100644 --- a/PlexRequests.UI/Views/UserLogin/Index.cshtml +++ b/PlexRequests.UI/Views/UserLogin/Index.cshtml @@ -24,11 +24,12 @@
    - +
    +
    } -
    + diff --git a/PlexRequests.Updater/Updater.cs b/PlexRequests.Updater/Updater.cs index 43b44007c..2b32317b0 100644 --- a/PlexRequests.Updater/Updater.cs +++ b/PlexRequests.Updater/Updater.cs @@ -95,7 +95,7 @@ namespace PlexRequests.Updater fullname = entry.FullName.Replace("Release/", string.Empty); } - var fullPath = Path.Combine(Path.Combine(Path.GetDirectoryName(Application.ExecutablePath)), fullname); + var fullPath = Path.Combine(Path.GetDirectoryName(Application.ExecutablePath), fullname); if (string.IsNullOrEmpty(entry.Name)) { From db00326a9ca1b86957400acee69a3bcf45feab9b Mon Sep 17 00:00:00 2001 From: tidusjar Date: Fri, 20 May 2016 16:01:57 +0100 Subject: [PATCH 64/76] fixed test --- PlexRequests.UI.Tests/AdminModuleTests.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/PlexRequests.UI.Tests/AdminModuleTests.cs b/PlexRequests.UI.Tests/AdminModuleTests.cs index 441e6fe53..7850bbdb7 100644 --- a/PlexRequests.UI.Tests/AdminModuleTests.cs +++ b/PlexRequests.UI.Tests/AdminModuleTests.cs @@ -333,9 +333,10 @@ namespace PlexRequests.UI.Tests Assert.That(HttpStatusCode.OK, Is.EqualTo(result.StatusCode)); - var body = JsonConvert.DeserializeObject(result.Body.AsString()); + var body = JsonConvert.DeserializeObject(result.Body.AsString()); + var user = (string)body["users"]; Assert.That(body, Is.Not.Null); - Assert.That(string.IsNullOrWhiteSpace(body), Is.True); + Assert.That(string.IsNullOrWhiteSpace(user), Is.True); PlexMock.Verify(x => x.GetUsers(It.IsAny()), Times.Never); AuthMock.Verify(x => x.GetSettings(), Times.Once); From e49b160500476b7bee38c6b4e2eb9017734e3598 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Fri, 20 May 2016 17:20:17 +0100 Subject: [PATCH 65/76] Fully finished #27 just need to test it! --- .../Jobs/PlexAvailabilityChecker.cs | 12 +- PlexRequests.Store/Models/UsersToNotify.cs | 36 +++ PlexRequests.Store/PlexRequests.Store.csproj | 1 + PlexRequests.Store/RequestedModel.cs | 16 -- PlexRequests.Store/SqlTables.sql | 9 +- PlexRequests.UI/Bootstrapper.cs | 1 + PlexRequests.UI/Content/search.js | 52 +++- .../RequestedModelDataProvider.cs | 1 - PlexRequests.UI/Modules/AdminModule.cs | 6 +- PlexRequests.UI/Modules/SearchModule.cs | 257 +++++++++++------- PlexRequests.UI/Views/Search/Index.cshtml | 2 +- 11 files changed, 254 insertions(+), 139 deletions(-) create mode 100644 PlexRequests.Store/Models/UsersToNotify.cs diff --git a/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs b/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs index 0e29b93d5..d6bd244ca 100644 --- a/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs +++ b/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs @@ -39,6 +39,8 @@ using PlexRequests.Services.Interfaces; using PlexRequests.Services.Models; using PlexRequests.Services.Notification; using PlexRequests.Store; +using PlexRequests.Store.Models; +using PlexRequests.Store.Repository; using Quartz; @@ -47,7 +49,7 @@ namespace PlexRequests.Services.Jobs public class PlexAvailabilityChecker : IJob, IAvailabilityChecker { public PlexAvailabilityChecker(ISettingsService plexSettings, ISettingsService auth, IRequestService request, IPlexApi plex, ICacheProvider cache, - INotificationService notify, IJobRecord rec) + INotificationService notify, IJobRecord rec, IRepository users) { Plex = plexSettings; Auth = auth; @@ -56,6 +58,7 @@ namespace PlexRequests.Services.Jobs Cache = cache; Notification = notify; Job = rec; + UserNotifyRepo = users; } private ISettingsService Plex { get; } @@ -66,7 +69,7 @@ namespace PlexRequests.Services.Jobs private ICacheProvider Cache { get; } private INotificationService Notification { get; } private IJobRecord Job { get; } - + private IRepository UserNotifyRepo { get; } public void CheckAndUpdateAll() { Log.Trace("Getting the settings"); @@ -328,10 +331,11 @@ namespace PlexRequests.Services.Jobs return; } + var users = UserNotifyRepo.GetAll().ToList(); foreach (var model in modelChanged) { - var usersToNotify = model.UsersToNotify; // Users that selected the notification button when requesting a movie/tv show - foreach (var user in usersToNotify) + var selectedUsers = users.Select(x => x.Username).Intersect(model.RequestedUsers); + foreach (var user in selectedUsers) { var email = plexUser.User.FirstOrDefault(x => x.Username == user); if (email == null) diff --git a/PlexRequests.Store/Models/UsersToNotify.cs b/PlexRequests.Store/Models/UsersToNotify.cs new file mode 100644 index 000000000..521288894 --- /dev/null +++ b/PlexRequests.Store/Models/UsersToNotify.cs @@ -0,0 +1,36 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: UsersToNotify.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 Dapper.Contrib.Extensions; + +namespace PlexRequests.Store.Models +{ + [Table("UsersToNotify")] + public class UsersToNotify : Entity + { + public string Username { get; set; } + } +} \ No newline at end of file diff --git a/PlexRequests.Store/PlexRequests.Store.csproj b/PlexRequests.Store/PlexRequests.Store.csproj index 8d52b35d7..19578aee5 100644 --- a/PlexRequests.Store/PlexRequests.Store.csproj +++ b/PlexRequests.Store/PlexRequests.Store.csproj @@ -60,6 +60,7 @@ + diff --git a/PlexRequests.Store/RequestedModel.cs b/PlexRequests.Store/RequestedModel.cs index 0f4a28f23..e8fc41e08 100644 --- a/PlexRequests.Store/RequestedModel.cs +++ b/PlexRequests.Store/RequestedModel.cs @@ -40,7 +40,6 @@ namespace PlexRequests.Store public List RequestedUsers { get; set; } public string ArtistName { get; set; } public string ArtistId { get; set; } - public List UsersToNotify { get; private set; } [JsonIgnore] public List AllUsers @@ -68,21 +67,6 @@ namespace PlexRequests.Store { return AllUsers.Any(x => x.Equals(username, StringComparison.OrdinalIgnoreCase)); } - - public void AddUserToNotification(string username) - { - if (UsersToNotify == null) - { - UsersToNotify = new List(); - } - if (UsersToNotify.FirstOrDefault(x => x == username) != null) - { - // User already exists in the notification list - return; - } - - UsersToNotify.Add(username); - } } public enum RequestType diff --git a/PlexRequests.Store/SqlTables.sql b/PlexRequests.Store/SqlTables.sql index 31db785b8..74d5f2c13 100644 --- a/PlexRequests.Store/SqlTables.sql +++ b/PlexRequests.Store/SqlTables.sql @@ -65,4 +65,11 @@ CREATE TABLE IF NOT EXISTS ScheduledJobs Name varchar(100) NOT NULL, LastRun varchar(100) NOT NULL ); -CREATE UNIQUE INDEX IF NOT EXISTS ScheduledJobs_Id ON ScheduledJobs (Id); \ No newline at end of file +CREATE UNIQUE INDEX IF NOT EXISTS ScheduledJobs_Id ON ScheduledJobs (Id); + +CREATE TABLE IF NOT EXISTS UsersToNotify +( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + Username varchar(100) NOT NULL +); +CREATE UNIQUE INDEX IF NOT EXISTS UsersToNotify_Id ON UsersToNotify (Id); \ No newline at end of file diff --git a/PlexRequests.UI/Bootstrapper.cs b/PlexRequests.UI/Bootstrapper.cs index 4966373c3..5f95ea9ba 100644 --- a/PlexRequests.UI/Bootstrapper.cs +++ b/PlexRequests.UI/Bootstrapper.cs @@ -86,6 +86,7 @@ namespace PlexRequests.UI // Repo's container.Register, GenericRepository>(); + container.Register, GenericRepository>(); container.Register, GenericRepository>(); container.Register(); container.Register(); diff --git a/PlexRequests.UI/Content/search.js b/PlexRequests.UI/Content/search.js index ad1dcf298..6c502eef0 100644 --- a/PlexRequests.UI/Content/search.js +++ b/PlexRequests.UI/Content/search.js @@ -29,6 +29,21 @@ $(function () { }); focusSearch($('li.active a', '#nav-tabs').first().attr('href')); + // Get the user notification setting + var url = createBaseUrl(base, '/search/notifyuser/'); + $.ajax({ + type: "get", + url: url, + dataType: "json", + success: function (response) { + $('#notifyUser').prop("checked", response); + }, + error: function (e) { + console.log(e); + generateNotify("Something went wrong!", "danger"); + } + }); + // Type in movie search $("#movieSearchContent").on("input", function () { if (searchTimer) { @@ -80,10 +95,6 @@ $(function () { data = data + "&seasons=first"; } - var $notify = $('#notifyUser').is(':checked'); - - data = data + "¬ify=" + $notify; - var type = $form.prop('method'); var url = $form.prop('action'); @@ -117,10 +128,6 @@ $(function () { var url = $form.prop('action'); var data = $form.serialize(); - var $notify = $('#notifyUser').is(':checked'); - - data = data + "¬ify=" + $notify; - sendRequestAjax(data, type, url, buttonId); }); @@ -142,13 +149,36 @@ $(function () { var type = $form.prop('method'); var url = $form.prop('action'); var data = $form.serialize(); - var $notify = $('#notifyUser').is(':checked'); - - data = data + "¬ify=" + $notify; sendRequestAjax(data, type, url, buttonId); }); + // Enable/Disable user notifications + $('#saveNotificationSettings') + .click(function (e) { + e.preventDefault(); + var url = createBaseUrl(base, '/search/notifyuser/'); + var checked = $('#notifyUser').prop('checked'); + $.ajax({ + type: "post", + url: url, + data: {notify: checked}, + dataType: "json", + success: function (response) { + console.log(response); + if (response.result === true) { + generateNotify(response.message || "Success!", "success"); + } else { + generateNotify(response.message, "warning"); + } + }, + error: function (e) { + console.log(e); + generateNotify("Something went wrong!", "danger"); + } + }); + }); + function focusSearch($content) { if ($content.length > 0) { $('input[type=text].form-control', $content).first().focus(); diff --git a/PlexRequests.UI/ModelDataProviders/RequestedModelDataProvider.cs b/PlexRequests.UI/ModelDataProviders/RequestedModelDataProvider.cs index fe75957d8..efaac31ed 100644 --- a/PlexRequests.UI/ModelDataProviders/RequestedModelDataProvider.cs +++ b/PlexRequests.UI/ModelDataProviders/RequestedModelDataProvider.cs @@ -64,7 +64,6 @@ namespace PlexRequests.UI.ModelDataProviders with.Property(x => x.RequestedDate).Description("The date if the request, if this is not set, the request date will be set at the time of the Api call"); with.Property(x => x.RequestedUsers).Description("A collection of the requested users").Required(true); with.Property(x => x.Type).Description("The type of request: Movie = 0, TvShow = 1, Album = 2").Required(true); - with.Property(x => x.UsersToNotify).Description("A list of Plex users to notify"); }); } } diff --git a/PlexRequests.UI/Modules/AdminModule.cs b/PlexRequests.UI/Modules/AdminModule.cs index 2e3c4233d..ee7bcb0b8 100644 --- a/PlexRequests.UI/Modules/AdminModule.cs +++ b/PlexRequests.UI/Modules/AdminModule.cs @@ -502,7 +502,11 @@ namespace PlexRequests.UI.Modules private Response AutoUpdate() { var url = Request.Form["url"]; - var startInfo = new ProcessStartInfo("PlexRequests.Updater.exe") { Arguments = url}; + + var startInfo = Type.GetType("Mono.Runtime") != null + ? new ProcessStartInfo("mono PlexRequests.Updater.exe") { Arguments = url } + : new ProcessStartInfo("PlexRequests.Updater.exe") { Arguments = url }; + Process.Start(startInfo); Environment.Exit(0); diff --git a/PlexRequests.UI/Modules/SearchModule.cs b/PlexRequests.UI/Modules/SearchModule.cs index 62601aee8..31f875453 100644 --- a/PlexRequests.UI/Modules/SearchModule.cs +++ b/PlexRequests.UI/Modules/SearchModule.cs @@ -31,7 +31,6 @@ using System.Linq; using Nancy; using Nancy.Responses.Negotiation; -using Nancy.Security; using NLog; @@ -42,7 +41,6 @@ using PlexRequests.Core; using PlexRequests.Core.SettingModels; using PlexRequests.Helpers; using PlexRequests.Helpers.Exceptions; -using PlexRequests.Services; using PlexRequests.Services.Interfaces; using PlexRequests.Services.Notification; using PlexRequests.Store; @@ -50,6 +48,9 @@ using PlexRequests.UI.Helpers; using PlexRequests.UI.Models; using System.Threading.Tasks; using PlexRequests.Api.Models.Tv; +using PlexRequests.Store.Models; +using PlexRequests.Store.Repository; + using TMDbLib.Objects.General; namespace PlexRequests.UI.Modules @@ -60,9 +61,9 @@ namespace PlexRequests.UI.Modules ISettingsService prSettings, IAvailabilityChecker checker, IRequestService request, ISonarrApi sonarrApi, ISettingsService sonarrSettings, ISettingsService sickRageService, ICouchPotatoApi cpApi, ISickRageApi srApi, - INotificationService notify, IMusicBrainzApi mbApi, IHeadphonesApi hpApi, ISettingsService hpService, - ICouchPotatoCacher cpCacher, ISonarrCacher sonarrCacher, ISickRageCacher sickRageCacher, IPlexApi plexApi, - ISettingsService plexService, ISettingsService auth) : base("search", prSettings) + INotificationService notify, IMusicBrainzApi mbApi, IHeadphonesApi hpApi, ISettingsService hpService, + ICouchPotatoCacher cpCacher, ISonarrCacher sonarrCacher, ISickRageCacher sickRageCacher, IPlexApi plexApi, + ISettingsService plexService, ISettingsService auth, IRepository u) : base("search", prSettings) { Auth = auth; PlexService = plexService; @@ -85,6 +86,7 @@ namespace PlexRequests.UI.Modules MusicBrainzApi = mbApi; HeadphonesApi = hpApi; HeadphonesService = hpService; + UsersToNotifyRepo = u; Get["/"] = parameters => RequestLoad(); @@ -97,9 +99,12 @@ namespace PlexRequests.UI.Modules Get["movie/upcoming"] = parameters => UpcomingMovies(); Get["movie/playing"] = parameters => CurrentlyPlayingMovies(); - Post["request/movie"] = parameters => RequestMovie((int)Request.Form.movieId, (bool)Request.Form.notify); - Post["request/tv"] = parameters => RequestTvShow((int)Request.Form.tvId, (string)Request.Form.seasons, (bool)Request.Form.notify); - Post["request/album"] = parameters => RequestAlbum((string)Request.Form.albumId, (bool)Request.Form.notify); + Post["request/movie"] = parameters => RequestMovie((int)Request.Form.movieId); + Post["request/tv"] = parameters => RequestTvShow((int)Request.Form.tvId, (string)Request.Form.seasons); + Post["request/album"] = parameters => RequestAlbum((string)Request.Form.albumId); + + Post["/notifyuser"] = x => NotifyUser((bool)Request.Form.notify); + Get["/notifyuser"] = x => GetUserNotificationSettings(); } private IPlexApi PlexApi { get; } private TheMovieDbApi MovieApi { get; } @@ -122,6 +127,7 @@ namespace PlexRequests.UI.Modules private ISickRageCacher SickRageCacher { get; } private IMusicBrainzApi MusicBrainzApi { get; } private IHeadphonesApi HeadphonesApi { get; } + private IRepository UsersToNotifyRepo { get; } private static Logger Log = LogManager.GetCurrentClassLogger(); private Negotiator RequestLoad() @@ -157,7 +163,7 @@ namespace PlexRequests.UI.Modules var apiMovies = new List(); taskList.Add(Task.Factory.StartNew(() => { - switch(searchType) + switch (searchType) { case MovieSearchType.Search: return MovieApi.SearchMovie(searchTerm).Result.Select(x => new MovieResult() @@ -263,7 +269,7 @@ namespace PlexRequests.UI.Modules { Log.Trace("Searching for TV Show {0}", searchTerm); - var taskList = new List(); + var taskList = new List(); var apiTv = new List(); taskList.Add(Task.Factory.StartNew(() => @@ -412,7 +418,7 @@ namespace PlexRequests.UI.Modules return Response.AsJson(viewAlbum); } - private Response RequestMovie(int movieId, bool notify = false) + private Response RequestMovie(int movieId) { var movieApi = new TheMovieDbApi(); var movieInfo = movieApi.GetMovieInformation(movieId).Result; @@ -431,10 +437,6 @@ namespace PlexRequests.UI.Modules // check if the current user is already marked as a requester for this movie, if not, add them if (!existingRequest.UserHasRequested(Username)) { - if (notify) - { - existingRequest.AddUserToNotification(Username); - } existingRequest.RequestedUsers.Add(Username); RequestService.UpdateRequest(existingRequest); } @@ -472,13 +474,8 @@ namespace PlexRequests.UI.Modules Approved = false, RequestedUsers = new List { Username }, Issues = IssueState.None, - - }; - if (notify) - { - model.AddUserToNotification(Username); - } + }; Log.Trace(settings.DumpJson()); if (ShouldAutoApprove(RequestType.Movie, settings)) @@ -500,15 +497,17 @@ namespace PlexRequests.UI.Modules RequestService.AddRequest(model); - if (ShouldSendNotification()) { - var notificationModel = new NotificationModel { - Title = model.Title, - User = Username, - DateTime = DateTime.Now, - NotificationType = NotificationType.NewRequest - }; - NotificationService.Publish (notificationModel); - } + if (ShouldSendNotification()) + { + var notificationModel = new NotificationModel + { + Title = model.Title, + User = Username, + DateTime = DateTime.Now, + NotificationType = NotificationType.NewRequest + }; + NotificationService.Publish(notificationModel); + } return Response.AsJson(new JsonResponseModel { Result = true, Message = $"{fullMovieName} was successfully added!" }); } return @@ -525,15 +524,17 @@ namespace PlexRequests.UI.Modules Log.Info("Adding movie to database (No approval required)"); RequestService.AddRequest(model); - if (ShouldSendNotification()) { - var notificationModel = new NotificationModel { - Title = model.Title, - User = Username, - DateTime = DateTime.Now, - NotificationType = NotificationType.NewRequest - }; - NotificationService.Publish (notificationModel); - } + if (ShouldSendNotification()) + { + var notificationModel = new NotificationModel + { + Title = model.Title, + User = Username, + DateTime = DateTime.Now, + NotificationType = NotificationType.NewRequest + }; + NotificationService.Publish(notificationModel); + } return Response.AsJson(new JsonResponseModel { Result = true, Message = $"{fullMovieName} was successfully added!" }); } @@ -564,7 +565,7 @@ namespace PlexRequests.UI.Modules /// The seasons. /// if set to true [notify]. /// - private Response RequestTvShow(int showId, string seasons, bool notify) + private Response RequestTvShow(int showId, string seasons) { var tvApi = new TvMazeApi(); @@ -584,10 +585,6 @@ namespace PlexRequests.UI.Modules // check if the current user is already marked as a requester for this show, if not, add them if (!existingRequest.UserHasRequested(Username)) { - if (notify) - { - existingRequest.AddUserToNotification(Username); - } existingRequest.RequestedUsers.Add(Username); RequestService.UpdateRequest(existingRequest); } @@ -625,10 +622,7 @@ namespace PlexRequests.UI.Modules ImdbId = showInfo.externals?.imdb ?? string.Empty, SeasonCount = showInfo.seasonCount }; - if (notify) - { - model.AddUserToNotification(Username); - } + var seasonsList = new List(); switch (seasons) { @@ -660,15 +654,17 @@ namespace PlexRequests.UI.Modules Log.Debug("Adding tv to database requests (No approval required & Sonarr)"); RequestService.AddRequest(model); - if (ShouldSendNotification()) { - var notify1 = new NotificationModel { - Title = model.Title, - User = Username, - DateTime = DateTime.Now, - NotificationType = NotificationType.NewRequest - }; - NotificationService.Publish (notify1); - } + if (ShouldSendNotification()) + { + var notify1 = new NotificationModel + { + Title = model.Title, + User = Username, + DateTime = DateTime.Now, + NotificationType = NotificationType.NewRequest + }; + NotificationService.Publish(notify1); + } return Response.AsJson(new JsonResponseModel { Result = true, Message = $"{fullShowName} was successfully added!" }); } @@ -686,22 +682,24 @@ namespace PlexRequests.UI.Modules model.Approved = true; Log.Debug("Adding tv to database requests (No approval required & SickRage)"); RequestService.AddRequest(model); - if (ShouldSendNotification()) { - var notify2 = new NotificationModel { - Title = model.Title, - User = Username, - DateTime = DateTime.Now, - NotificationType = NotificationType.NewRequest - }; - NotificationService.Publish (notify2); - } + if (ShouldSendNotification()) + { + var notify2 = new NotificationModel + { + Title = model.Title, + User = Username, + DateTime = DateTime.Now, + NotificationType = NotificationType.NewRequest + }; + NotificationService.Publish(notify2); + } return Response.AsJson(new JsonResponseModel { Result = true, Message = $"{fullShowName} was successfully added!" }); } return Response.AsJson(new JsonResponseModel { Result = false, Message = result?.message != null ? "Message From SickRage: " + result.message : "Something went wrong adding the movie to SickRage! Please check your settings." }); } - return Response.AsJson(new JsonResponseModel { Result=false, Message = "The request of TV Shows is not correctly set up. Please contact your admin."}); + return Response.AsJson(new JsonResponseModel { Result = false, Message = "The request of TV Shows is not correctly set up. Please contact your admin." }); } @@ -713,19 +711,22 @@ namespace PlexRequests.UI.Modules return Response.AsJson(new JsonResponseModel { Result = true, Message = $"{fullShowName} was successfully added!" }); } - private bool ShouldSendNotification(){ - var sendNotification = true; - var claims = Context.CurrentUser?.Claims; - if (claims != null) { - if (claims.Contains (UserClaims.Admin) || claims.Contains (UserClaims.PowerUser)) { - sendNotification = false; // Don't bother sending a notification if the user is an admin - } - } - return sendNotification; - } + private bool ShouldSendNotification() + { + var sendNotification = true; + var claims = Context.CurrentUser?.Claims; + if (claims != null) + { + if (claims.Contains(UserClaims.Admin) || claims.Contains(UserClaims.PowerUser)) + { + sendNotification = false; // Don't bother sending a notification if the user is an admin + } + } + return sendNotification; + } - private Response RequestAlbum(string releaseId, bool notify) + private Response RequestAlbum(string releaseId) { var settings = PrService.GetSettings(); var existingRequest = RequestService.CheckRequest(releaseId); @@ -736,10 +737,7 @@ namespace PlexRequests.UI.Modules Log.Debug("We do have an existing album request"); if (!existingRequest.UserHasRequested(Username)) { - if (notify) - { - existingRequest.AddUserToNotification(Username); - } + Log.Debug("Not in the requested list so adding them and updating the request. User: {0}", Username); existingRequest.RequestedUsers.Add(Username); RequestService.UpdateRequest(existingRequest); @@ -797,10 +795,6 @@ namespace PlexRequests.UI.Modules ArtistId = artist.id }; - if (notify) - { - model.AddUserToNotification(Username); - } if (ShouldAutoApprove(RequestType.Album, settings)) { Log.Debug("We don't require approval OR the user is in the whitelist"); @@ -825,15 +819,17 @@ namespace PlexRequests.UI.Modules model.Approved = true; RequestService.AddRequest(model); - if (ShouldSendNotification ()) { - var notify2 = new NotificationModel { - Title = model.Title, - User = Username, - DateTime = DateTime.Now, - NotificationType = NotificationType.NewRequest - }; - NotificationService.Publish (notify2); - } + if (ShouldSendNotification()) + { + var notify2 = new NotificationModel + { + Title = model.Title, + User = Username, + DateTime = DateTime.Now, + NotificationType = NotificationType.NewRequest + }; + NotificationService.Publish(notify2); + } return Response.AsJson(new JsonResponseModel @@ -843,15 +839,17 @@ namespace PlexRequests.UI.Modules }); } - if (ShouldSendNotification ()) { - var notify2 = new NotificationModel { - Title = model.Title, - User = Username, - DateTime = DateTime.Now, - NotificationType = NotificationType.NewRequest - }; - NotificationService.Publish (notify2); - } + if (ShouldSendNotification()) + { + var notify2 = new NotificationModel + { + Title = model.Title, + User = Username, + DateTime = DateTime.Now, + NotificationType = NotificationType.NewRequest + }; + NotificationService.Publish(notify2); + } var result = RequestService.AddRequest(model); return Response.AsJson(new JsonResponseModel { @@ -892,5 +890,56 @@ namespace PlexRequests.UI.Modules return false; } } + + private Response NotifyUser(bool notify) + { + var auth = Auth.GetSettings().UserAuthentication; + if (!auth) + { + return Response.AsJson(new JsonResponseModel { Result = false, Message = "Sorry, but this functionality is currently only for users with Plex accounts"}); + } + var username = Username; + var originalList = UsersToNotifyRepo.GetAll(); + if (!notify) + { + if (originalList == null) + { + return Response.AsJson(new JsonResponseModel { Result = false, Message = "We could not remove this notification because you never had it!" }); + } + var userToRemove = originalList.FirstOrDefault(x => x.Username == username); + if (userToRemove != null) + { + UsersToNotifyRepo.Delete(userToRemove); + } + return Response.AsJson(new JsonResponseModel { Result = true }); + } + + + if (originalList == null) + { + var userModel = new UsersToNotify { Username = username }; + var insertResult = UsersToNotifyRepo.Insert(userModel); + return Response.AsJson(insertResult != -1 ? new JsonResponseModel { Result = true } : new JsonResponseModel { Result = false, Message = "Could not save, please try again" }); + } + + var existingUser = originalList.FirstOrDefault(x => x.Username == username); + if (existingUser != null) + { + return Response.AsJson(new JsonResponseModel { Result = true }); // It's already enabled + } + else + { + var userModel = new UsersToNotify { Username = username }; + var insertResult = UsersToNotifyRepo.Insert(userModel); + return Response.AsJson(insertResult != -1 ? new JsonResponseModel { Result = true } : new JsonResponseModel { Result = false, Message = "Could not save, please try again" }); + } + + } + + public Response GetUserNotificationSettings() + { + var retval = UsersToNotifyRepo.GetAll().FirstOrDefault(x => x.Username == Username); + return Response.AsJson(retval != null); + } } } diff --git a/PlexRequests.UI/Views/Search/Index.cshtml b/PlexRequests.UI/Views/Search/Index.cshtml index 3d26aab63..20a1251b9 100644 --- a/PlexRequests.UI/Views/Search/Index.cshtml +++ b/PlexRequests.UI/Views/Search/Index.cshtml @@ -128,7 +128,7 @@
    - +
    From 5e0f022c831817b54f96216f3e79204dfa14b25a Mon Sep 17 00:00:00 2001 From: tidusjar Date: Fri, 20 May 2016 17:23:31 +0100 Subject: [PATCH 66/76] color --- PlexRequests.UI/Views/Shared/_Layout.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PlexRequests.UI/Views/Shared/_Layout.cshtml b/PlexRequests.UI/Views/Shared/_Layout.cshtml index 7e071b494..52737c7b4 100644 --- a/PlexRequests.UI/Views/Shared/_Layout.cshtml +++ b/PlexRequests.UI/Views/Shared/_Layout.cshtml @@ -56,7 +56,7 @@
  • Settings
  • Change password
  • -
  • Donate!
  • +
  • Donate!
  • Logout
  • From 86fa887bce92a88ce19e7b428a87a67d8a1da93f Mon Sep 17 00:00:00 2001 From: Jamie Date: Fri, 20 May 2016 17:26:44 +0100 Subject: [PATCH 67/76] Update README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 373f7b474..2ac9cd001 100644 --- a/README.md +++ b/README.md @@ -51,3 +51,6 @@ If you feel like donating you can [here!](https://paypal.me/PlexRequestsNet) ## A massive thanks to everyone below for all their help! [heartisall](https://github.com/heartisall), [Stuke00](https://github.com/Stuke00), [shiitake](https://github.com/shiitake), [Drewster727](https://github.com/Drewster727), Majawat, [EddiYo](https://github.com/EddiYo), [SaskiFX](https://github.com/SaskiFX) + +## Stats +[![Throughput Graph](https://graphs.waffle.io/tidusjar/PlexRequests.Net/throughput.svg)](https://waffle.io/tidusjar/PlexRequests.Net/metrics/throughput) From 4fd3db1ae51e85f8285b86424270f052a74bb46a Mon Sep 17 00:00:00 2001 From: tidusjar Date: Fri, 20 May 2016 21:24:26 +0100 Subject: [PATCH 68/76] Using Mailkit to fix #204 --- .../Notification/EmailMessageNotification.cs | 64 ++++++++++--------- PlexRequests.UI/Modules/BaseAuthModule.cs | 26 +++++--- .../Views/Admin/Authentication.cshtml | 2 +- 3 files changed, 52 insertions(+), 40 deletions(-) diff --git a/PlexRequests.Services/Notification/EmailMessageNotification.cs b/PlexRequests.Services/Notification/EmailMessageNotification.cs index cd4c6b0a2..311ef6d69 100644 --- a/PlexRequests.Services/Notification/EmailMessageNotification.cs +++ b/PlexRequests.Services/Notification/EmailMessageNotification.cs @@ -28,12 +28,13 @@ using System; using System.Net; using System.Net.Mail; using System.Threading.Tasks; - +using MimeKit; using NLog; using PlexRequests.Core; using PlexRequests.Core.SettingModels; using PlexRequests.Services.Interfaces; +using SmtpClient = MailKit.Net.Smtp.SmtpClient; namespace PlexRequests.Services.Notification { @@ -111,28 +112,28 @@ namespace PlexRequests.Services.Notification private async Task EmailNewRequest(NotificationModel model, EmailNotificationSettings settings) { - var message = new MailMessage + var message = new MimeMessage { - IsBodyHtml = true, - To = { new MailAddress(settings.RecipientEmail) }, - Body = $"Hello! The user '{model.User}' has requested {model.Title}! Please log in to approve this request. Request Date: {model.DateTime.ToString("f")}", - From = new MailAddress(settings.EmailSender), + Body = new TextPart("plain") { Text = $"Hello! The user '{model.User}' has requested {model.Title}! Please log in to approve this request. Request Date: {model.DateTime.ToString("f")}" }, Subject = $"Plex Requests: New request for {model.Title}!" }; + message.From.Add(new MailboxAddress(settings.EmailSender, settings.EmailSender)); + message.To.Add(new MailboxAddress(settings.RecipientEmail, settings.RecipientEmail)); + await Send(message, settings); } private async Task EmailIssue(NotificationModel model, EmailNotificationSettings settings) { - var message = new MailMessage + var message = new MimeMessage { - IsBodyHtml = true, - To = { new MailAddress(settings.RecipientEmail) }, - Body = $"Hello! The user '{model.User}' has reported a new issue {model.Body} for the title {model.Title}!", - From = new MailAddress(settings.EmailSender), + Body = new TextPart("plain") { Text = $"Hello! The user '{model.User}' has reported a new issue {model.Body} for the title {model.Title}!" }, Subject = $"Plex Requests: New issue for {model.Title}!" }; + message.From.Add(new MailboxAddress(settings.EmailSender, settings.EmailSender)); + message.To.Add(new MailboxAddress(settings.RecipientEmail, settings.RecipientEmail)); + await Send(message, settings); } @@ -144,33 +145,35 @@ namespace PlexRequests.Services.Notification await Task.FromResult(false); } - var message = new MailMessage + var message = new MimeMessage { - IsBodyHtml = true, - To = { new MailAddress(model.UserEmail) }, - Body = $"Hello! You requested {model.Title} on PlexRequests! This is now available on Plex! :)", - From = new MailAddress(settings.EmailSender), + Body = new TextPart("plain") { Text = $"Hello! You requested {model.Title} on PlexRequests! This is now available on Plex! :)" }, Subject = $"Plex Requests: {model.Title} is now available!" }; + message.From.Add(new MailboxAddress(settings.EmailSender, settings.EmailSender)); + message.To.Add(new MailboxAddress(model.UserEmail, model.UserEmail)); await Send(message, settings); } - private async Task Send(MailMessage message, EmailNotificationSettings settings) + private async Task Send(MimeMessage message, EmailNotificationSettings settings) { try { - using (var smtp = new SmtpClient(settings.EmailHost, settings.EmailPort)) + using (var client = new SmtpClient()) { - smtp.Credentials = new NetworkCredential(settings.EmailUsername, settings.EmailPassword); - smtp.EnableSsl = settings.Ssl; - await smtp.SendMailAsync(message).ConfigureAwait(false); + client.Connect(settings.EmailHost, settings.EmailPort, settings.Ssl); + + // Note: since we don't have an OAuth2 token, disable + // the XOAUTH2 authentication mechanism. + client.AuthenticationMechanisms.Remove("XOAUTH2"); + + client.Authenticate(settings.EmailUsername, settings.EmailPassword); + + await client.SendAsync(message); + await client.DisconnectAsync(true); } } - catch (SmtpException smtp) - { - Log.Error(smtp); - } catch (Exception e) { Log.Error(e); @@ -179,14 +182,13 @@ namespace PlexRequests.Services.Notification private async Task EmailTest(NotificationModel model, EmailNotificationSettings settings) { - var message = new MailMessage + var message = new MimeMessage { - IsBodyHtml = true, - To = { new MailAddress(settings.RecipientEmail) }, - Body = "This is just a test! Success!", - From = new MailAddress(settings.EmailSender), - Subject = "Plex Requests: Test Message!" + Body = new TextPart("plain") {Text= "This is just a test! Success!"}, + Subject = "Plex Requests: Test Message!", }; + message.From.Add(new MailboxAddress(settings.EmailSender, settings.EmailSender)); + message.To.Add(new MailboxAddress(settings.RecipientEmail, settings.RecipientEmail)); await Send(message, settings); } diff --git a/PlexRequests.UI/Modules/BaseAuthModule.cs b/PlexRequests.UI/Modules/BaseAuthModule.cs index 0bbad4a02..708a4cb49 100644 --- a/PlexRequests.UI/Modules/BaseAuthModule.cs +++ b/PlexRequests.UI/Modules/BaseAuthModule.cs @@ -56,12 +56,22 @@ namespace PlexRequests.UI.Modules } } - protected bool IsAdmin { get { - var claims = Context.CurrentUser.Claims.ToList(); - if(claims.Contains(UserClaims.Admin) || claims.Contains(UserClaims.PowerUser)){ - return true;} - return false; - } } + protected bool IsAdmin + { + get + { + if (Context.CurrentUser == null) + { + return false; + } + var claims = Context.CurrentUser.Claims.ToList(); + if (claims.Contains(UserClaims.Admin) || claims.Contains(UserClaims.PowerUser)) + { + return true; + } + return false; + } + } protected int DateTimeOffset { @@ -96,8 +106,8 @@ namespace PlexRequests.UI.Modules var redirectPath = string.IsNullOrEmpty(baseUrl) ? "~/userlogin" : $"~/{baseUrl}/userlogin"; - return Session[SessionKeys.UsernameKey] == null - ? Context.GetRedirect(redirectPath) + return Session[SessionKeys.UsernameKey] == null + ? Context.GetRedirect(redirectPath) : null; } } diff --git a/PlexRequests.UI/Views/Admin/Authentication.cshtml b/PlexRequests.UI/Views/Admin/Authentication.cshtml index 471b10903..badd56f05 100644 --- a/PlexRequests.UI/Views/Admin/Authentication.cshtml +++ b/PlexRequests.UI/Views/Admin/Authentication.cshtml @@ -164,7 +164,7 @@ return; } if (response.users.length > 1) { - $(response).each(function () { + $(response.users).each(function () { $('#users').append(""); }); } else { From e1ca857d7ebf62b08a14658999b1e7006c7e9a89 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Fri, 20 May 2016 21:31:54 +0100 Subject: [PATCH 69/76] Fixed #215 --- PlexRequests.UI/Content/requests.js | 3 ++- PlexRequests.UI/Views/Requests/Index.cshtml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/PlexRequests.UI/Content/requests.js b/PlexRequests.UI/Content/requests.js index fbff4a934..b3a820cf4 100644 --- a/PlexRequests.UI/Content/requests.js +++ b/PlexRequests.UI/Content/requests.js @@ -552,7 +552,8 @@ function buildRequestContext(result, type) { coverArtUrl: result.coverArtUrl, qualities: result.qualities, hasQualities: result.qualities && result.qualities.length > 0, - artist: result.artistName + artist: result.artistName, + musicBrainzId : result.musicBrainzId }; return context; diff --git a/PlexRequests.UI/Views/Requests/Index.cshtml b/PlexRequests.UI/Views/Requests/Index.cshtml index c8061e369..39cfa8b2d 100644 --- a/PlexRequests.UI/Views/Requests/Index.cshtml +++ b/PlexRequests.UI/Views/Requests/Index.cshtml @@ -269,7 +269,7 @@
    - +

    {{artist}} - {{title}} {{#if year}} From 7ce05028fcf523c8c0ba5c2b907f111cc9f23c12 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Fri, 20 May 2016 21:52:06 +0100 Subject: [PATCH 70/76] #27 fully finished --- PlexRequests.UI/Modules/SearchModule.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/PlexRequests.UI/Modules/SearchModule.cs b/PlexRequests.UI/Modules/SearchModule.cs index 31f875453..29a39fa37 100644 --- a/PlexRequests.UI/Modules/SearchModule.cs +++ b/PlexRequests.UI/Modules/SearchModule.cs @@ -63,7 +63,7 @@ namespace PlexRequests.UI.Modules ISettingsService sickRageService, ICouchPotatoApi cpApi, ISickRageApi srApi, INotificationService notify, IMusicBrainzApi mbApi, IHeadphonesApi hpApi, ISettingsService hpService, ICouchPotatoCacher cpCacher, ISonarrCacher sonarrCacher, ISickRageCacher sickRageCacher, IPlexApi plexApi, - ISettingsService plexService, ISettingsService auth, IRepository u) : base("search", prSettings) + ISettingsService plexService, ISettingsService auth, IRepository u, ISettingsService email) : base("search", prSettings) { Auth = auth; PlexService = plexService; @@ -87,6 +87,7 @@ namespace PlexRequests.UI.Modules HeadphonesApi = hpApi; HeadphonesService = hpService; UsersToNotifyRepo = u; + EmailNotificationSettings = email; Get["/"] = parameters => RequestLoad(); @@ -121,6 +122,7 @@ namespace PlexRequests.UI.Modules private ISettingsService SonarrService { get; } private ISettingsService SickRageService { get; } private ISettingsService HeadphonesService { get; } + private ISettingsService EmailNotificationSettings { get; } private IAvailabilityChecker Checker { get; } private ICouchPotatoCacher CpCacher { get; } private ISonarrCacher SonarrCacher { get; } @@ -894,10 +896,15 @@ namespace PlexRequests.UI.Modules private Response NotifyUser(bool notify) { var auth = Auth.GetSettings().UserAuthentication; + var email = EmailNotificationSettings.GetSettings().EnableUserEmailNotifications; if (!auth) { return Response.AsJson(new JsonResponseModel { Result = false, Message = "Sorry, but this functionality is currently only for users with Plex accounts"}); } + if (!email) + { + return Response.AsJson(new JsonResponseModel { Result = false, Message = "Sorry, but your administrator has not yet enabled this functionality." }); + } var username = Username; var originalList = UsersToNotifyRepo.GetAll(); if (!notify) From d0e6a2a3413cec73e0f69acdb9a065f3d8762d6c Mon Sep 17 00:00:00 2001 From: tidusjar Date: Fri, 20 May 2016 22:16:59 +0100 Subject: [PATCH 71/76] small changes --- PlexRequests.UI/Views/Requests/Index.cshtml | 6 +++--- PlexRequests.UI/Views/Shared/_Layout.cshtml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/PlexRequests.UI/Views/Requests/Index.cshtml b/PlexRequests.UI/Views/Requests/Index.cshtml index 39cfa8b2d..cdf45b288 100644 --- a/PlexRequests.UI/Views/Requests/Index.cshtml +++ b/PlexRequests.UI/Views/Requests/Index.cshtml @@ -17,15 +17,15 @@
    diff --git a/PlexRequests.UI/Views/Shared/_Layout.cshtml b/PlexRequests.UI/Views/Shared/_Layout.cshtml index 52737c7b4..1ca9c6f48 100644 --- a/PlexRequests.UI/Views/Shared/_Layout.cshtml +++ b/PlexRequests.UI/Views/Shared/_Layout.cshtml @@ -56,7 +56,7 @@
  • Settings
  • Change password
  • -
  • Donate!
  • +
  • Donate!
  • Logout
  • From 8cce80654b88b53c59e0ded7acf9be77ee457953 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Mon, 23 May 2016 11:40:22 +0100 Subject: [PATCH 72/76] Resolved #224 , Removed the 'SSL' option from the email notification settings. We will now use the correct secure socket options (SSL/TLS) for your email host. --- .../SettingModels/EmailNotificationSettings.cs | 1 - .../Notification/EmailMessageNotification.cs | 7 ++++--- .../Views/Admin/EmailNotifications.cshtml | 14 -------------- 3 files changed, 4 insertions(+), 18 deletions(-) diff --git a/PlexRequests.Core/SettingModels/EmailNotificationSettings.cs b/PlexRequests.Core/SettingModels/EmailNotificationSettings.cs index 74cb7ec92..2e3db9451 100644 --- a/PlexRequests.Core/SettingModels/EmailNotificationSettings.cs +++ b/PlexRequests.Core/SettingModels/EmailNotificationSettings.cs @@ -36,6 +36,5 @@ namespace PlexRequests.Core.SettingModels public bool Enabled { get; set; } public bool EnableUserEmailNotifications { get; set; } public string RecipientEmail { get; set; } - public bool Ssl { get; set; } } } \ No newline at end of file diff --git a/PlexRequests.Services/Notification/EmailMessageNotification.cs b/PlexRequests.Services/Notification/EmailMessageNotification.cs index 311ef6d69..efd4abe08 100644 --- a/PlexRequests.Services/Notification/EmailMessageNotification.cs +++ b/PlexRequests.Services/Notification/EmailMessageNotification.cs @@ -25,9 +25,10 @@ // ************************************************************************/ #endregion using System; -using System.Net; -using System.Net.Mail; using System.Threading.Tasks; + +using MailKit.Security; + using MimeKit; using NLog; @@ -162,7 +163,7 @@ namespace PlexRequests.Services.Notification { using (var client = new SmtpClient()) { - client.Connect(settings.EmailHost, settings.EmailPort, settings.Ssl); + client.Connect(settings.EmailHost, settings.EmailPort, SecureSocketOptions.Auto); // Let MailKit figure out the correct SecureSocketOptions. // Note: since we don't have an OAuth2 token, disable // the XOAUTH2 authentication mechanism. diff --git a/PlexRequests.UI/Views/Admin/EmailNotifications.cshtml b/PlexRequests.UI/Views/Admin/EmailNotifications.cshtml index b9b22f5a7..ce7bddbf2 100644 --- a/PlexRequests.UI/Views/Admin/EmailNotifications.cshtml +++ b/PlexRequests.UI/Views/Admin/EmailNotifications.cshtml @@ -30,20 +30,6 @@

    -
    -
    - - @if (Model.Ssl) - { - - } - else - { - - } - -
    -
    From 5a6863456fbd2697b7a187803132c133853864de Mon Sep 17 00:00:00 2001 From: tidusjar Date: Mon, 23 May 2016 13:28:16 +0100 Subject: [PATCH 73/76] #164 has been resolved --- PlexRequests.Api.Interfaces/ISlackApi.cs | 37 +++++ .../PlexRequests.Api.Interfaces.csproj | 1 + .../Notifications/SlackNotificationBody.cs | 56 +++++++ .../PlexRequests.Api.Models.csproj | 1 + PlexRequests.Api/ApiRequest.cs | 3 +- PlexRequests.Api/PlexRequests.Api.csproj | 1 + PlexRequests.Api/SlackApi.cs | 62 +++++++ PlexRequests.Core/PlexRequests.Core.csproj | 1 + .../SlackNotificationSettings.cs | 37 +++++ .../Notification/PushbulletNotification.cs | 4 +- .../Notification/SlackNotification.cs | 155 ++++++++++++++++++ .../PlexRequests.Services.csproj | 1 + PlexRequests.UI/Bootstrapper.cs | 9 + .../Modules/AdminNotificationsModule.cs | 130 +++++++++++++++ PlexRequests.UI/PlexRequests.UI.csproj | 5 + .../Validators/SlackSettingsValidator.cs | 40 +++++ .../Views/Admin/Authentication.cshtml | 2 +- .../Views/Admin/SlackNotifications.cshtml | 119 ++++++++++++++ PlexRequests.UI/Views/Admin/_Sidebar.cshtml | 1 + PlexRequests.UI/Views/Login/Index.cshtml | 2 +- PlexRequests.UI/Views/Login/Register.cshtml | 2 +- PlexRequests.UI/Views/UserLogin/Index.cshtml | 2 +- 22 files changed, 663 insertions(+), 8 deletions(-) create mode 100644 PlexRequests.Api.Interfaces/ISlackApi.cs create mode 100644 PlexRequests.Api.Models/Notifications/SlackNotificationBody.cs create mode 100644 PlexRequests.Api/SlackApi.cs create mode 100644 PlexRequests.Core/SettingModels/SlackNotificationSettings.cs create mode 100644 PlexRequests.Services/Notification/SlackNotification.cs create mode 100644 PlexRequests.UI/Modules/AdminNotificationsModule.cs create mode 100644 PlexRequests.UI/Validators/SlackSettingsValidator.cs create mode 100644 PlexRequests.UI/Views/Admin/SlackNotifications.cshtml diff --git a/PlexRequests.Api.Interfaces/ISlackApi.cs b/PlexRequests.Api.Interfaces/ISlackApi.cs new file mode 100644 index 000000000..812419f69 --- /dev/null +++ b/PlexRequests.Api.Interfaces/ISlackApi.cs @@ -0,0 +1,37 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: ISlackApi.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.Threading.Tasks; + +using PlexRequests.Api.Models.Notifications; + +namespace PlexRequests.Api.Interfaces +{ + public interface ISlackApi + { + Task PushAsync(string team, string token, string service, SlackNotificationBody message); + } +} \ No newline at end of file diff --git a/PlexRequests.Api.Interfaces/PlexRequests.Api.Interfaces.csproj b/PlexRequests.Api.Interfaces/PlexRequests.Api.Interfaces.csproj index 3b3321ec6..8512ca932 100644 --- a/PlexRequests.Api.Interfaces/PlexRequests.Api.Interfaces.csproj +++ b/PlexRequests.Api.Interfaces/PlexRequests.Api.Interfaces.csproj @@ -51,6 +51,7 @@ + diff --git a/PlexRequests.Api.Models/Notifications/SlackNotificationBody.cs b/PlexRequests.Api.Models/Notifications/SlackNotificationBody.cs new file mode 100644 index 000000000..a92b06d4f --- /dev/null +++ b/PlexRequests.Api.Models/Notifications/SlackNotificationBody.cs @@ -0,0 +1,56 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: SlackNotificationBody.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 Newtonsoft.Json; + +namespace PlexRequests.Api.Models.Notifications +{ + public class SlackNotificationBody + { + [JsonConstructor] + public SlackNotificationBody() + { + username = "Plex Requests"; + } + + [JsonIgnore] + private string _username; + public string username + { + get { return _username; } + set + { + if (!string.IsNullOrEmpty(value)) + _username = value; + } + } + public string channel { get; set; } + public string text { get; set; } + + public string icon_url { get; set; } + public string icon_emoji { get; set; } + } +} \ No newline at end of file diff --git a/PlexRequests.Api.Models/PlexRequests.Api.Models.csproj b/PlexRequests.Api.Models/PlexRequests.Api.Models.csproj index efd7d834d..8e670e858 100644 --- a/PlexRequests.Api.Models/PlexRequests.Api.Models.csproj +++ b/PlexRequests.Api.Models/PlexRequests.Api.Models.csproj @@ -58,6 +58,7 @@ + diff --git a/PlexRequests.Api/ApiRequest.cs b/PlexRequests.Api/ApiRequest.cs index b5ffa8c76..c1395c561 100644 --- a/PlexRequests.Api/ApiRequest.cs +++ b/PlexRequests.Api/ApiRequest.cs @@ -72,7 +72,7 @@ namespace PlexRequests.Api return response.Data; } - + public IRestResponse Execute(IRestRequest request, Uri baseUri) { var client = new RestClient { BaseUrl = baseUri }; @@ -109,7 +109,6 @@ namespace PlexRequests.Api public T ExecuteJson(IRestRequest request, Uri baseUri) where T : new() { var client = new RestClient { BaseUrl = baseUri }; - var response = client.Execute(request); Log.Trace("Api Content Response:"); Log.Trace(response.Content); diff --git a/PlexRequests.Api/PlexRequests.Api.csproj b/PlexRequests.Api/PlexRequests.Api.csproj index b1f9e1391..d627e0ac9 100644 --- a/PlexRequests.Api/PlexRequests.Api.csproj +++ b/PlexRequests.Api/PlexRequests.Api.csproj @@ -71,6 +71,7 @@ MockApiData.resx + diff --git a/PlexRequests.Api/SlackApi.cs b/PlexRequests.Api/SlackApi.cs new file mode 100644 index 000000000..929b475f1 --- /dev/null +++ b/PlexRequests.Api/SlackApi.cs @@ -0,0 +1,62 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: PlexApi.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.Threading.Tasks; + +using PlexRequests.Api.Interfaces; +using PlexRequests.Api.Models.Notifications; + +using RestSharp; + +namespace PlexRequests.Api +{ + public class SlackApi : ISlackApi + { + public async Task PushAsync(string team, string token, string service, SlackNotificationBody message) + { + var request = new RestRequest + { + Method = Method.POST, + Resource = "/services/{team}/{service}/{token}" + }; + + request.AddUrlSegment("team", team); + request.AddUrlSegment("service", service); + request.AddUrlSegment("token", token); + request.AddJsonBody(message); + + var api = new ApiRequest(); + return await Task.Run( + () => + { + var result = api.Execute(request, new Uri("https://hooks.slack.com/")); + return result.Content; + }); + } + } +} + diff --git a/PlexRequests.Core/PlexRequests.Core.csproj b/PlexRequests.Core/PlexRequests.Core.csproj index 8a66e8d43..0b454381e 100644 --- a/PlexRequests.Core/PlexRequests.Core.csproj +++ b/PlexRequests.Core/PlexRequests.Core.csproj @@ -73,6 +73,7 @@ + diff --git a/PlexRequests.Core/SettingModels/SlackNotificationSettings.cs b/PlexRequests.Core/SettingModels/SlackNotificationSettings.cs new file mode 100644 index 000000000..2b412ed3e --- /dev/null +++ b/PlexRequests.Core/SettingModels/SlackNotificationSettings.cs @@ -0,0 +1,37 @@ +using System; + +using Newtonsoft.Json; + +namespace PlexRequests.Core.SettingModels +{ + public class SlackNotificationSettings : Settings + { + public bool Enabled { get; set; } + public string WebhookUrl { get; set; } + public string Channel { get; set; } + public string Username { get; set; } + + [JsonIgnore] + public string Team => SplitWebUrl(3); + + [JsonIgnore] + public string Service => SplitWebUrl(4); + + [JsonIgnore] + public string Token => SplitWebUrl(5); + + private string SplitWebUrl(int index) + { + if (!WebhookUrl.StartsWith("http", StringComparison.InvariantCulture)) + { + WebhookUrl = "https://" + WebhookUrl; + } + var split = WebhookUrl.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); + + return split.Length < index + ? string.Empty + : split[index]; + } + + } +} \ No newline at end of file diff --git a/PlexRequests.Services/Notification/PushbulletNotification.cs b/PlexRequests.Services/Notification/PushbulletNotification.cs index f4d6f802e..70f318331 100644 --- a/PlexRequests.Services/Notification/PushbulletNotification.cs +++ b/PlexRequests.Services/Notification/PushbulletNotification.cs @@ -77,7 +77,7 @@ namespace PlexRequests.Services.Notification case NotificationType.AdminNote: break; case NotificationType.Test: - await PushTestAsync(model, pushSettings); + await PushTestAsync(pushSettings); break; default: throw new ArgumentOutOfRangeException(); @@ -116,7 +116,7 @@ namespace PlexRequests.Services.Notification await Push(settings, message, pushTitle); } - private async Task PushTestAsync(NotificationModel model, PushbulletNotificationSettings settings) + private async Task PushTestAsync(PushbulletNotificationSettings settings) { var message = "This is just a test! Success!"; var pushTitle = "Plex Requests: Test Message!"; diff --git a/PlexRequests.Services/Notification/SlackNotification.cs b/PlexRequests.Services/Notification/SlackNotification.cs new file mode 100644 index 000000000..5da0c156a --- /dev/null +++ b/PlexRequests.Services/Notification/SlackNotification.cs @@ -0,0 +1,155 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: SlackNotification.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.Threading.Tasks; + +using NLog; + +using PlexRequests.Api.Interfaces; +using PlexRequests.Api.Models.Notifications; +using PlexRequests.Core; +using PlexRequests.Core.SettingModels; +using PlexRequests.Services.Interfaces; + +namespace PlexRequests.Services.Notification +{ + public class SlackNotification : INotification + { + public SlackNotification(ISlackApi api, ISettingsService sn) + { + Api = api; + Settings = sn; + } + + public string NotificationName => "SlackNotification"; + + private ISlackApi Api { get; } + private ISettingsService Settings { get; } + private static Logger Log = LogManager.GetCurrentClassLogger(); + + + public async Task NotifyAsync(NotificationModel model) + { + var settings = Settings.GetSettings(); + + await NotifyAsync(model, settings); + } + + public async Task NotifyAsync(NotificationModel model, Settings settings) + { + if (settings == null) await NotifyAsync(model); + + var pushSettings = (SlackNotificationSettings)settings; + if (!ValidateConfiguration(pushSettings)) + { + Log.Error("Settings for Slack was not correct, we cannot push a notification"); + return; + } + + switch (model.NotificationType) + { + case NotificationType.NewRequest: + await PushNewRequestAsync(model, pushSettings); + break; + case NotificationType.Issue: + await PushIssueAsync(model, pushSettings); + break; + case NotificationType.RequestAvailable: + break; + case NotificationType.RequestApproved: + break; + case NotificationType.AdminNote: + break; + case NotificationType.Test: + await PushTest(pushSettings); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + private async Task PushNewRequestAsync(NotificationModel model, SlackNotificationSettings settings) + { + var message = $"{model.Title} has been requested by user: {model.User}"; + await Push(settings, message); + } + + private async Task PushIssueAsync(NotificationModel model, SlackNotificationSettings settings) + { + var message = $"A new issue: {model.Body} has been reported by user: {model.User} for the title: {model.Title}"; + await Push(settings, message); + } + + private async Task PushTest(SlackNotificationSettings settings) + { + var message = $"This is a test from Plex Requests, if you can see this then we have successfully pushed a notification!"; + await Push(settings, message); + } + + private async Task Push(SlackNotificationSettings config, string message) + { + try + { + var notification = new SlackNotificationBody { username = config.Username, channel = config.Channel ?? string.Empty, text = message }; + + var result = await Api.PushAsync(config.Team, config.Token, config.Service, notification); + if (!result.Equals("ok")) + { + Log.Error("Slack returned a message that was not 'ok', the notification did not get pushed"); + Log.Error($"Message that slack returned: {result}"); + } + } + catch (Exception e) + { + Log.Error(e); + } + } + + private bool ValidateConfiguration(SlackNotificationSettings settings) + { + if (!settings.Enabled) + { + return false; + } + if (string.IsNullOrEmpty(settings.WebhookUrl)) + { + return false; + } + try + { + var a = settings.Team; + var b = settings.Service; + var c = settings.Token; + } + catch (IndexOutOfRangeException) + { + return false; + } + return true; + } + } +} \ No newline at end of file diff --git a/PlexRequests.Services/PlexRequests.Services.csproj b/PlexRequests.Services/PlexRequests.Services.csproj index 607bc20f9..257fc5b7e 100644 --- a/PlexRequests.Services/PlexRequests.Services.csproj +++ b/PlexRequests.Services/PlexRequests.Services.csproj @@ -93,6 +93,7 @@ + diff --git a/PlexRequests.UI/Bootstrapper.cs b/PlexRequests.UI/Bootstrapper.cs index 5f95ea9ba..4eef3be38 100644 --- a/PlexRequests.UI/Bootstrapper.cs +++ b/PlexRequests.UI/Bootstrapper.cs @@ -83,6 +83,7 @@ namespace PlexRequests.UI container.Register, SettingsServiceV2>(); container.Register, SettingsServiceV2>(); container.Register, SettingsServiceV2>(); + container.Register, SettingsServiceV2>(); // Repo's container.Register, GenericRepository>(); @@ -108,6 +109,7 @@ namespace PlexRequests.UI container.Register(); container.Register(); container.Register(); + container.Register(); // NotificationService container.Register().AsSingleton(); @@ -193,6 +195,13 @@ namespace PlexRequests.UI { notificationService.Subscribe(new PushoverNotification(container.Resolve(), pushoverService)); } + + var slackService = container.Resolve>(); + var slackSettings = slackService.GetSettings(); + if (slackSettings.Enabled) + { + notificationService.Subscribe(new SlackNotification(container.Resolve(), slackService)); + } } protected override void RequestStartup(TinyIoCContainer container, IPipelines pipelines, NancyContext context) diff --git a/PlexRequests.UI/Modules/AdminNotificationsModule.cs b/PlexRequests.UI/Modules/AdminNotificationsModule.cs new file mode 100644 index 000000000..26565bc95 --- /dev/null +++ b/PlexRequests.UI/Modules/AdminNotificationsModule.cs @@ -0,0 +1,130 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: AdminNotificationsModule.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 Nancy; +using Nancy.ModelBinding; +using Nancy.Responses.Negotiation; +using Nancy.Security; +using Nancy.Validation; + +using NLog; + +using PlexRequests.Api.Interfaces; +using PlexRequests.Core; +using PlexRequests.Core.SettingModels; +using PlexRequests.Helpers; +using PlexRequests.Services.Interfaces; +using PlexRequests.Services.Notification; +using PlexRequests.UI.Helpers; +using PlexRequests.UI.Models; + +namespace PlexRequests.UI.Modules +{ + public class AdminNotificationsModule : BaseModule + { + public AdminNotificationsModule(ISettingsService prService, ISettingsService slackSettings, + INotificationService notify, ISlackApi slackApi) : base("admin", prService) + { + this.RequiresClaims(UserClaims.Admin); + + SlackSettings = slackSettings; + NotificationService = notify; + SlackApi = slackApi; + + Post["/testslacknotification"] = _ => TestSlackNotification(); + + Get["/slacknotification"] = _ => SlackNotifications(); + Post["/slacknotification"] = _ => SaveSlackNotifications(); + } + private ISettingsService SlackSettings { get; } + private INotificationService NotificationService { get; } + private ISlackApi SlackApi { get; } + + private static Logger Log = LogManager.GetCurrentClassLogger(); + + private Response TestSlackNotification() + { + var settings = this.BindAndValidate(); + if (!ModelValidationResult.IsValid) + { + return Response.AsJson(ModelValidationResult.SendJsonError()); + } + var notificationModel = new NotificationModel + { + NotificationType = NotificationType.Test, + DateTime = DateTime.Now + }; + try + { + NotificationService.Subscribe(new SlackNotification(SlackApi,SlackSettings)); + settings.Enabled = true; + NotificationService.Publish(notificationModel, settings); + Log.Info("Sent slack notification test"); + } + catch (Exception e) + { + Log.Error(e,"Failed to subscribe and publish test Slack Notification"); + } + finally + { + NotificationService.UnSubscribe(new SlackNotification(SlackApi, SlackSettings)); + } + return Response.AsJson(new JsonResponseModel { Result = true, Message = "Successfully sent a test Slack Notification! If you do not receive it please check the logs." }); + } + + private Negotiator SlackNotifications() + { + var settings = SlackSettings.GetSettings(); + return View["SlackNotifications", settings]; + } + + private Response SaveSlackNotifications() + { + var settings = this.BindAndValidate(); + if (!ModelValidationResult.IsValid) + { + return Response.AsJson(ModelValidationResult.SendJsonError()); + } + + var result = SlackSettings.SaveSettings(settings); + if (settings.Enabled) + { + NotificationService.Subscribe(new SlackNotification(SlackApi, SlackSettings)); + } + else + { + NotificationService.UnSubscribe(new SlackNotification(SlackApi, SlackSettings)); + } + + Log.Info("Saved slack settings, result: {0}", result); + return Response.AsJson(result + ? new JsonResponseModel { Result = true, Message = "Successfully Updated the Settings for Slack Notifications!" } + : new JsonResponseModel { Result = false, Message = "Could not update the settings, take a look at the logs." }); + } + } +} \ No newline at end of file diff --git a/PlexRequests.UI/PlexRequests.UI.csproj b/PlexRequests.UI/PlexRequests.UI.csproj index 50c045ad1..0abadf609 100644 --- a/PlexRequests.UI/PlexRequests.UI.csproj +++ b/PlexRequests.UI/PlexRequests.UI.csproj @@ -175,6 +175,7 @@ + @@ -186,6 +187,7 @@ + @@ -479,6 +481,9 @@ Always + + Always + web.config diff --git a/PlexRequests.UI/Validators/SlackSettingsValidator.cs b/PlexRequests.UI/Validators/SlackSettingsValidator.cs new file mode 100644 index 000000000..c3c477514 --- /dev/null +++ b/PlexRequests.UI/Validators/SlackSettingsValidator.cs @@ -0,0 +1,40 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: SlackSettingsValidator.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 FluentValidation; + +using PlexRequests.Core.SettingModels; + +namespace PlexRequests.UI.Validators +{ + public class SlackSettingsValidator : AbstractValidator + { + public SlackSettingsValidator() + { + RuleFor(request => request.WebhookUrl).NotEmpty().WithMessage("You must specify a Webhook Url"); + } + } +} \ No newline at end of file diff --git a/PlexRequests.UI/Views/Admin/Authentication.cshtml b/PlexRequests.UI/Views/Admin/Authentication.cshtml index badd56f05..d25094b27 100644 --- a/PlexRequests.UI/Views/Admin/Authentication.cshtml +++ b/PlexRequests.UI/Views/Admin/Authentication.cshtml @@ -58,7 +58,7 @@
    - +

    diff --git a/PlexRequests.UI/Views/Admin/SlackNotifications.cshtml b/PlexRequests.UI/Views/Admin/SlackNotifications.cshtml new file mode 100644 index 000000000..f24d62691 --- /dev/null +++ b/PlexRequests.UI/Views/Admin/SlackNotifications.cshtml @@ -0,0 +1,119 @@ +@using PlexRequests.UI.Helpers +@Html.Partial("_Sidebar") + +
    +
    +
    + Slack Notifications + +
    +
    + + @if (Model.Enabled) + { + + } + else + { + + } + +
    +
    + +
    + + This is the full webhook url. + Slack > Settings > Add app or integration > Build > Make a Custom Integration > Incoming Webhooks > Add Incoming Webhook. You will then have a Webhook Url +
    + +
    +
    + +
    + + You can override the default channel here +
    + +
    +
    + +
    + + You can override the default username (Plex Requests) here +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    +
    + + \ No newline at end of file diff --git a/PlexRequests.UI/Views/Admin/_Sidebar.cshtml b/PlexRequests.UI/Views/Admin/_Sidebar.cshtml index 04dc3457a..9b7fee6cc 100644 --- a/PlexRequests.UI/Views/Admin/_Sidebar.cshtml +++ b/PlexRequests.UI/Views/Admin/_Sidebar.cshtml @@ -11,6 +11,7 @@ @Html.GetSidebarUrl(Context, "/admin/emailnotification", "Email Notifications") @Html.GetSidebarUrl(Context, "/admin/pushbulletnotification", "Pushbullet Notifications") @Html.GetSidebarUrl(Context, "/admin/pushovernotification", "Pushover Notifications") + @Html.GetSidebarUrl(Context, "/admin/slacknotification", "Slack Notifications") @Html.GetSidebarUrl(Context, "/admin/logs", "Logs") @Html.GetSidebarUrl(Context, "/admin/status", "Status")
    diff --git a/PlexRequests.UI/Views/Login/Index.cshtml b/PlexRequests.UI/Views/Login/Index.cshtml index bbdf57068..f74d44d21 100644 --- a/PlexRequests.UI/Views/Login/Index.cshtml +++ b/PlexRequests.UI/Views/Login/Index.cshtml @@ -8,7 +8,7 @@ } }
    - Username + Username
    Password
    diff --git a/PlexRequests.UI/Views/Login/Register.cshtml b/PlexRequests.UI/Views/Login/Register.cshtml index c53fa0193..e41ab1ef6 100644 --- a/PlexRequests.UI/Views/Login/Register.cshtml +++ b/PlexRequests.UI/Views/Login/Register.cshtml @@ -1,5 +1,5 @@  - Username + Username
    Password
    diff --git a/PlexRequests.UI/Views/UserLogin/Index.cshtml b/PlexRequests.UI/Views/UserLogin/Index.cshtml index ebef5b3cb..eaece9a55 100644 --- a/PlexRequests.UI/Views/UserLogin/Index.cshtml +++ b/PlexRequests.UI/Views/UserLogin/Index.cshtml @@ -13,7 +13,7 @@
    - +

    From 2b20af5df0245a066ff4df9ef1eb33466124b6ab Mon Sep 17 00:00:00 2001 From: tidusjar Date: Mon, 23 May 2016 13:30:44 +0100 Subject: [PATCH 74/76] undid some small changes that was checked in by accident --- PlexRequests.UI/Views/Login/Index.cshtml | 2 +- PlexRequests.UI/Views/Login/Register.cshtml | 2 +- PlexRequests.UI/Views/UserLogin/Index.cshtml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/PlexRequests.UI/Views/Login/Index.cshtml b/PlexRequests.UI/Views/Login/Index.cshtml index f74d44d21..bbdf57068 100644 --- a/PlexRequests.UI/Views/Login/Index.cshtml +++ b/PlexRequests.UI/Views/Login/Index.cshtml @@ -8,7 +8,7 @@ } } - Username + Username
    Password
    diff --git a/PlexRequests.UI/Views/Login/Register.cshtml b/PlexRequests.UI/Views/Login/Register.cshtml index e41ab1ef6..c53fa0193 100644 --- a/PlexRequests.UI/Views/Login/Register.cshtml +++ b/PlexRequests.UI/Views/Login/Register.cshtml @@ -1,5 +1,5 @@  - Username + Username
    Password
    diff --git a/PlexRequests.UI/Views/UserLogin/Index.cshtml b/PlexRequests.UI/Views/UserLogin/Index.cshtml index eaece9a55..ebef5b3cb 100644 --- a/PlexRequests.UI/Views/UserLogin/Index.cshtml +++ b/PlexRequests.UI/Views/UserLogin/Index.cshtml @@ -13,7 +13,7 @@
    - +

    From b13aeab9fa3987f23fb0efa7a30c3ab8cf66a3ad Mon Sep 17 00:00:00 2001 From: MiuiSwitzerland Date: Tue, 24 May 2016 09:09:35 +0200 Subject: [PATCH 75/76] CSS Modification for the Plexrequests.net I wanted to make Plexrequets.net to look like my Plex Mediacenter layout, I decided to teak the Bootstrap css to look like it. Feel free to download it and change it to suit your needs. --- PlexRequests.UI/Content/bootstrap.css | 87 ++++++++++++++------------ PlexRequests.UI/Content/custom.min.css | 2 +- 2 files changed, 48 insertions(+), 41 deletions(-) diff --git a/PlexRequests.UI/Content/bootstrap.css b/PlexRequests.UI/Content/bootstrap.css index 83e3b6fa3..48de3f2e7 100644 --- a/PlexRequests.UI/Content/bootstrap.css +++ b/PlexRequests.UI/Content/bootstrap.css @@ -18,7 +18,7 @@ html { -webkit-text-size-adjust: 100%; } body { - margin: 0; + margin: 60; } article, aside, @@ -1085,12 +1085,12 @@ html { font-size: 16px; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); } -body { - font-family: "Lato", "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 15px; - line-height: 1.42857143; - color: #ebebeb; - background-color: #2b3e50; +body{ + font-family:Open Sans Regular,Helvetica Neue,Helvetica,Arial,sans-serif; + font-size:14px; + line-height:1.71428571; + color:#eee; + background-color:#1f1f1f } input, button, @@ -1152,7 +1152,7 @@ hr { margin-top: 21px; margin-bottom: 21px; border: 0; - border-top: 1px solid #596a7b; + border-top: 1px dashed #5cb85c; } .sr-only { position: absolute; @@ -2323,10 +2323,10 @@ th { border-bottom-width: 2px; } .table-striped > tbody > tr:nth-of-type(odd) { - background-color: #4e5d6c; + background-color: #333333; } .table-hover > tbody > tr:hover { - background-color: #485563; + background-color: #282828; } table col[class*="col-"] { position: static; @@ -2559,8 +2559,8 @@ output { padding: 8px 16px; font-size: 15px; line-height: 1.42857143; - color: #2b3e50; - background-color: #ffffff; + color: #fefefe; + background-color: #333333; background-image: none; border: 1px solid transparent; border-radius: .25rem; @@ -3537,7 +3537,7 @@ tbody.collapse.in { list-style: none; font-size: 15px; text-align: left; - background-color: #4e5d6c; + background-color: #282828; border: 1px solid transparent; border-radius: 0; -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); @@ -3568,7 +3568,7 @@ tbody.collapse.in { .dropdown-menu > li > a:focus { text-decoration: none; color: #ebebeb; - background-color: #485563; + background-color: #333333; } .dropdown-menu > .active > a, .dropdown-menu > .active > a:hover, @@ -3908,7 +3908,7 @@ select[multiple].input-group-sm > .input-group-btn > .btn { line-height: 1; color: #2b3e50; text-align: center; - background-color: #4e5d6c; + background-color: #333333; border: 1px solid transparent; border-radius: 0; } @@ -3994,7 +3994,7 @@ select[multiple].input-group-sm > .input-group-btn > .btn { .nav > li > a:hover, .nav > li > a:focus { text-decoration: none; - background-color: #4e5d6c; + background-color: #df691a; } .nav > li.disabled > a { color: #4e5d6c; @@ -4035,14 +4035,16 @@ select[multiple].input-group-sm > .input-group-btn > .btn { border-radius: 0 0 0 0; } .nav-tabs > li > a:hover { - border-color: #4e5d6c #4e5d6c transparent; + border-color: #df691a #df691a transparent; + border-bottom-right-radius: 0; + border-top-right-radius: 0; } .nav-tabs > li.active > a, .nav-tabs > li.active > a:hover, .nav-tabs > li.active > a:focus { color: #ebebeb; - background-color: #2b3e50; - border: 1px solid #4e5d6c; + background-color: #df691a; + border: 1px solid #df691a; border-bottom-color: transparent; cursor: default; } @@ -4077,7 +4079,7 @@ select[multiple].input-group-sm > .input-group-btn > .btn { .nav-tabs.nav-justified > .active > a, .nav-tabs.nav-justified > .active > a:hover, .nav-tabs.nav-justified > .active > a:focus { - border: 1px solid #4e5d6c; + border: 1px solid #df691a; } @media (min-width: 768px) { .nav-tabs.nav-justified > li > a { @@ -4170,10 +4172,15 @@ select[multiple].input-group-sm > .input-group-btn > .btn { border-top-left-radius: 0; } .navbar { - position: relative; - min-height: 40px; - margin-bottom: 21px; - border: 1px solid transparent; + position: fixed; + top: 0px; + right: 0px; + left: 0px; + z-index: 1000; + padding: 0px 3px; + font-size: 24px; + background-color: #000; + box-shadow: 0px 0px 0px 3px rgba(0, 0, 0, 0.2); } @media (min-width: 768px) { .navbar { @@ -4499,11 +4506,11 @@ select[multiple].input-group-sm > .input-group-btn > .btn { } } .navbar-default { - background-color: #4e5d6c; + background-color: #0a0a0a; border-color: transparent; } .navbar-default .navbar-brand { - color: #ebebeb; + color: #2828; } .navbar-default .navbar-brand:hover, .navbar-default .navbar-brand:focus { @@ -4518,14 +4525,14 @@ select[multiple].input-group-sm > .input-group-btn > .btn { } .navbar-default .navbar-nav > li > a:hover, .navbar-default .navbar-nav > li > a:focus { - color: #ebebeb; - background-color: #485563; + color: #eeeeee; + background-color: #282828; } .navbar-default .navbar-nav > .active > a, .navbar-default .navbar-nav > .active > a:hover, .navbar-default .navbar-nav > .active > a:focus { - color: #ebebeb; - background-color: #485563; + color: #f9be03; + background-color: #282828; } .navbar-default .navbar-nav > .disabled > a, .navbar-default .navbar-nav > .disabled > a:hover, @@ -4550,8 +4557,8 @@ select[multiple].input-group-sm > .input-group-btn > .btn { .navbar-default .navbar-nav > .open > a, .navbar-default .navbar-nav > .open > a:hover, .navbar-default .navbar-nav > .open > a:focus { - background-color: #485563; - color: #ebebeb; + background-color: #f9be03; + color: #282828; } @media (max-width: 767px) { .navbar-default .navbar-nav .open .dropdown-menu > li > a { @@ -4731,7 +4738,7 @@ fieldset[disabled] .navbar-inverse .btn-link:focus { line-height: 1.42857143; text-decoration: none; color: #ebebeb; - background-color: #4e5d6c; + background-color: #282828; border: 1px solid transparent; margin-left: -1px; } @@ -4752,7 +4759,7 @@ fieldset[disabled] .navbar-inverse .btn-link:focus { .pagination > li > span:focus { z-index: 2; color: #ebebeb; - background-color: #485563; + background-color: #333333; border-color: transparent; } .pagination > .active > a, @@ -4773,8 +4780,8 @@ fieldset[disabled] .navbar-inverse .btn-link:focus { .pagination > .disabled > a, .pagination > .disabled > a:hover, .pagination > .disabled > a:focus { - color: #323c46; - background-color: #4e5d6c; + color: #fefefe; + background-color: #333333; border-color: transparent; cursor: not-allowed; } @@ -5256,7 +5263,7 @@ a.thumbnail.active { display: block; padding: 10px 15px; margin-bottom: -1px; - background-color: #4e5d6c; + background-color: #282828; border: 1px solid transparent; } .list-group-item:first-child { @@ -5282,7 +5289,7 @@ a.list-group-item:focus, button.list-group-item:focus { text-decoration: none; color: #ebebeb; - background-color: #485563; + background-color: #333333; } button.list-group-item { width: 100%; @@ -6854,7 +6861,7 @@ label, } .input-addon, .input-group-addon { - color: #ebebeb; + color: #df691a; } .has-warning .help-block, .has-warning .control-label, @@ -6994,4 +7001,4 @@ a.list-group-item-danger.active:focus { } .popover-title { border: none; -} \ No newline at end of file +} diff --git a/PlexRequests.UI/Content/custom.min.css b/PlexRequests.UI/Content/custom.min.css index 9c604fa2f..6f7c7827f 100644 --- a/PlexRequests.UI/Content/custom.min.css +++ b/PlexRequests.UI/Content/custom.min.css @@ -1 +1 @@ -@media(min-width:768px){.row{position:relative;}.bottom-align-text{position:absolute;bottom:0;right:0;}}@media(max-width:48em){.home{padding-top:1rem;}}@media(min-width:48em){.home{padding-top:4rem;}}.btn{border-radius:.25rem !important;}.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;}.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:13px;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:#ffa400;text-align:center;font-size:15px;}.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;}.input-group-sm{padding-top:2px;padding-bottom:2px;}.tab-pane .form-horizontal .form-group{margin-right:15px;margin-left:15px;} \ No newline at end of file +@media(min-width:768px){.row{position:relative;}.bottom-align-text{position:absolute;bottom:0;right:0;}}@media(max-width:48em){.home{padding-top:1rem;}}@media(min-width:48em){.home{padding-top:4rem;}}.btn{border-radius:.25rem !important;}.multiSelect{background-color:#4e5d6c;}.form-control-custom{background-color:#333333 !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;}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{background:#df691a;}.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:#ffffff !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:#df691a;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:#71DD98;}.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:#A672DE;}.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:13px;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;} From 8f526e09be7757db3e9ed8bd2e712f5b530a30c8 Mon Sep 17 00:00:00 2001 From: MiuiSwitzerland Date: Tue, 24 May 2016 13:08:08 +0200 Subject: [PATCH 76/76] PlexRequests.net Plex Mediacenter Skin New Skin for the PlexRequests.net --- PlexRequests.UI/Content/bootstrap.css | 14011 ++++++++++++----------- PlexRequests.UI/Content/custom.min.css | 2 +- 2 files changed, 7008 insertions(+), 7005 deletions(-) diff --git a/PlexRequests.UI/Content/bootstrap.css b/PlexRequests.UI/Content/bootstrap.css index 48de3f2e7..88ced6fa7 100644 --- a/PlexRequests.UI/Content/bootstrap.css +++ b/PlexRequests.UI/Content/bootstrap.css @@ -1,7004 +1,7007 @@ -@import url("https://fonts.googleapis.com/css?family=Lato:300,400,700"); -/*! - * bootswatch v3.3.6 - * Homepage: http://bootswatch.com - * Copyright 2012-2016 Thomas Park - * Licensed under MIT - * Based on Bootstrap -*/ -/*! - * Bootstrap v3.3.6 (http://getbootstrap.com) - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - */ -/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */ -html { - font-family: sans-serif; - -ms-text-size-adjust: 100%; - -webkit-text-size-adjust: 100%; -} -body { - margin: 60; -} -article, -aside, -details, -figcaption, -figure, -footer, -header, -hgroup, -main, -menu, -nav, -section, -summary { - display: block; -} -audio, -canvas, -progress, -video { - display: inline-block; - vertical-align: baseline; -} -audio:not([controls]) { - display: none; - height: 0; -} -[hidden], -template { - display: none; -} -a { - background-color: transparent; -} -a:active, -a:hover { - outline: 0; -} -abbr[title] { - border-bottom: 1px dotted; -} -b, -strong { - font-weight: bold; -} -dfn { - font-style: italic; -} -h1 { - font-size: 2em; - margin: 0.67em 0; -} -mark { - background: #ff0; - color: #000; -} -small { - font-size: 80%; -} -sub, -sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; -} -sup { - top: -0.5em; -} -sub { - bottom: -0.25em; -} -img { - border: 0; -} -svg:not(:root) { - overflow: hidden; -} -figure { - margin: 1em 40px; -} -hr { - -webkit-box-sizing: content-box; - -moz-box-sizing: content-box; - box-sizing: content-box; - height: 0; -} -pre { - overflow: auto; -} -code, -kbd, -pre, -samp { - font-family: monospace, monospace; - font-size: 1em; -} -button, -input, -optgroup, -select, -textarea { - color: inherit; - font: inherit; - margin: 0; -} -button { - overflow: visible; -} -button, -select { - text-transform: none; -} -button, -html input[type="button"], -input[type="reset"], -input[type="submit"] { - -webkit-appearance: button; - cursor: pointer; -} -button[disabled], -html input[disabled] { - cursor: default; -} -button::-moz-focus-inner, -input::-moz-focus-inner { - border: 0; - padding: 0; -} -input { - line-height: normal; -} -input[type="checkbox"], -input[type="radio"] { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - padding: 0; -} -input[type="number"]::-webkit-inner-spin-button, -input[type="number"]::-webkit-outer-spin-button { - height: auto; -} -input[type="search"] { - -webkit-appearance: textfield; - -webkit-box-sizing: content-box; - -moz-box-sizing: content-box; - box-sizing: content-box; -} -input[type="search"]::-webkit-search-cancel-button, -input[type="search"]::-webkit-search-decoration { - -webkit-appearance: none; -} -fieldset { - border: 1px solid #c0c0c0; - margin: 0 2px; - padding: 0.35em 0.625em 0.75em; -} -legend { - border: 0; - padding: 0; -} -textarea { - overflow: auto; -} -optgroup { - font-weight: bold; -} -table { - border-collapse: collapse; - border-spacing: 0; -} -td, -th { - padding: 0; -} -/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */ -@media print { - *, - *:before, - *:after { - background: transparent !important; - color: #000 !important; - -webkit-box-shadow: none !important; - box-shadow: none !important; - text-shadow: none !important; - } - a, - a:visited { - text-decoration: underline; - } - a[href]:after { - content: " (" attr(href) ")"; - } - abbr[title]:after { - content: " (" attr(title) ")"; - } - a[href^="#"]:after, - a[href^="javascript:"]:after { - content: ""; - } - pre, - blockquote { - border: 1px solid #999; - page-break-inside: avoid; - } - thead { - display: table-header-group; - } - tr, - img { - page-break-inside: avoid; - } - img { - max-width: 100% !important; - } - p, - h2, - h3 { - orphans: 3; - widows: 3; - } - h2, - h3 { - page-break-after: avoid; - } - .navbar { - display: none; - } - .btn > .caret, - .dropup > .btn > .caret { - border-top-color: #000 !important; - } - .label { - border: 1px solid #000; - } - .table { - border-collapse: collapse !important; - } - .table td, - .table th { - background-color: #fff !important; - } - .table-bordered th, - .table-bordered td { - border: 1px solid #ddd !important; - } -} -@font-face { - font-family: 'Glyphicons Halflings'; - src: url('../Content/fonts/glyphicons-halflings-regular.eot'); - src: url('../Content/fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../Content/fonts/glyphicons-halflings-regular.woff2') format('woff2'), url('../Content/fonts/glyphicons-halflings-regular.woff') format('woff'), url('../Content/fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('../Content/fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg'); -} -.glyphicon { - position: relative; - top: 1px; - display: inline-block; - font-family: 'Glyphicons Halflings'; - font-style: normal; - font-weight: normal; - line-height: 1; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} -.glyphicon-asterisk:before { - content: "\002a"; -} -.glyphicon-plus:before { - content: "\002b"; -} -.glyphicon-euro:before, -.glyphicon-eur:before { - content: "\20ac"; -} -.glyphicon-minus:before { - content: "\2212"; -} -.glyphicon-cloud:before { - content: "\2601"; -} -.glyphicon-envelope:before { - content: "\2709"; -} -.glyphicon-pencil:before { - content: "\270f"; -} -.glyphicon-glass:before { - content: "\e001"; -} -.glyphicon-music:before { - content: "\e002"; -} -.glyphicon-search:before { - content: "\e003"; -} -.glyphicon-heart:before { - content: "\e005"; -} -.glyphicon-star:before { - content: "\e006"; -} -.glyphicon-star-empty:before { - content: "\e007"; -} -.glyphicon-user:before { - content: "\e008"; -} -.glyphicon-film:before { - content: "\e009"; -} -.glyphicon-th-large:before { - content: "\e010"; -} -.glyphicon-th:before { - content: "\e011"; -} -.glyphicon-th-list:before { - content: "\e012"; -} -.glyphicon-ok:before { - content: "\e013"; -} -.glyphicon-remove:before { - content: "\e014"; -} -.glyphicon-zoom-in:before { - content: "\e015"; -} -.glyphicon-zoom-out:before { - content: "\e016"; -} -.glyphicon-off:before { - content: "\e017"; -} -.glyphicon-signal:before { - content: "\e018"; -} -.glyphicon-cog:before { - content: "\e019"; -} -.glyphicon-trash:before { - content: "\e020"; -} -.glyphicon-home:before { - content: "\e021"; -} -.glyphicon-file:before { - content: "\e022"; -} -.glyphicon-time:before { - content: "\e023"; -} -.glyphicon-road:before { - content: "\e024"; -} -.glyphicon-download-alt:before { - content: "\e025"; -} -.glyphicon-download:before { - content: "\e026"; -} -.glyphicon-upload:before { - content: "\e027"; -} -.glyphicon-inbox:before { - content: "\e028"; -} -.glyphicon-play-circle:before { - content: "\e029"; -} -.glyphicon-repeat:before { - content: "\e030"; -} -.glyphicon-refresh:before { - content: "\e031"; -} -.glyphicon-list-alt:before { - content: "\e032"; -} -.glyphicon-lock:before { - content: "\e033"; -} -.glyphicon-flag:before { - content: "\e034"; -} -.glyphicon-headphones:before { - content: "\e035"; -} -.glyphicon-volume-off:before { - content: "\e036"; -} -.glyphicon-volume-down:before { - content: "\e037"; -} -.glyphicon-volume-up:before { - content: "\e038"; -} -.glyphicon-qrcode:before { - content: "\e039"; -} -.glyphicon-barcode:before { - content: "\e040"; -} -.glyphicon-tag:before { - content: "\e041"; -} -.glyphicon-tags:before { - content: "\e042"; -} -.glyphicon-book:before { - content: "\e043"; -} -.glyphicon-bookmark:before { - content: "\e044"; -} -.glyphicon-print:before { - content: "\e045"; -} -.glyphicon-camera:before { - content: "\e046"; -} -.glyphicon-font:before { - content: "\e047"; -} -.glyphicon-bold:before { - content: "\e048"; -} -.glyphicon-italic:before { - content: "\e049"; -} -.glyphicon-text-height:before { - content: "\e050"; -} -.glyphicon-text-width:before { - content: "\e051"; -} -.glyphicon-align-left:before { - content: "\e052"; -} -.glyphicon-align-center:before { - content: "\e053"; -} -.glyphicon-align-right:before { - content: "\e054"; -} -.glyphicon-align-justify:before { - content: "\e055"; -} -.glyphicon-list:before { - content: "\e056"; -} -.glyphicon-indent-left:before { - content: "\e057"; -} -.glyphicon-indent-right:before { - content: "\e058"; -} -.glyphicon-facetime-video:before { - content: "\e059"; -} -.glyphicon-picture:before { - content: "\e060"; -} -.glyphicon-map-marker:before { - content: "\e062"; -} -.glyphicon-adjust:before { - content: "\e063"; -} -.glyphicon-tint:before { - content: "\e064"; -} -.glyphicon-edit:before { - content: "\e065"; -} -.glyphicon-share:before { - content: "\e066"; -} -.glyphicon-check:before { - content: "\e067"; -} -.glyphicon-move:before { - content: "\e068"; -} -.glyphicon-step-backward:before { - content: "\e069"; -} -.glyphicon-fast-backward:before { - content: "\e070"; -} -.glyphicon-backward:before { - content: "\e071"; -} -.glyphicon-play:before { - content: "\e072"; -} -.glyphicon-pause:before { - content: "\e073"; -} -.glyphicon-stop:before { - content: "\e074"; -} -.glyphicon-forward:before { - content: "\e075"; -} -.glyphicon-fast-forward:before { - content: "\e076"; -} -.glyphicon-step-forward:before { - content: "\e077"; -} -.glyphicon-eject:before { - content: "\e078"; -} -.glyphicon-chevron-left:before { - content: "\e079"; -} -.glyphicon-chevron-right:before { - content: "\e080"; -} -.glyphicon-plus-sign:before { - content: "\e081"; -} -.glyphicon-minus-sign:before { - content: "\e082"; -} -.glyphicon-remove-sign:before { - content: "\e083"; -} -.glyphicon-ok-sign:before { - content: "\e084"; -} -.glyphicon-question-sign:before { - content: "\e085"; -} -.glyphicon-info-sign:before { - content: "\e086"; -} -.glyphicon-screenshot:before { - content: "\e087"; -} -.glyphicon-remove-circle:before { - content: "\e088"; -} -.glyphicon-ok-circle:before { - content: "\e089"; -} -.glyphicon-ban-circle:before { - content: "\e090"; -} -.glyphicon-arrow-left:before { - content: "\e091"; -} -.glyphicon-arrow-right:before { - content: "\e092"; -} -.glyphicon-arrow-up:before { - content: "\e093"; -} -.glyphicon-arrow-down:before { - content: "\e094"; -} -.glyphicon-share-alt:before { - content: "\e095"; -} -.glyphicon-resize-full:before { - content: "\e096"; -} -.glyphicon-resize-small:before { - content: "\e097"; -} -.glyphicon-exclamation-sign:before { - content: "\e101"; -} -.glyphicon-gift:before { - content: "\e102"; -} -.glyphicon-leaf:before { - content: "\e103"; -} -.glyphicon-fire:before { - content: "\e104"; -} -.glyphicon-eye-open:before { - content: "\e105"; -} -.glyphicon-eye-close:before { - content: "\e106"; -} -.glyphicon-warning-sign:before { - content: "\e107"; -} -.glyphicon-plane:before { - content: "\e108"; -} -.glyphicon-calendar:before { - content: "\e109"; -} -.glyphicon-random:before { - content: "\e110"; -} -.glyphicon-comment:before { - content: "\e111"; -} -.glyphicon-magnet:before { - content: "\e112"; -} -.glyphicon-chevron-up:before { - content: "\e113"; -} -.glyphicon-chevron-down:before { - content: "\e114"; -} -.glyphicon-retweet:before { - content: "\e115"; -} -.glyphicon-shopping-cart:before { - content: "\e116"; -} -.glyphicon-folder-close:before { - content: "\e117"; -} -.glyphicon-folder-open:before { - content: "\e118"; -} -.glyphicon-resize-vertical:before { - content: "\e119"; -} -.glyphicon-resize-horizontal:before { - content: "\e120"; -} -.glyphicon-hdd:before { - content: "\e121"; -} -.glyphicon-bullhorn:before { - content: "\e122"; -} -.glyphicon-bell:before { - content: "\e123"; -} -.glyphicon-certificate:before { - content: "\e124"; -} -.glyphicon-thumbs-up:before { - content: "\e125"; -} -.glyphicon-thumbs-down:before { - content: "\e126"; -} -.glyphicon-hand-right:before { - content: "\e127"; -} -.glyphicon-hand-left:before { - content: "\e128"; -} -.glyphicon-hand-up:before { - content: "\e129"; -} -.glyphicon-hand-down:before { - content: "\e130"; -} -.glyphicon-circle-arrow-right:before { - content: "\e131"; -} -.glyphicon-circle-arrow-left:before { - content: "\e132"; -} -.glyphicon-circle-arrow-up:before { - content: "\e133"; -} -.glyphicon-circle-arrow-down:before { - content: "\e134"; -} -.glyphicon-globe:before { - content: "\e135"; -} -.glyphicon-wrench:before { - content: "\e136"; -} -.glyphicon-tasks:before { - content: "\e137"; -} -.glyphicon-filter:before { - content: "\e138"; -} -.glyphicon-briefcase:before { - content: "\e139"; -} -.glyphicon-fullscreen:before { - content: "\e140"; -} -.glyphicon-dashboard:before { - content: "\e141"; -} -.glyphicon-paperclip:before { - content: "\e142"; -} -.glyphicon-heart-empty:before { - content: "\e143"; -} -.glyphicon-link:before { - content: "\e144"; -} -.glyphicon-phone:before { - content: "\e145"; -} -.glyphicon-pushpin:before { - content: "\e146"; -} -.glyphicon-usd:before { - content: "\e148"; -} -.glyphicon-gbp:before { - content: "\e149"; -} -.glyphicon-sort:before { - content: "\e150"; -} -.glyphicon-sort-by-alphabet:before { - content: "\e151"; -} -.glyphicon-sort-by-alphabet-alt:before { - content: "\e152"; -} -.glyphicon-sort-by-order:before { - content: "\e153"; -} -.glyphicon-sort-by-order-alt:before { - content: "\e154"; -} -.glyphicon-sort-by-attributes:before { - content: "\e155"; -} -.glyphicon-sort-by-attributes-alt:before { - content: "\e156"; -} -.glyphicon-unchecked:before { - content: "\e157"; -} -.glyphicon-expand:before { - content: "\e158"; -} -.glyphicon-collapse-down:before { - content: "\e159"; -} -.glyphicon-collapse-up:before { - content: "\e160"; -} -.glyphicon-log-in:before { - content: "\e161"; -} -.glyphicon-flash:before { - content: "\e162"; -} -.glyphicon-log-out:before { - content: "\e163"; -} -.glyphicon-new-window:before { - content: "\e164"; -} -.glyphicon-record:before { - content: "\e165"; -} -.glyphicon-save:before { - content: "\e166"; -} -.glyphicon-open:before { - content: "\e167"; -} -.glyphicon-saved:before { - content: "\e168"; -} -.glyphicon-import:before { - content: "\e169"; -} -.glyphicon-export:before { - content: "\e170"; -} -.glyphicon-send:before { - content: "\e171"; -} -.glyphicon-floppy-disk:before { - content: "\e172"; -} -.glyphicon-floppy-saved:before { - content: "\e173"; -} -.glyphicon-floppy-remove:before { - content: "\e174"; -} -.glyphicon-floppy-save:before { - content: "\e175"; -} -.glyphicon-floppy-open:before { - content: "\e176"; -} -.glyphicon-credit-card:before { - content: "\e177"; -} -.glyphicon-transfer:before { - content: "\e178"; -} -.glyphicon-cutlery:before { - content: "\e179"; -} -.glyphicon-header:before { - content: "\e180"; -} -.glyphicon-compressed:before { - content: "\e181"; -} -.glyphicon-earphone:before { - content: "\e182"; -} -.glyphicon-phone-alt:before { - content: "\e183"; -} -.glyphicon-tower:before { - content: "\e184"; -} -.glyphicon-stats:before { - content: "\e185"; -} -.glyphicon-sd-video:before { - content: "\e186"; -} -.glyphicon-hd-video:before { - content: "\e187"; -} -.glyphicon-subtitles:before { - content: "\e188"; -} -.glyphicon-sound-stereo:before { - content: "\e189"; -} -.glyphicon-sound-dolby:before { - content: "\e190"; -} -.glyphicon-sound-5-1:before { - content: "\e191"; -} -.glyphicon-sound-6-1:before { - content: "\e192"; -} -.glyphicon-sound-7-1:before { - content: "\e193"; -} -.glyphicon-copyright-mark:before { - content: "\e194"; -} -.glyphicon-registration-mark:before { - content: "\e195"; -} -.glyphicon-cloud-download:before { - content: "\e197"; -} -.glyphicon-cloud-upload:before { - content: "\e198"; -} -.glyphicon-tree-conifer:before { - content: "\e199"; -} -.glyphicon-tree-deciduous:before { - content: "\e200"; -} -.glyphicon-cd:before { - content: "\e201"; -} -.glyphicon-save-file:before { - content: "\e202"; -} -.glyphicon-open-file:before { - content: "\e203"; -} -.glyphicon-level-up:before { - content: "\e204"; -} -.glyphicon-copy:before { - content: "\e205"; -} -.glyphicon-paste:before { - content: "\e206"; -} -.glyphicon-alert:before { - content: "\e209"; -} -.glyphicon-equalizer:before { - content: "\e210"; -} -.glyphicon-king:before { - content: "\e211"; -} -.glyphicon-queen:before { - content: "\e212"; -} -.glyphicon-pawn:before { - content: "\e213"; -} -.glyphicon-bishop:before { - content: "\e214"; -} -.glyphicon-knight:before { - content: "\e215"; -} -.glyphicon-baby-formula:before { - content: "\e216"; -} -.glyphicon-tent:before { - content: "\26fa"; -} -.glyphicon-blackboard:before { - content: "\e218"; -} -.glyphicon-bed:before { - content: "\e219"; -} -.glyphicon-apple:before { - content: "\f8ff"; -} -.glyphicon-erase:before { - content: "\e221"; -} -.glyphicon-hourglass:before { - content: "\231b"; -} -.glyphicon-lamp:before { - content: "\e223"; -} -.glyphicon-duplicate:before { - content: "\e224"; -} -.glyphicon-piggy-bank:before { - content: "\e225"; -} -.glyphicon-scissors:before { - content: "\e226"; -} -.glyphicon-bitcoin:before { - content: "\e227"; -} -.glyphicon-btc:before { - content: "\e227"; -} -.glyphicon-xbt:before { - content: "\e227"; -} -.glyphicon-yen:before { - content: "\00a5"; -} -.glyphicon-jpy:before { - content: "\00a5"; -} -.glyphicon-ruble:before { - content: "\20bd"; -} -.glyphicon-rub:before { - content: "\20bd"; -} -.glyphicon-scale:before { - content: "\e230"; -} -.glyphicon-ice-lolly:before { - content: "\e231"; -} -.glyphicon-ice-lolly-tasted:before { - content: "\e232"; -} -.glyphicon-education:before { - content: "\e233"; -} -.glyphicon-option-horizontal:before { - content: "\e234"; -} -.glyphicon-option-vertical:before { - content: "\e235"; -} -.glyphicon-menu-hamburger:before { - content: "\e236"; -} -.glyphicon-modal-window:before { - content: "\e237"; -} -.glyphicon-oil:before { - content: "\e238"; -} -.glyphicon-grain:before { - content: "\e239"; -} -.glyphicon-sunglasses:before { - content: "\e240"; -} -.glyphicon-text-size:before { - content: "\e241"; -} -.glyphicon-text-color:before { - content: "\e242"; -} -.glyphicon-text-background:before { - content: "\e243"; -} -.glyphicon-object-align-top:before { - content: "\e244"; -} -.glyphicon-object-align-bottom:before { - content: "\e245"; -} -.glyphicon-object-align-horizontal:before { - content: "\e246"; -} -.glyphicon-object-align-left:before { - content: "\e247"; -} -.glyphicon-object-align-vertical:before { - content: "\e248"; -} -.glyphicon-object-align-right:before { - content: "\e249"; -} -.glyphicon-triangle-right:before { - content: "\e250"; -} -.glyphicon-triangle-left:before { - content: "\e251"; -} -.glyphicon-triangle-bottom:before { - content: "\e252"; -} -.glyphicon-triangle-top:before { - content: "\e253"; -} -.glyphicon-console:before { - content: "\e254"; -} -.glyphicon-superscript:before { - content: "\e255"; -} -.glyphicon-subscript:before { - content: "\e256"; -} -.glyphicon-menu-left:before { - content: "\e257"; -} -.glyphicon-menu-right:before { - content: "\e258"; -} -.glyphicon-menu-down:before { - content: "\e259"; -} -.glyphicon-menu-up:before { - content: "\e260"; -} -* { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} -*:before, -*:after { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} -html { - font-size: 16px; - -webkit-tap-highlight-color: rgba(0, 0, 0, 0); -} -body{ - font-family:Open Sans Regular,Helvetica Neue,Helvetica,Arial,sans-serif; - font-size:14px; - line-height:1.71428571; - color:#eee; - background-color:#1f1f1f -} -input, -button, -select, -textarea { - font-family: inherit; - font-size: inherit; - line-height: inherit; -} -a { - color: #df691a; - text-decoration: none; -} -a:hover, -a:focus { - color: #df691a; - text-decoration: underline; -} -a:focus { - outline: thin dotted; - outline: 5px auto -webkit-focus-ring-color; - outline-offset: -2px; -} -figure { - margin: 0; -} -img { - vertical-align: middle; -} -.img-responsive, -.thumbnail > img, -.thumbnail a > img, -.carousel-inner > .item > img, -.carousel-inner > .item > a > img { - display: block; - max-width: 100%; - height: auto; -} -.img-rounded { - border-radius: 0; -} -.img-thumbnail { - padding: 4px; - line-height: 1.42857143; - background-color: #2b3e50; - border: 1px solid #dddddd; - border-radius: 0; - -webkit-transition: all 0.2s ease-in-out; - -o-transition: all 0.2s ease-in-out; - transition: all 0.2s ease-in-out; - display: inline-block; - max-width: 100%; - height: auto; -} -.img-circle { - border-radius: 50%; -} -hr { - margin-top: 21px; - margin-bottom: 21px; - border: 0; - border-top: 1px dashed #5cb85c; -} -.sr-only { - position: absolute; - width: 1px; - height: 1px; - margin: -1px; - padding: 0; - overflow: hidden; - clip: rect(0, 0, 0, 0); - border: 0; -} -.sr-only-focusable:active, -.sr-only-focusable:focus { - position: static; - width: auto; - height: auto; - margin: 0; - overflow: visible; - clip: auto; -} -[role="button"] { - cursor: pointer; -} -h1, -h2, -h3, -h4, -h5, -h6, -.h1, -.h2, -.h3, -.h4, -.h5, -.h6 { - font-family: inherit; - font-weight: 400; - line-height: 1.1; - color: inherit; -} -h1 small, -h2 small, -h3 small, -h4 small, -h5 small, -h6 small, -.h1 small, -.h2 small, -.h3 small, -.h4 small, -.h5 small, -.h6 small, -h1 .small, -h2 .small, -h3 .small, -h4 .small, -h5 .small, -h6 .small, -.h1 .small, -.h2 .small, -.h3 .small, -.h4 .small, -.h5 .small, -.h6 .small { - font-weight: normal; - line-height: 1; - color: #ebebeb; -} -h1, -.h1, -h2, -.h2, -h3, -.h3 { - margin-top: 21px; - margin-bottom: 10.5px; -} -h1 small, -.h1 small, -h2 small, -.h2 small, -h3 small, -.h3 small, -h1 .small, -.h1 .small, -h2 .small, -.h2 .small, -h3 .small, -.h3 .small { - font-size: 65%; -} -h4, -.h4, -h5, -.h5, -h6, -.h6 { - margin-top: 10.5px; - margin-bottom: 10.5px; -} -h4 small, -.h4 small, -h5 small, -.h5 small, -h6 small, -.h6 small, -h4 .small, -.h4 .small, -h5 .small, -.h5 .small, -h6 .small, -.h6 .small { - font-size: 75%; -} -h1, -.h1 { - font-size: 39px; -} -h2, -.h2 { - font-size: 32px; -} -h3, -.h3 { - font-size: 26px; -} -h4, -.h4 { - font-size: 19px; -} -h5, -.h5 { - font-size: 15px; -} -h6, -.h6 { - font-size: 13px; -} -p { - margin: 0 0 10.5px; -} -.lead { - margin-bottom: 21px; - font-size: 17px; - font-weight: 300; - line-height: 1.4; -} -@media (min-width: 768px) { - .lead { - font-size: 22.5px; - } -} -small, -.small { - font-size: 80%; -} -mark, -.mark { - background-color: #f0ad4e; - padding: .2em; -} -.text-left { - text-align: left; -} -.text-right { - text-align: right; -} -.text-center { - text-align: center; -} -.text-justify { - text-align: justify; -} -.text-nowrap { - white-space: nowrap; -} -.text-lowercase { - text-transform: lowercase; -} -.text-uppercase { - text-transform: uppercase; -} -.text-capitalize { - text-transform: capitalize; -} -.text-muted { - color: #4e5d6c; -} -.text-primary { - color: #df691a; -} -a.text-primary:hover, -a.text-primary:focus { - color: #b15315; -} -.text-success { - color: #ebebeb; -} -a.text-success:hover, -a.text-success:focus { - color: #d2d2d2; -} -.text-info { - color: #ebebeb; -} -a.text-info:hover, -a.text-info:focus { - color: #d2d2d2; -} -.text-warning { - color: #ebebeb; -} -a.text-warning:hover, -a.text-warning:focus { - color: #d2d2d2; -} -.text-danger { - color: #ebebeb; -} -a.text-danger:hover, -a.text-danger:focus { - color: #d2d2d2; -} -.bg-primary { - color: #fff; - background-color: #df691a; -} -a.bg-primary:hover, -a.bg-primary:focus { - background-color: #b15315; -} -.bg-success { - background-color: #5cb85c; -} -a.bg-success:hover, -a.bg-success:focus { - background-color: #449d44; -} -.bg-info { - background-color: #5bc0de; -} -a.bg-info:hover, -a.bg-info:focus { - background-color: #31b0d5; -} -.bg-warning { - background-color: #f0ad4e; -} -a.bg-warning:hover, -a.bg-warning:focus { - background-color: #ec971f; -} -.bg-danger { - background-color: #d9534f; -} -a.bg-danger:hover, -a.bg-danger:focus { - background-color: #c9302c; -} -.page-header { - padding-bottom: 9.5px; - margin: 42px 0 21px; - border-bottom: 1px solid #ebebeb; -} -ul, -ol { - margin-top: 0; - margin-bottom: 10.5px; -} -ul ul, -ol ul, -ul ol, -ol ol { - margin-bottom: 0; -} -.list-unstyled { - padding-left: 0; - list-style: none; -} -.list-inline { - padding-left: 0; - list-style: none; - margin-left: -5px; -} -.list-inline > li { - display: inline-block; - padding-left: 5px; - padding-right: 5px; -} -dl { - margin-top: 0; - margin-bottom: 21px; -} -dt, -dd { - line-height: 1.42857143; -} -dt { - font-weight: bold; -} -dd { - margin-left: 0; -} -@media (min-width: 768px) { - .dl-horizontal dt { - float: left; - width: 160px; - clear: left; - text-align: right; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - .dl-horizontal dd { - margin-left: 180px; - } -} -abbr[title], -abbr[data-original-title] { - cursor: help; - border-bottom: 1px dotted #4e5d6c; -} -.initialism { - font-size: 90%; - text-transform: uppercase; -} -blockquote { - padding: 10.5px 21px; - margin: 0 0 21px; - font-size: 18.75px; - border-left: 5px solid #4e5d6c; -} -blockquote p:last-child, -blockquote ul:last-child, -blockquote ol:last-child { - margin-bottom: 0; -} -blockquote footer, -blockquote small, -blockquote .small { - display: block; - font-size: 80%; - line-height: 1.42857143; - color: #ebebeb; -} -blockquote footer:before, -blockquote small:before, -blockquote .small:before { - content: '\2014 \00A0'; -} -.blockquote-reverse, -blockquote.pull-right { - padding-right: 15px; - padding-left: 0; - border-right: 5px solid #4e5d6c; - border-left: 0; - text-align: right; -} -.blockquote-reverse footer:before, -blockquote.pull-right footer:before, -.blockquote-reverse small:before, -blockquote.pull-right small:before, -.blockquote-reverse .small:before, -blockquote.pull-right .small:before { - content: ''; -} -.blockquote-reverse footer:after, -blockquote.pull-right footer:after, -.blockquote-reverse small:after, -blockquote.pull-right small:after, -.blockquote-reverse .small:after, -blockquote.pull-right .small:after { - content: '\00A0 \2014'; -} -address { - margin-bottom: 21px; - font-style: normal; - line-height: 1.42857143; -} -code, -kbd, -pre, -samp { - font-family: Menlo, Monaco, Consolas, "Courier New", monospace; -} -code { - padding: 2px 4px; - font-size: 90%; - color: #c7254e; - background-color: #f9f2f4; - border-radius: 0; -} -kbd { - padding: 2px 4px; - font-size: 90%; - color: #ffffff; - background-color: #333333; - border-radius: 0; - -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); - box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); -} -kbd kbd { - padding: 0; - font-size: 100%; - font-weight: bold; - -webkit-box-shadow: none; - box-shadow: none; -} -pre { - display: block; - padding: 10px; - margin: 0 0 10.5px; - font-size: 14px; - line-height: 1.42857143; - word-break: break-all; - word-wrap: break-word; - color: #333333; - background-color: #f5f5f5; - border: 1px solid #cccccc; - border-radius: 0; -} -pre code { - padding: 0; - font-size: inherit; - color: inherit; - white-space: pre-wrap; - background-color: transparent; - border-radius: 0; -} -.pre-scrollable { - max-height: 340px; - overflow-y: scroll; -} -.container { - margin-right: auto; - margin-left: auto; - padding-left: .9375rem; - padding-right: .9375rem; -} -@media (min-width: 768px) { - .container { - width: 750px; - } -} -@media (min-width: 992px) { - .container { - width: 970px; - } -} -@media (min-width: 1200px) { - .container { - width: 1170px; - } -} -.container-fluid { - margin-right: auto; - margin-left: auto; - padding-left: 15px; - padding-right: 15px; -} -.row { - margin-left: -15px; - margin-right: -15px; -} -.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 { - position: relative; - min-height: 1px; - padding-left: 15px; - padding-right: 15px; -} -.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12 { - float: left; -} -.col-xs-12 { - width: 100%; -} -.col-xs-11 { - width: 91.66666667%; -} -.col-xs-10 { - width: 83.33333333%; -} -.col-xs-9 { - width: 75%; -} -.col-xs-8 { - width: 66.66666667%; -} -.col-xs-7 { - width: 58.33333333%; -} -.col-xs-6 { - width: 50%; -} -.col-xs-5 { - width: 41.66666667%; -} -.col-xs-4 { - width: 33.33333333%; -} -.col-xs-3 { - width: 25%; -} -.col-xs-2 { - width: 16.66666667%; -} -.col-xs-1 { - width: 8.33333333%; -} -.col-xs-pull-12 { - right: 100%; -} -.col-xs-pull-11 { - right: 91.66666667%; -} -.col-xs-pull-10 { - right: 83.33333333%; -} -.col-xs-pull-9 { - right: 75%; -} -.col-xs-pull-8 { - right: 66.66666667%; -} -.col-xs-pull-7 { - right: 58.33333333%; -} -.col-xs-pull-6 { - right: 50%; -} -.col-xs-pull-5 { - right: 41.66666667%; -} -.col-xs-pull-4 { - right: 33.33333333%; -} -.col-xs-pull-3 { - right: 25%; -} -.col-xs-pull-2 { - right: 16.66666667%; -} -.col-xs-pull-1 { - right: 8.33333333%; -} -.col-xs-pull-0 { - right: auto; -} -.col-xs-push-12 { - left: 100%; -} -.col-xs-push-11 { - left: 91.66666667%; -} -.col-xs-push-10 { - left: 83.33333333%; -} -.col-xs-push-9 { - left: 75%; -} -.col-xs-push-8 { - left: 66.66666667%; -} -.col-xs-push-7 { - left: 58.33333333%; -} -.col-xs-push-6 { - left: 50%; -} -.col-xs-push-5 { - left: 41.66666667%; -} -.col-xs-push-4 { - left: 33.33333333%; -} -.col-xs-push-3 { - left: 25%; -} -.col-xs-push-2 { - left: 16.66666667%; -} -.col-xs-push-1 { - left: 8.33333333%; -} -.col-xs-push-0 { - left: auto; -} -.col-xs-offset-12 { - margin-left: 100%; -} -.col-xs-offset-11 { - margin-left: 91.66666667%; -} -.col-xs-offset-10 { - margin-left: 83.33333333%; -} -.col-xs-offset-9 { - margin-left: 75%; -} -.col-xs-offset-8 { - margin-left: 66.66666667%; -} -.col-xs-offset-7 { - margin-left: 58.33333333%; -} -.col-xs-offset-6 { - margin-left: 50%; -} -.col-xs-offset-5 { - margin-left: 41.66666667%; -} -.col-xs-offset-4 { - margin-left: 33.33333333%; -} -.col-xs-offset-3 { - margin-left: 25%; -} -.col-xs-offset-2 { - margin-left: 16.66666667%; -} -.col-xs-offset-1 { - margin-left: 8.33333333%; -} -.col-xs-offset-0 { - margin-left: 0%; -} -@media (min-width: 768px) { - .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12 { - float: left; - } - .col-sm-12 { - width: 100%; - } - .col-sm-11 { - width: 91.66666667%; - } - .col-sm-10 { - width: 83.33333333%; - } - .col-sm-9 { - width: 75%; - } - .col-sm-8 { - width: 66.66666667%; - } - .col-sm-7 { - width: 58.33333333%; - } - .col-sm-6 { - width: 50%; - } - .col-sm-5 { - width: 41.66666667%; - } - .col-sm-4 { - width: 33.33333333%; - } - .col-sm-3 { - width: 25%; - } - .col-sm-2 { - width: 16.66666667%; - } - .col-sm-1 { - width: 8.33333333%; - } - .col-sm-pull-12 { - right: 100%; - } - .col-sm-pull-11 { - right: 91.66666667%; - } - .col-sm-pull-10 { - right: 83.33333333%; - } - .col-sm-pull-9 { - right: 75%; - } - .col-sm-pull-8 { - right: 66.66666667%; - } - .col-sm-pull-7 { - right: 58.33333333%; - } - .col-sm-pull-6 { - right: 50%; - } - .col-sm-pull-5 { - right: 41.66666667%; - } - .col-sm-pull-4 { - right: 33.33333333%; - } - .col-sm-pull-3 { - right: 25%; - } - .col-sm-pull-2 { - right: 16.66666667%; - } - .col-sm-pull-1 { - right: 8.33333333%; - } - .col-sm-pull-0 { - right: auto; - } - .col-sm-push-12 { - left: 100%; - } - .col-sm-push-11 { - left: 91.66666667%; - } - .col-sm-push-10 { - left: 83.33333333%; - } - .col-sm-push-9 { - left: 75%; - } - .col-sm-push-8 { - left: 66.66666667%; - } - .col-sm-push-7 { - left: 58.33333333%; - } - .col-sm-push-6 { - left: 50%; - } - .col-sm-push-5 { - left: 41.66666667%; - } - .col-sm-push-4 { - left: 33.33333333%; - } - .col-sm-push-3 { - left: 25%; - } - .col-sm-push-2 { - left: 16.66666667%; - } - .col-sm-push-1 { - left: 8.33333333%; - } - .col-sm-push-0 { - left: auto; - } - .col-sm-offset-12 { - margin-left: 100%; - } - .col-sm-offset-11 { - margin-left: 91.66666667%; - } - .col-sm-offset-10 { - margin-left: 83.33333333%; - } - .col-sm-offset-9 { - margin-left: 75%; - } - .col-sm-offset-8 { - margin-left: 66.66666667%; - } - .col-sm-offset-7 { - margin-left: 58.33333333%; - } - .col-sm-offset-6 { - margin-left: 50%; - } - .col-sm-offset-5 { - margin-left: 41.66666667%; - } - .col-sm-offset-4 { - margin-left: 33.33333333%; - } - .col-sm-offset-3 { - margin-left: 25%; - } - .col-sm-offset-2 { - margin-left: 16.66666667%; - } - .col-sm-offset-1 { - margin-left: 8.33333333%; - } - .col-sm-offset-0 { - margin-left: 0%; - } -} -@media (min-width: 992px) { - .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12 { - float: left; - } - .col-md-12 { - width: 100%; - } - .col-md-11 { - width: 91.66666667%; - } - .col-md-10 { - width: 83.33333333%; - } - .col-md-9 { - width: 75%; - } - .col-md-8 { - width: 66.66666667%; - } - .col-md-7 { - width: 58.33333333%; - } - .col-md-6 { - width: 50%; - } - .col-md-5 { - width: 41.66666667%; - } - .col-md-4 { - width: 33.33333333%; - } - .col-md-3 { - width: 25%; - } - .col-md-2 { - width: 16.66666667%; - } - .col-md-1 { - width: 8.33333333%; - } - .col-md-pull-12 { - right: 100%; - } - .col-md-pull-11 { - right: 91.66666667%; - } - .col-md-pull-10 { - right: 83.33333333%; - } - .col-md-pull-9 { - right: 75%; - } - .col-md-pull-8 { - right: 66.66666667%; - } - .col-md-pull-7 { - right: 58.33333333%; - } - .col-md-pull-6 { - right: 50%; - } - .col-md-pull-5 { - right: 41.66666667%; - } - .col-md-pull-4 { - right: 33.33333333%; - } - .col-md-pull-3 { - right: 25%; - } - .col-md-pull-2 { - right: 16.66666667%; - } - .col-md-pull-1 { - right: 8.33333333%; - } - .col-md-pull-0 { - right: auto; - } - .col-md-push-12 { - left: 100%; - } - .col-md-push-11 { - left: 91.66666667%; - } - .col-md-push-10 { - left: 83.33333333%; - } - .col-md-push-9 { - left: 75%; - } - .col-md-push-8 { - left: 66.66666667%; - } - .col-md-push-7 { - left: 58.33333333%; - } - .col-md-push-6 { - left: 50%; - } - .col-md-push-5 { - left: 41.66666667%; - } - .col-md-push-4 { - left: 33.33333333%; - } - .col-md-push-3 { - left: 25%; - } - .col-md-push-2 { - left: 16.66666667%; - } - .col-md-push-1 { - left: 8.33333333%; - } - .col-md-push-0 { - left: auto; - } - .col-md-offset-12 { - margin-left: 100%; - } - .col-md-offset-11 { - margin-left: 91.66666667%; - } - .col-md-offset-10 { - margin-left: 83.33333333%; - } - .col-md-offset-9 { - margin-left: 75%; - } - .col-md-offset-8 { - margin-left: 66.66666667%; - } - .col-md-offset-7 { - margin-left: 58.33333333%; - } - .col-md-offset-6 { - margin-left: 50%; - } - .col-md-offset-5 { - margin-left: 41.66666667%; - } - .col-md-offset-4 { - margin-left: 33.33333333%; - } - .col-md-offset-3 { - margin-left: 25%; - } - .col-md-offset-2 { - margin-left: 16.66666667%; - } - .col-md-offset-1 { - margin-left: 8.33333333%; - } - .col-md-offset-0 { - margin-left: 0%; - } -} -@media (min-width: 1200px) { - .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12 { - float: left; - } - .col-lg-12 { - width: 100%; - } - .col-lg-11 { - width: 91.66666667%; - } - .col-lg-10 { - width: 83.33333333%; - } - .col-lg-9 { - width: 75%; - } - .col-lg-8 { - width: 66.66666667%; - } - .col-lg-7 { - width: 58.33333333%; - } - .col-lg-6 { - width: 50%; - } - .col-lg-5 { - width: 41.66666667%; - } - .col-lg-4 { - width: 33.33333333%; - } - .col-lg-3 { - width: 25%; - } - .col-lg-2 { - width: 16.66666667%; - } - .col-lg-1 { - width: 8.33333333%; - } - .col-lg-pull-12 { - right: 100%; - } - .col-lg-pull-11 { - right: 91.66666667%; - } - .col-lg-pull-10 { - right: 83.33333333%; - } - .col-lg-pull-9 { - right: 75%; - } - .col-lg-pull-8 { - right: 66.66666667%; - } - .col-lg-pull-7 { - right: 58.33333333%; - } - .col-lg-pull-6 { - right: 50%; - } - .col-lg-pull-5 { - right: 41.66666667%; - } - .col-lg-pull-4 { - right: 33.33333333%; - } - .col-lg-pull-3 { - right: 25%; - } - .col-lg-pull-2 { - right: 16.66666667%; - } - .col-lg-pull-1 { - right: 8.33333333%; - } - .col-lg-pull-0 { - right: auto; - } - .col-lg-push-12 { - left: 100%; - } - .col-lg-push-11 { - left: 91.66666667%; - } - .col-lg-push-10 { - left: 83.33333333%; - } - .col-lg-push-9 { - left: 75%; - } - .col-lg-push-8 { - left: 66.66666667%; - } - .col-lg-push-7 { - left: 58.33333333%; - } - .col-lg-push-6 { - left: 50%; - } - .col-lg-push-5 { - left: 41.66666667%; - } - .col-lg-push-4 { - left: 33.33333333%; - } - .col-lg-push-3 { - left: 25%; - } - .col-lg-push-2 { - left: 16.66666667%; - } - .col-lg-push-1 { - left: 8.33333333%; - } - .col-lg-push-0 { - left: auto; - } - .col-lg-offset-12 { - margin-left: 100%; - } - .col-lg-offset-11 { - margin-left: 91.66666667%; - } - .col-lg-offset-10 { - margin-left: 83.33333333%; - } - .col-lg-offset-9 { - margin-left: 75%; - } - .col-lg-offset-8 { - margin-left: 66.66666667%; - } - .col-lg-offset-7 { - margin-left: 58.33333333%; - } - .col-lg-offset-6 { - margin-left: 50%; - } - .col-lg-offset-5 { - margin-left: 41.66666667%; - } - .col-lg-offset-4 { - margin-left: 33.33333333%; - } - .col-lg-offset-3 { - margin-left: 25%; - } - .col-lg-offset-2 { - margin-left: 16.66666667%; - } - .col-lg-offset-1 { - margin-left: 8.33333333%; - } - .col-lg-offset-0 { - margin-left: 0%; - } -} -table { - background-color: transparent; -} -caption { - padding-top: 6px; - padding-bottom: 6px; - color: #4e5d6c; - text-align: left; -} -th { - text-align: left; -} -.table { - width: 100%; - max-width: 100%; - margin-bottom: 21px; -} -.table > thead > tr > th, -.table > tbody > tr > th, -.table > tfoot > tr > th, -.table > thead > tr > td, -.table > tbody > tr > td, -.table > tfoot > tr > td { - padding: 6px; - line-height: 1.42857143; - vertical-align: top; - border-top: 1px solid #4e5d6c; -} -.table > thead > tr > th { - vertical-align: bottom; - border-bottom: 2px solid #4e5d6c; -} -.table > caption + thead > tr:first-child > th, -.table > colgroup + thead > tr:first-child > th, -.table > thead:first-child > tr:first-child > th, -.table > caption + thead > tr:first-child > td, -.table > colgroup + thead > tr:first-child > td, -.table > thead:first-child > tr:first-child > td { - border-top: 0; -} -.table > tbody + tbody { - border-top: 2px solid #4e5d6c; -} -.table .table { - background-color: #2b3e50; -} -.table-condensed > thead > tr > th, -.table-condensed > tbody > tr > th, -.table-condensed > tfoot > tr > th, -.table-condensed > thead > tr > td, -.table-condensed > tbody > tr > td, -.table-condensed > tfoot > tr > td { - padding: 3px; -} -.table-bordered { - border: 1px solid #4e5d6c; -} -.table-bordered > thead > tr > th, -.table-bordered > tbody > tr > th, -.table-bordered > tfoot > tr > th, -.table-bordered > thead > tr > td, -.table-bordered > tbody > tr > td, -.table-bordered > tfoot > tr > td { - border: 1px solid #4e5d6c; -} -.table-bordered > thead > tr > th, -.table-bordered > thead > tr > td { - border-bottom-width: 2px; -} -.table-striped > tbody > tr:nth-of-type(odd) { - background-color: #333333; -} -.table-hover > tbody > tr:hover { - background-color: #282828; -} -table col[class*="col-"] { - position: static; - float: none; - display: table-column; -} -table td[class*="col-"], -table th[class*="col-"] { - position: static; - float: none; - display: table-cell; -} -.table > thead > tr > td.active, -.table > tbody > tr > td.active, -.table > tfoot > tr > td.active, -.table > thead > tr > th.active, -.table > tbody > tr > th.active, -.table > tfoot > tr > th.active, -.table > thead > tr.active > td, -.table > tbody > tr.active > td, -.table > tfoot > tr.active > td, -.table > thead > tr.active > th, -.table > tbody > tr.active > th, -.table > tfoot > tr.active > th { - background-color: #485563; -} -.table-hover > tbody > tr > td.active:hover, -.table-hover > tbody > tr > th.active:hover, -.table-hover > tbody > tr.active:hover > td, -.table-hover > tbody > tr:hover > .active, -.table-hover > tbody > tr.active:hover > th { - background-color: #3d4954; -} -.table > thead > tr > td.success, -.table > tbody > tr > td.success, -.table > tfoot > tr > td.success, -.table > thead > tr > th.success, -.table > tbody > tr > th.success, -.table > tfoot > tr > th.success, -.table > thead > tr.success > td, -.table > tbody > tr.success > td, -.table > tfoot > tr.success > td, -.table > thead > tr.success > th, -.table > tbody > tr.success > th, -.table > tfoot > tr.success > th { - background-color: #5cb85c; -} -.table-hover > tbody > tr > td.success:hover, -.table-hover > tbody > tr > th.success:hover, -.table-hover > tbody > tr.success:hover > td, -.table-hover > tbody > tr:hover > .success, -.table-hover > tbody > tr.success:hover > th { - background-color: #4cae4c; -} -.table > thead > tr > td.info, -.table > tbody > tr > td.info, -.table > tfoot > tr > td.info, -.table > thead > tr > th.info, -.table > tbody > tr > th.info, -.table > tfoot > tr > th.info, -.table > thead > tr.info > td, -.table > tbody > tr.info > td, -.table > tfoot > tr.info > td, -.table > thead > tr.info > th, -.table > tbody > tr.info > th, -.table > tfoot > tr.info > th { - background-color: #5bc0de; -} -.table-hover > tbody > tr > td.info:hover, -.table-hover > tbody > tr > th.info:hover, -.table-hover > tbody > tr.info:hover > td, -.table-hover > tbody > tr:hover > .info, -.table-hover > tbody > tr.info:hover > th { - background-color: #46b8da; -} -.table > thead > tr > td.warning, -.table > tbody > tr > td.warning, -.table > tfoot > tr > td.warning, -.table > thead > tr > th.warning, -.table > tbody > tr > th.warning, -.table > tfoot > tr > th.warning, -.table > thead > tr.warning > td, -.table > tbody > tr.warning > td, -.table > tfoot > tr.warning > td, -.table > thead > tr.warning > th, -.table > tbody > tr.warning > th, -.table > tfoot > tr.warning > th { - background-color: #f0ad4e; -} -.table-hover > tbody > tr > td.warning:hover, -.table-hover > tbody > tr > th.warning:hover, -.table-hover > tbody > tr.warning:hover > td, -.table-hover > tbody > tr:hover > .warning, -.table-hover > tbody > tr.warning:hover > th { - background-color: #eea236; -} -.table > thead > tr > td.danger, -.table > tbody > tr > td.danger, -.table > tfoot > tr > td.danger, -.table > thead > tr > th.danger, -.table > tbody > tr > th.danger, -.table > tfoot > tr > th.danger, -.table > thead > tr.danger > td, -.table > tbody > tr.danger > td, -.table > tfoot > tr.danger > td, -.table > thead > tr.danger > th, -.table > tbody > tr.danger > th, -.table > tfoot > tr.danger > th { - background-color: #d9534f; -} -.table-hover > tbody > tr > td.danger:hover, -.table-hover > tbody > tr > th.danger:hover, -.table-hover > tbody > tr.danger:hover > td, -.table-hover > tbody > tr:hover > .danger, -.table-hover > tbody > tr.danger:hover > th { - background-color: #d43f3a; -} -.table-responsive { - overflow-x: auto; - min-height: 0.01%; -} -@media screen and (max-width: 767px) { - .table-responsive { - width: 100%; - margin-bottom: 15.75px; - overflow-y: hidden; - -ms-overflow-style: -ms-autohiding-scrollbar; - border: 1px solid #4e5d6c; - } - .table-responsive > .table { - margin-bottom: 0; - } - .table-responsive > .table > thead > tr > th, - .table-responsive > .table > tbody > tr > th, - .table-responsive > .table > tfoot > tr > th, - .table-responsive > .table > thead > tr > td, - .table-responsive > .table > tbody > tr > td, - .table-responsive > .table > tfoot > tr > td { - white-space: nowrap; - } - .table-responsive > .table-bordered { - border: 0; - } - .table-responsive > .table-bordered > thead > tr > th:first-child, - .table-responsive > .table-bordered > tbody > tr > th:first-child, - .table-responsive > .table-bordered > tfoot > tr > th:first-child, - .table-responsive > .table-bordered > thead > tr > td:first-child, - .table-responsive > .table-bordered > tbody > tr > td:first-child, - .table-responsive > .table-bordered > tfoot > tr > td:first-child { - border-left: 0; - } - .table-responsive > .table-bordered > thead > tr > th:last-child, - .table-responsive > .table-bordered > tbody > tr > th:last-child, - .table-responsive > .table-bordered > tfoot > tr > th:last-child, - .table-responsive > .table-bordered > thead > tr > td:last-child, - .table-responsive > .table-bordered > tbody > tr > td:last-child, - .table-responsive > .table-bordered > tfoot > tr > td:last-child { - border-right: 0; - } - .table-responsive > .table-bordered > tbody > tr:last-child > th, - .table-responsive > .table-bordered > tfoot > tr:last-child > th, - .table-responsive > .table-bordered > tbody > tr:last-child > td, - .table-responsive > .table-bordered > tfoot > tr:last-child > td { - border-bottom: 0; - } -} -fieldset { - padding: 0; - margin: 0; - border: 0; - min-width: 0; -} -legend { - display: block; - width: 100%; - padding: 0; - margin-bottom: 21px; - font-size: 22.5px; - line-height: inherit; - color: #ebebeb; - border: 0; - border-bottom: 1px solid #4e5d6c; -} -label { - display: inline-block; - max-width: 100%; - margin-bottom: 5px; - font-weight: bold; -} -input[type="search"] { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} -input[type="radio"], -input[type="checkbox"] { - margin: 4px 0 0; - margin-top: 1px \9; - line-height: normal; -} -input[type="file"] { - display: block; -} -input[type="range"] { - display: block; - width: 100%; -} -select[multiple], -select[size] { - height: auto; -} -input[type="file"]:focus, -input[type="radio"]:focus, -input[type="checkbox"]:focus { - outline: thin dotted; - outline: 5px auto -webkit-focus-ring-color; - outline-offset: -2px; -} -output { - display: block; - padding-top: 9px; - font-size: 15px; - line-height: 1.42857143; - color: #2b3e50; -} -.form-control { - display: block; - width: 100%; - height: 39px; - padding: 8px 16px; - font-size: 15px; - line-height: 1.42857143; - color: #fefefe; - background-color: #333333; - background-image: none; - border: 1px solid transparent; - border-radius: .25rem; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - -webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s; - -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; - transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; -} -.form-control:focus { - border-color: transparent; - outline: 0; - -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(0, 0, 0, 0.6); - box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(0, 0, 0, 0.6); -} -.form-control::-moz-placeholder { - color: #cccccc; - opacity: 1; -} -.form-control:-ms-input-placeholder { - color: #cccccc; -} -.form-control::-webkit-input-placeholder { - color: #cccccc; -} -.form-control::-ms-expand { - border: 0; - background-color: transparent; -} -.form-control[disabled], -.form-control[readonly], -fieldset[disabled] .form-control { - background-color: #ebebeb; - opacity: 1; -} -.form-control[disabled], -fieldset[disabled] .form-control { - cursor: not-allowed; -} -textarea.form-control { - height: auto; -} -input[type="search"] { - -webkit-appearance: none; -} -@media screen and (-webkit-min-device-pixel-ratio: 0) { - input[type="date"].form-control, - input[type="time"].form-control, - input[type="datetime-local"].form-control, - input[type="month"].form-control { - line-height: 39px; - } - input[type="date"].input-sm, - input[type="time"].input-sm, - input[type="datetime-local"].input-sm, - input[type="month"].input-sm, - .input-group-sm input[type="date"], - .input-group-sm input[type="time"], - .input-group-sm input[type="datetime-local"], - .input-group-sm input[type="month"] { - line-height: 30px; - } - input[type="date"].input-lg, - input[type="time"].input-lg, - input[type="datetime-local"].input-lg, - input[type="month"].input-lg, - .input-group-lg input[type="date"], - .input-group-lg input[type="time"], - .input-group-lg input[type="datetime-local"], - .input-group-lg input[type="month"] { - line-height: 52px; - } -} -.form-group { - margin-bottom: 15px; -} -.radio, -.checkbox { - position: relative; - display: block; - margin-top: 10px; - margin-bottom: 10px; -} -.radio label, -.checkbox label { - min-height: 21px; - padding-left: 20px; - margin-bottom: 0; - font-weight: normal; - cursor: pointer; -} -.radio input[type="radio"], -.radio-inline input[type="radio"], -.checkbox input[type="checkbox"], -.checkbox-inline input[type="checkbox"] { - position: absolute; - margin-left: -20px; - margin-top: 4px \9; -} -.radio + .radio, -.checkbox + .checkbox { - margin-top: -5px; -} -.radio-inline, -.checkbox-inline { - position: relative; - display: inline-block; - padding-left: 20px; - margin-bottom: 0; - vertical-align: middle; - font-weight: normal; - cursor: pointer; -} -.radio-inline + .radio-inline, -.checkbox-inline + .checkbox-inline { - margin-top: 0; - margin-left: 10px; -} -input[type="radio"][disabled], -input[type="checkbox"][disabled], -input[type="radio"].disabled, -input[type="checkbox"].disabled, -fieldset[disabled] input[type="radio"], -fieldset[disabled] input[type="checkbox"] { - cursor: not-allowed; -} -.radio-inline.disabled, -.checkbox-inline.disabled, -fieldset[disabled] .radio-inline, -fieldset[disabled] .checkbox-inline { - cursor: not-allowed; -} -.radio.disabled label, -.checkbox.disabled label, -fieldset[disabled] .radio label, -fieldset[disabled] .checkbox label { - cursor: not-allowed; -} -.form-control-static { - padding-top: 9px; - padding-bottom: 9px; - margin-bottom: 0; - min-height: 36px; -} -.form-control-static.input-lg, -.form-control-static.input-sm { - padding-left: 0; - padding-right: 0; -} -.input-sm { - height: 30px; - padding: 5px 10px; - font-size: 12px; - line-height: 1.5; - border-radius: 0; -} -select.input-sm { - height: 30px; - line-height: 30px; -} -textarea.input-sm, -select[multiple].input-sm { - height: auto; -} -.form-group-sm .form-control { - height: 30px; - padding: 5px 10px; - font-size: 12px; - line-height: 1.5; - border-radius: 0; -} -.form-group-sm select.form-control { - height: 30px; - line-height: 30px; -} -.form-group-sm textarea.form-control, -.form-group-sm select[multiple].form-control { - height: auto; -} -.form-group-sm .form-control-static { - height: 30px; - min-height: 33px; - padding: 6px 10px; - font-size: 12px; - line-height: 1.5; -} -.input-lg { - height: 52px; - padding: 12px 24px; - font-size: 19px; - line-height: 1.3333333; - border-radius: 0; -} -select.input-lg { - height: 52px; - line-height: 52px; -} -textarea.input-lg, -select[multiple].input-lg { - height: auto; -} -.form-group-lg .form-control { - height: 52px; - padding: 12px 24px; - font-size: 19px; - line-height: 1.3333333; - border-radius: 0; -} -.form-group-lg select.form-control { - height: 52px; - line-height: 52px; -} -.form-group-lg textarea.form-control, -.form-group-lg select[multiple].form-control { - height: auto; -} -.form-group-lg .form-control-static { - height: 52px; - min-height: 40px; - padding: 13px 24px; - font-size: 19px; - line-height: 1.3333333; -} -.has-feedback { - position: relative; -} -.has-feedback .form-control { - padding-right: 48.75px; -} -.form-control-feedback { - position: absolute; - top: 0; - right: 0; - z-index: 2; - display: block; - width: 39px; - height: 39px; - line-height: 39px; - text-align: center; - pointer-events: none; -} -.input-lg + .form-control-feedback, -.input-group-lg + .form-control-feedback, -.form-group-lg .form-control + .form-control-feedback { - width: 52px; - height: 52px; - line-height: 52px; -} -.input-sm + .form-control-feedback, -.input-group-sm + .form-control-feedback, -.form-group-sm .form-control + .form-control-feedback { - width: 30px; - height: 30px; - line-height: 30px; -} -.has-success .help-block, -.has-success .control-label, -.has-success .radio, -.has-success .checkbox, -.has-success .radio-inline, -.has-success .checkbox-inline, -.has-success.radio label, -.has-success.checkbox label, -.has-success.radio-inline label, -.has-success.checkbox-inline label { - color: #ebebeb; -} -.has-success .form-control { - border-color: #ebebeb; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); -} -.has-success .form-control:focus { - border-color: #d2d2d2; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ffffff; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ffffff; -} -.has-success .input-group-addon { - color: #ebebeb; - border-color: #ebebeb; - background-color: #5cb85c; -} -.has-success .form-control-feedback { - color: #ebebeb; -} -.has-warning .help-block, -.has-warning .control-label, -.has-warning .radio, -.has-warning .checkbox, -.has-warning .radio-inline, -.has-warning .checkbox-inline, -.has-warning.radio label, -.has-warning.checkbox label, -.has-warning.radio-inline label, -.has-warning.checkbox-inline label { - color: #ebebeb; -} -.has-warning .form-control { - border-color: #ebebeb; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); -} -.has-warning .form-control:focus { - border-color: #d2d2d2; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ffffff; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ffffff; -} -.has-warning .input-group-addon { - color: #ebebeb; - border-color: #ebebeb; - background-color: #f0ad4e; -} -.has-warning .form-control-feedback { - color: #ebebeb; -} -.has-error .help-block, -.has-error .control-label, -.has-error .radio, -.has-error .checkbox, -.has-error .radio-inline, -.has-error .checkbox-inline, -.has-error.radio label, -.has-error.checkbox label, -.has-error.radio-inline label, -.has-error.checkbox-inline label { - color: #ebebeb; -} -.has-error .form-control { - border-color: #ebebeb; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); -} -.has-error .form-control:focus { - border-color: #d2d2d2; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ffffff; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ffffff; -} -.has-error .input-group-addon { - color: #ebebeb; - border-color: #ebebeb; - background-color: #d9534f; -} -.has-error .form-control-feedback { - color: #ebebeb; -} -.has-feedback label ~ .form-control-feedback { - top: 26px; -} -.has-feedback label.sr-only ~ .form-control-feedback { - top: 0; -} -.help-block { - display: block; - margin-top: 5px; - margin-bottom: 10px; - color: #ffffff; -} -@media (min-width: 768px) { - .form-inline .form-group { - display: inline-block; - margin-bottom: 0; - vertical-align: middle; - } - .form-inline .form-control { - display: inline-block; - width: auto; - vertical-align: middle; - } - .form-inline .form-control-static { - display: inline-block; - } - .form-inline .input-group { - display: inline-table; - vertical-align: middle; - } - .form-inline .input-group .input-group-addon, - .form-inline .input-group .input-group-btn, - .form-inline .input-group .form-control { - width: auto; - } - .form-inline .input-group > .form-control { - width: 100%; - } - .form-inline .control-label { - margin-bottom: 0; - vertical-align: middle; - } - .form-inline .radio, - .form-inline .checkbox { - display: inline-block; - margin-top: 0; - margin-bottom: 0; - vertical-align: middle; - } - .form-inline .radio label, - .form-inline .checkbox label { - padding-left: 0; - } - .form-inline .radio input[type="radio"], - .form-inline .checkbox input[type="checkbox"] { - position: relative; - margin-left: 0; - } - .form-inline .has-feedback .form-control-feedback { - top: 0; - } -} -.form-horizontal .radio, -.form-horizontal .checkbox, -.form-horizontal .radio-inline, -.form-horizontal .checkbox-inline { - margin-top: 0; - margin-bottom: 0; - padding-top: 9px; -} -.form-horizontal .radio, -.form-horizontal .checkbox { - min-height: 30px; -} -.form-horizontal .form-group { - margin-left: -15px; - margin-right: -15px; -} -@media (min-width: 768px) { - .form-horizontal .control-label { - text-align: right; - margin-bottom: 0; - padding-top: 9px; - } -} -.form-horizontal .has-feedback .form-control-feedback { - right: 15px; -} -@media (min-width: 768px) { - .form-horizontal .form-group-lg .control-label { - padding-top: 13px; - font-size: 19px; - } -} -@media (min-width: 768px) { - .form-horizontal .form-group-sm .control-label { - padding-top: 6px; - font-size: 12px; - } -} -.btn { - display: inline-block; - margin-bottom: 0; - font-weight: normal; - text-align: center; - vertical-align: middle; - -ms-touch-action: manipulation; - touch-action: manipulation; - cursor: pointer; - background-image: none; - border: 1px solid transparent; - white-space: nowrap; - padding: 8px 16px; - font-size: 15px; - line-height: 1.42857143; - border-radius: 0; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; -} -.btn:focus, -.btn:active:focus, -.btn.active:focus, -.btn.focus, -.btn:active.focus, -.btn.active.focus { - outline: thin dotted; - outline: 5px auto -webkit-focus-ring-color; - outline-offset: -2px; -} -.btn:hover, -.btn:focus, -.btn.focus { - color: #ffffff; - text-decoration: none; -} -.btn:active, -.btn.active { - outline: 0; - background-image: none; - -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); - box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); -} -.btn.disabled, -.btn[disabled], -fieldset[disabled] .btn { - cursor: not-allowed; - opacity: 0.65; - filter: alpha(opacity=65); - -webkit-box-shadow: none; - box-shadow: none; -} -a.btn.disabled, -fieldset[disabled] a.btn { - pointer-events: none; -} -.btn-default { - color: #ffffff; - background-color: #4e5d6c; - border-color: transparent; -} -.btn-default:focus, -.btn-default.focus { - color: #ffffff; - background-color: #39444e; - border-color: rgba(0, 0, 0, 0); -} -.btn-default:hover { - color: #ffffff; - background-color: #39444e; - border-color: rgba(0, 0, 0, 0); -} -.btn-default:active, -.btn-default.active, -.open > .dropdown-toggle.btn-default { - color: #ffffff; - background-color: #39444e; - border-color: rgba(0, 0, 0, 0); -} -.btn-default:active:hover, -.btn-default.active:hover, -.open > .dropdown-toggle.btn-default:hover, -.btn-default:active:focus, -.btn-default.active:focus, -.open > .dropdown-toggle.btn-default:focus, -.btn-default:active.focus, -.btn-default.active.focus, -.open > .dropdown-toggle.btn-default.focus { - color: #ffffff; - background-color: #2a323a; - border-color: rgba(0, 0, 0, 0); -} -.btn-default:active, -.btn-default.active, -.open > .dropdown-toggle.btn-default { - background-image: none; -} -.btn-default.disabled:hover, -.btn-default[disabled]:hover, -fieldset[disabled] .btn-default:hover, -.btn-default.disabled:focus, -.btn-default[disabled]:focus, -fieldset[disabled] .btn-default:focus, -.btn-default.disabled.focus, -.btn-default[disabled].focus, -fieldset[disabled] .btn-default.focus { - background-color: #4e5d6c; - border-color: transparent; -} -.btn-default .badge { - color: #4e5d6c; - background-color: #ffffff; -} -.btn-primary { - color: #ffffff; - background-color: #df691a; - border-color: transparent; -} -.btn-primary:focus, -.btn-primary.focus { - color: #ffffff; - background-color: #b15315; - border-color: rgba(0, 0, 0, 0); -} -.btn-primary:hover { - color: #ffffff; - background-color: #b15315; - border-color: rgba(0, 0, 0, 0); -} -.btn-primary:active, -.btn-primary.active, -.open > .dropdown-toggle.btn-primary { - color: #ffffff; - background-color: #b15315; - border-color: rgba(0, 0, 0, 0); -} -.btn-primary:active:hover, -.btn-primary.active:hover, -.open > .dropdown-toggle.btn-primary:hover, -.btn-primary:active:focus, -.btn-primary.active:focus, -.open > .dropdown-toggle.btn-primary:focus, -.btn-primary:active.focus, -.btn-primary.active.focus, -.open > .dropdown-toggle.btn-primary.focus { - color: #ffffff; - background-color: #914411; - border-color: rgba(0, 0, 0, 0); -} -.btn-primary:active, -.btn-primary.active, -.open > .dropdown-toggle.btn-primary { - background-image: none; -} -.btn-primary.disabled:hover, -.btn-primary[disabled]:hover, -fieldset[disabled] .btn-primary:hover, -.btn-primary.disabled:focus, -.btn-primary[disabled]:focus, -fieldset[disabled] .btn-primary:focus, -.btn-primary.disabled.focus, -.btn-primary[disabled].focus, -fieldset[disabled] .btn-primary.focus { - background-color: #df691a; - border-color: transparent; -} -.btn-primary .badge { - color: #df691a; - background-color: #ffffff; -} -.btn-success { - color: #ffffff; - background-color: #5cb85c; - border-color: transparent; -} -.btn-success:focus, -.btn-success.focus { - color: #ffffff; - background-color: #449d44; - border-color: rgba(0, 0, 0, 0); -} -.btn-success:hover { - color: #ffffff; - background-color: #449d44; - border-color: rgba(0, 0, 0, 0); -} -.btn-success:active, -.btn-success.active, -.open > .dropdown-toggle.btn-success { - color: #ffffff; - background-color: #449d44; - border-color: rgba(0, 0, 0, 0); -} -.btn-success:active:hover, -.btn-success.active:hover, -.open > .dropdown-toggle.btn-success:hover, -.btn-success:active:focus, -.btn-success.active:focus, -.open > .dropdown-toggle.btn-success:focus, -.btn-success:active.focus, -.btn-success.active.focus, -.open > .dropdown-toggle.btn-success.focus { - color: #ffffff; - background-color: #398439; - border-color: rgba(0, 0, 0, 0); -} -.btn-success:active, -.btn-success.active, -.open > .dropdown-toggle.btn-success { - background-image: none; -} -.btn-success.disabled:hover, -.btn-success[disabled]:hover, -fieldset[disabled] .btn-success:hover, -.btn-success.disabled:focus, -.btn-success[disabled]:focus, -fieldset[disabled] .btn-success:focus, -.btn-success.disabled.focus, -.btn-success[disabled].focus, -fieldset[disabled] .btn-success.focus { - background-color: #5cb85c; - border-color: transparent; -} -.btn-success .badge { - color: #5cb85c; - background-color: #ffffff; -} -.btn-info { - color: #ffffff; - background-color: #5bc0de; - border-color: transparent; -} -.btn-info:focus, -.btn-info.focus { - color: #ffffff; - background-color: #31b0d5; - border-color: rgba(0, 0, 0, 0); -} -.btn-info:hover { - color: #ffffff; - background-color: #31b0d5; - border-color: rgba(0, 0, 0, 0); -} -.btn-info:active, -.btn-info.active, -.open > .dropdown-toggle.btn-info { - color: #ffffff; - background-color: #31b0d5; - border-color: rgba(0, 0, 0, 0); -} -.btn-info:active:hover, -.btn-info.active:hover, -.open > .dropdown-toggle.btn-info:hover, -.btn-info:active:focus, -.btn-info.active:focus, -.open > .dropdown-toggle.btn-info:focus, -.btn-info:active.focus, -.btn-info.active.focus, -.open > .dropdown-toggle.btn-info.focus { - color: #ffffff; - background-color: #269abc; - border-color: rgba(0, 0, 0, 0); -} -.btn-info:active, -.btn-info.active, -.open > .dropdown-toggle.btn-info { - background-image: none; -} -.btn-info.disabled:hover, -.btn-info[disabled]:hover, -fieldset[disabled] .btn-info:hover, -.btn-info.disabled:focus, -.btn-info[disabled]:focus, -fieldset[disabled] .btn-info:focus, -.btn-info.disabled.focus, -.btn-info[disabled].focus, -fieldset[disabled] .btn-info.focus { - background-color: #5bc0de; - border-color: transparent; -} -.btn-info .badge { - color: #5bc0de; - background-color: #ffffff; -} -.btn-warning { - color: #ffffff; - background-color: #f0ad4e; - border-color: transparent; -} -.btn-warning:focus, -.btn-warning.focus { - color: #ffffff; - background-color: #ec971f; - border-color: rgba(0, 0, 0, 0); -} -.btn-warning:hover { - color: #ffffff; - background-color: #ec971f; - border-color: rgba(0, 0, 0, 0); -} -.btn-warning:active, -.btn-warning.active, -.open > .dropdown-toggle.btn-warning { - color: #ffffff; - background-color: #ec971f; - border-color: rgba(0, 0, 0, 0); -} -.btn-warning:active:hover, -.btn-warning.active:hover, -.open > .dropdown-toggle.btn-warning:hover, -.btn-warning:active:focus, -.btn-warning.active:focus, -.open > .dropdown-toggle.btn-warning:focus, -.btn-warning:active.focus, -.btn-warning.active.focus, -.open > .dropdown-toggle.btn-warning.focus { - color: #ffffff; - background-color: #d58512; - border-color: rgba(0, 0, 0, 0); -} -.btn-warning:active, -.btn-warning.active, -.open > .dropdown-toggle.btn-warning { - background-image: none; -} -.btn-warning.disabled:hover, -.btn-warning[disabled]:hover, -fieldset[disabled] .btn-warning:hover, -.btn-warning.disabled:focus, -.btn-warning[disabled]:focus, -fieldset[disabled] .btn-warning:focus, -.btn-warning.disabled.focus, -.btn-warning[disabled].focus, -fieldset[disabled] .btn-warning.focus { - background-color: #f0ad4e; - border-color: transparent; -} -.btn-warning .badge { - color: #f0ad4e; - background-color: #ffffff; -} -.btn-danger { - color: #ffffff; - background-color: #d9534f; - border-color: transparent; -} -.btn-danger:focus, -.btn-danger.focus { - color: #ffffff; - background-color: #c9302c; - border-color: rgba(0, 0, 0, 0); -} -.btn-danger:hover { - color: #ffffff; - background-color: #c9302c; - border-color: rgba(0, 0, 0, 0); -} -.btn-danger:active, -.btn-danger.active, -.open > .dropdown-toggle.btn-danger { - color: #ffffff; - background-color: #c9302c; - border-color: rgba(0, 0, 0, 0); -} -.btn-danger:active:hover, -.btn-danger.active:hover, -.open > .dropdown-toggle.btn-danger:hover, -.btn-danger:active:focus, -.btn-danger.active:focus, -.open > .dropdown-toggle.btn-danger:focus, -.btn-danger:active.focus, -.btn-danger.active.focus, -.open > .dropdown-toggle.btn-danger.focus { - color: #ffffff; - background-color: #ac2925; - border-color: rgba(0, 0, 0, 0); -} -.btn-danger:active, -.btn-danger.active, -.open > .dropdown-toggle.btn-danger { - background-image: none; -} -.btn-danger.disabled:hover, -.btn-danger[disabled]:hover, -fieldset[disabled] .btn-danger:hover, -.btn-danger.disabled:focus, -.btn-danger[disabled]:focus, -fieldset[disabled] .btn-danger:focus, -.btn-danger.disabled.focus, -.btn-danger[disabled].focus, -fieldset[disabled] .btn-danger.focus { - background-color: #d9534f; - border-color: transparent; -} -.btn-danger .badge { - color: #d9534f; - background-color: #ffffff; -} -.btn-link { - color: #df691a; - font-weight: normal; - border-radius: 0; -} -.btn-link, -.btn-link:active, -.btn-link.active, -.btn-link[disabled], -fieldset[disabled] .btn-link { - background-color: transparent; - -webkit-box-shadow: none; - box-shadow: none; -} -.btn-link, -.btn-link:hover, -.btn-link:focus, -.btn-link:active { - border-color: transparent; -} -.btn-link:hover, -.btn-link:focus { - color: #df691a; - text-decoration: underline; - background-color: transparent; -} -.btn-link[disabled]:hover, -fieldset[disabled] .btn-link:hover, -.btn-link[disabled]:focus, -fieldset[disabled] .btn-link:focus { - color: #4e5d6c; - text-decoration: none; -} -.btn-lg, -.btn-group-lg > .btn { - padding: 12px 24px; - font-size: 19px; - line-height: 1.3333333; - border-radius: 0; -} -.btn-sm, -.btn-group-sm > .btn { - padding: 5px 10px; - font-size: 12px; - line-height: 1.5; - border-radius: 0; -} -.btn-xs, -.btn-group-xs > .btn { - padding: 1px 5px; - font-size: 12px; - line-height: 1.5; - border-radius: 0; -} -.btn-block { - display: block; - width: 100%; -} -.btn-block + .btn-block { - margin-top: 5px; -} -input[type="submit"].btn-block, -input[type="reset"].btn-block, -input[type="button"].btn-block { - width: 100%; -} -.fade { - opacity: 0; - -webkit-transition: opacity 0.15s linear; - -o-transition: opacity 0.15s linear; - transition: opacity 0.15s linear; -} -.fade.in { - opacity: 1; -} -.collapse { - display: none; -} -.collapse.in { - display: block; -} -tr.collapse.in { - display: table-row; -} -tbody.collapse.in { - display: table-row-group; -} -.collapsing { - position: relative; - height: 0; - overflow: hidden; - -webkit-transition-property: height, visibility; - -o-transition-property: height, visibility; - transition-property: height, visibility; - -webkit-transition-duration: 0.35s; - -o-transition-duration: 0.35s; - transition-duration: 0.35s; - -webkit-transition-timing-function: ease; - -o-transition-timing-function: ease; - transition-timing-function: ease; -} -.caret { - display: inline-block; - width: 0; - height: 0; - margin-left: 2px; - vertical-align: middle; - border-top: 4px dashed; - border-top: 4px solid \9; - border-right: 4px solid transparent; - border-left: 4px solid transparent; -} -.dropup, -.dropdown { - position: relative; -} -.dropdown-toggle:focus { - outline: 0; -} -.dropdown-menu { - position: absolute; - top: 100%; - left: 0; - z-index: 1000; - display: none; - float: left; - min-width: 160px; - padding: 5px 0; - margin: 2px 0 0; - list-style: none; - font-size: 15px; - text-align: left; - background-color: #282828; - border: 1px solid transparent; - border-radius: 0; - -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); - box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); - -webkit-background-clip: padding-box; - background-clip: padding-box; -} -.dropdown-menu.pull-right { - right: 0; - left: auto; -} -.dropdown-menu .divider { - height: 1px; - margin: 9.5px 0; - overflow: hidden; - background-color: #2b3e50; -} -.dropdown-menu > li > a { - display: block; - padding: 3px 20px; - clear: both; - font-weight: normal; - line-height: 1.42857143; - color: #ebebeb; - white-space: nowrap; -} -.dropdown-menu > li > a:hover, -.dropdown-menu > li > a:focus { - text-decoration: none; - color: #ebebeb; - background-color: #333333; -} -.dropdown-menu > .active > a, -.dropdown-menu > .active > a:hover, -.dropdown-menu > .active > a:focus { - color: #ffffff; - text-decoration: none; - outline: 0; - background-color: #df691a; -} -.dropdown-menu > .disabled > a, -.dropdown-menu > .disabled > a:hover, -.dropdown-menu > .disabled > a:focus { - color: #2b3e50; -} -.dropdown-menu > .disabled > a:hover, -.dropdown-menu > .disabled > a:focus { - text-decoration: none; - background-color: transparent; - background-image: none; - filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); - cursor: not-allowed; -} -.open > .dropdown-menu { - display: block; -} -.open > a { - outline: 0; -} -.dropdown-menu-right { - left: auto; - right: 0; -} -.dropdown-menu-left { - left: 0; - right: auto; -} -.dropdown-header { - display: block; - padding: 3px 20px; - font-size: 12px; - line-height: 1.42857143; - color: #2b3e50; - white-space: nowrap; -} -.dropdown-backdrop { - position: fixed; - left: 0; - right: 0; - bottom: 0; - top: 0; - z-index: 990; -} -.pull-right > .dropdown-menu { - right: 0; - left: auto; -} -.dropup .caret, -.navbar-fixed-bottom .dropdown .caret { - border-top: 0; - border-bottom: 4px dashed; - border-bottom: 4px solid \9; - content: ""; -} -.dropup .dropdown-menu, -.navbar-fixed-bottom .dropdown .dropdown-menu { - top: auto; - bottom: 100%; - margin-bottom: 2px; -} -@media (min-width: 768px) { - .navbar-right .dropdown-menu { - left: auto; - right: 0; - } - .navbar-right .dropdown-menu-left { - left: 0; - right: auto; - } -} -.btn-group, -.btn-group-vertical { - position: relative; - display: inline-block; - vertical-align: middle; -} -.btn-group > .btn, -.btn-group-vertical > .btn { - position: relative; - float: left; -} -.btn-group > .btn:hover, -.btn-group-vertical > .btn:hover, -.btn-group > .btn:focus, -.btn-group-vertical > .btn:focus, -.btn-group > .btn:active, -.btn-group-vertical > .btn:active, -.btn-group > .btn.active, -.btn-group-vertical > .btn.active { - z-index: 2; -} -.btn-group .btn + .btn, -.btn-group .btn + .btn-group, -.btn-group .btn-group + .btn, -.btn-group .btn-group + .btn-group { - margin-left: -1px; -} -.btn-toolbar { - margin-left: -5px; -} -.btn-toolbar .btn, -.btn-toolbar .btn-group, -.btn-toolbar .input-group { - float: left; -} -.btn-toolbar > .btn, -.btn-toolbar > .btn-group, -.btn-toolbar > .input-group { - margin-left: 5px; -} -.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) { - border-radius: 0; -} -.btn-group > .btn:first-child { - margin-left: 0; -} -.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) { - border-bottom-right-radius: 0; - border-top-right-radius: 0; -} -.btn-group > .btn:last-child:not(:first-child), -.btn-group > .dropdown-toggle:not(:first-child) { - border-bottom-left-radius: 0; - border-top-left-radius: 0; -} -.btn-group > .btn-group { - float: left; -} -.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn { - border-radius: 0; -} -.btn-group > .btn-group:first-child:not(:last-child) > .btn:last-child, -.btn-group > .btn-group:first-child:not(:last-child) > .dropdown-toggle { - border-bottom-right-radius: 0; - border-top-right-radius: 0; -} -.btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child { - border-bottom-left-radius: 0; - border-top-left-radius: 0; -} -.btn-group .dropdown-toggle:active, -.btn-group.open .dropdown-toggle { - outline: 0; -} -.btn-group > .btn + .dropdown-toggle { - padding-left: 8px; - padding-right: 8px; -} -.btn-group > .btn-lg + .dropdown-toggle { - padding-left: 12px; - padding-right: 12px; -} -.btn-group.open .dropdown-toggle { - -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); - box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); -} -.btn-group.open .dropdown-toggle.btn-link { - -webkit-box-shadow: none; - box-shadow: none; -} -.btn .caret { - margin-left: 0; -} -.btn-lg .caret { - border-width: 5px 5px 0; - border-bottom-width: 0; -} -.dropup .btn-lg .caret { - border-width: 0 5px 5px; -} -.btn-group-vertical > .btn, -.btn-group-vertical > .btn-group, -.btn-group-vertical > .btn-group > .btn { - display: block; - float: none; - width: 100%; - max-width: 100%; -} -.btn-group-vertical > .btn-group > .btn { - float: none; -} -.btn-group-vertical > .btn + .btn, -.btn-group-vertical > .btn + .btn-group, -.btn-group-vertical > .btn-group + .btn, -.btn-group-vertical > .btn-group + .btn-group { - margin-top: -1px; - margin-left: 0; -} -.btn-group-vertical > .btn:not(:first-child):not(:last-child) { - border-radius: 0; -} -.btn-group-vertical > .btn:first-child:not(:last-child) { - border-top-right-radius: 0; - border-top-left-radius: 0; - border-bottom-right-radius: 0; - border-bottom-left-radius: 0; -} -.btn-group-vertical > .btn:last-child:not(:first-child) { - border-top-right-radius: 0; - border-top-left-radius: 0; - border-bottom-right-radius: 0; - border-bottom-left-radius: 0; -} -.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn { - border-radius: 0; -} -.btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child, -.btn-group-vertical > .btn-group:first-child:not(:last-child) > .dropdown-toggle { - border-bottom-right-radius: 0; - border-bottom-left-radius: 0; -} -.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child { - border-top-right-radius: 0; - border-top-left-radius: 0; -} -.btn-group-justified { - display: table; - width: 100%; - table-layout: fixed; - border-collapse: separate; -} -.btn-group-justified > .btn, -.btn-group-justified > .btn-group { - float: none; - display: table-cell; - width: 1%; -} -.btn-group-justified > .btn-group .btn { - width: 100%; -} -.btn-group-justified > .btn-group .dropdown-menu { - left: auto; -} -[data-toggle="buttons"] > .btn input[type="radio"], -[data-toggle="buttons"] > .btn-group > .btn input[type="radio"], -[data-toggle="buttons"] > .btn input[type="checkbox"], -[data-toggle="buttons"] > .btn-group > .btn input[type="checkbox"] { - position: absolute; - clip: rect(0, 0, 0, 0); - pointer-events: none; -} -.input-group { - position: relative; - display: table; - border-collapse: separate; -} -.input-group[class*="col-"] { - float: none; - padding-left: 0; - padding-right: 0; -} -.input-group .form-control { - position: relative; - z-index: 2; - float: left; - width: 100%; - margin-bottom: 0; -} -.input-group .form-control:focus { - z-index: 3; -} -.input-group-lg > .form-control, -.input-group-lg > .input-group-addon, -.input-group-lg > .input-group-btn > .btn { - height: 52px; - padding: 12px 24px; - font-size: 19px; - line-height: 1.3333333; - border-radius: 0; -} -select.input-group-lg > .form-control, -select.input-group-lg > .input-group-addon, -select.input-group-lg > .input-group-btn > .btn { - height: 52px; - line-height: 52px; -} -textarea.input-group-lg > .form-control, -textarea.input-group-lg > .input-group-addon, -textarea.input-group-lg > .input-group-btn > .btn, -select[multiple].input-group-lg > .form-control, -select[multiple].input-group-lg > .input-group-addon, -select[multiple].input-group-lg > .input-group-btn > .btn { - height: auto; -} -.input-group-sm > .form-control, -.input-group-sm > .input-group-addon, -.input-group-sm > .input-group-btn > .btn { - height: 30px; - padding: 5px 10px; - font-size: 12px; - line-height: 1.5; - border-radius: 0; -} -select.input-group-sm > .form-control, -select.input-group-sm > .input-group-addon, -select.input-group-sm > .input-group-btn > .btn { - height: 30px; - line-height: 30px; -} -textarea.input-group-sm > .form-control, -textarea.input-group-sm > .input-group-addon, -textarea.input-group-sm > .input-group-btn > .btn, -select[multiple].input-group-sm > .form-control, -select[multiple].input-group-sm > .input-group-addon, -select[multiple].input-group-sm > .input-group-btn > .btn { - height: auto; -} -.input-group-addon, -.input-group-btn, -.input-group .form-control { - display: table-cell; -} -.input-group-addon:not(:first-child):not(:last-child), -.input-group-btn:not(:first-child):not(:last-child), -.input-group .form-control:not(:first-child):not(:last-child) { - border-radius: 0; -} -.input-group-addon, -.input-group-btn { - width: 1%; - white-space: nowrap; - vertical-align: middle; -} -.input-group-addon { - padding: 8px 16px; - font-size: 15px; - font-weight: normal; - line-height: 1; - color: #2b3e50; - text-align: center; - background-color: #333333; - border: 1px solid transparent; - border-radius: 0; -} -.input-group-addon.input-sm { - padding: 5px 10px; - font-size: 12px; - border-radius: 0; -} -.input-group-addon.input-lg { - padding: 12px 24px; - font-size: 19px; - border-radius: 0; -} -.input-group-addon input[type="radio"], -.input-group-addon input[type="checkbox"] { - margin-top: 0; -} -.input-group .form-control:first-child, -.input-group-addon:first-child, -.input-group-btn:first-child > .btn, -.input-group-btn:first-child > .btn-group > .btn, -.input-group-btn:first-child > .dropdown-toggle, -.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle), -.input-group-btn:last-child > .btn-group:not(:last-child) > .btn { - border-bottom-right-radius: 0; - border-top-right-radius: 0; -} -.input-group-addon:first-child { - border-right: 0; -} -.input-group .form-control:last-child, -.input-group-addon:last-child, -.input-group-btn:last-child > .btn, -.input-group-btn:last-child > .btn-group > .btn, -.input-group-btn:last-child > .dropdown-toggle, -.input-group-btn:first-child > .btn:not(:first-child), -.input-group-btn:first-child > .btn-group:not(:first-child) > .btn { - border-bottom-left-radius: 0; - border-top-left-radius: 0; -} -.input-group-addon:last-child { - border-left: 0; -} -.input-group-btn { - position: relative; - font-size: 0; - white-space: nowrap; -} -.input-group-btn > .btn { - position: relative; -} -.input-group-btn > .btn + .btn { - margin-left: -1px; -} -.input-group-btn > .btn:hover, -.input-group-btn > .btn:focus, -.input-group-btn > .btn:active { - z-index: 2; -} -.input-group-btn:first-child > .btn, -.input-group-btn:first-child > .btn-group { - margin-right: -1px; -} -.input-group-btn:last-child > .btn, -.input-group-btn:last-child > .btn-group { - z-index: 2; - margin-left: -1px; -} -.nav { - margin-bottom: 0; - padding-left: 0; - list-style: none; -} -.nav > li { - position: relative; - display: block; -} -.nav > li > a { - position: relative; - display: block; - padding: 10px 15px; -} -.nav > li > a:hover, -.nav > li > a:focus { - text-decoration: none; - background-color: #df691a; -} -.nav > li.disabled > a { - color: #4e5d6c; -} -.nav > li.disabled > a:hover, -.nav > li.disabled > a:focus { - color: #4e5d6c; - text-decoration: none; - background-color: transparent; - cursor: not-allowed; -} -.nav .open > a, -.nav .open > a:hover, -.nav .open > a:focus { - background-color: #4e5d6c; - border-color: #df691a; -} -.nav .nav-divider { - height: 1px; - margin: 9.5px 0; - overflow: hidden; - background-color: #e5e5e5; -} -.nav > li > a > img { - max-width: none; -} -.nav-tabs { - border-bottom: 1px solid transparent; -} -.nav-tabs > li { - float: left; - margin-bottom: -1px; -} -.nav-tabs > li > a { - margin-right: 2px; - line-height: 1.42857143; - border: 1px solid transparent; - border-radius: 0 0 0 0; -} -.nav-tabs > li > a:hover { - border-color: #df691a #df691a transparent; - border-bottom-right-radius: 0; - border-top-right-radius: 0; -} -.nav-tabs > li.active > a, -.nav-tabs > li.active > a:hover, -.nav-tabs > li.active > a:focus { - color: #ebebeb; - background-color: #df691a; - border: 1px solid #df691a; - border-bottom-color: transparent; - cursor: default; -} -.nav-tabs.nav-justified { - width: 100%; - border-bottom: 0; -} -.nav-tabs.nav-justified > li { - float: none; -} -.nav-tabs.nav-justified > li > a { - text-align: center; - margin-bottom: 5px; -} -.nav-tabs.nav-justified > .dropdown .dropdown-menu { - top: auto; - left: auto; -} -@media (min-width: 768px) { - .nav-tabs.nav-justified > li { - display: table-cell; - width: 1%; - } - .nav-tabs.nav-justified > li > a { - margin-bottom: 0; - } -} -.nav-tabs.nav-justified > li > a { - margin-right: 0; - border-radius: 0; -} -.nav-tabs.nav-justified > .active > a, -.nav-tabs.nav-justified > .active > a:hover, -.nav-tabs.nav-justified > .active > a:focus { - border: 1px solid #df691a; -} -@media (min-width: 768px) { - .nav-tabs.nav-justified > li > a { - border-bottom: 1px solid #4e5d6c; - border-radius: 0 0 0 0; - } - .nav-tabs.nav-justified > .active > a, - .nav-tabs.nav-justified > .active > a:hover, - .nav-tabs.nav-justified > .active > a:focus { - border-bottom-color: #4e5d6c; - } -} -.nav-pills > li { - float: left; -} -.nav-pills > li > a { - border-radius: 0; -} -.nav-pills > li + li { - margin-left: 2px; -} -.nav-pills > li.active > a, -.nav-pills > li.active > a:hover, -.nav-pills > li.active > a:focus { - color: #ffffff; - background-color: #df691a; -} -.nav-stacked > li { - float: none; -} -.nav-stacked > li + li { - margin-top: 2px; - margin-left: 0; -} -.nav-justified { - width: 100%; -} -.nav-justified > li { - float: none; -} -.nav-justified > li > a { - text-align: center; - margin-bottom: 5px; -} -.nav-justified > .dropdown .dropdown-menu { - top: auto; - left: auto; -} -@media (min-width: 768px) { - .nav-justified > li { - display: table-cell; - width: 1%; - } - .nav-justified > li > a { - margin-bottom: 0; - } -} -.nav-tabs-justified { - border-bottom: 0; -} -.nav-tabs-justified > li > a { - margin-right: 0; - border-radius: 0; -} -.nav-tabs-justified > .active > a, -.nav-tabs-justified > .active > a:hover, -.nav-tabs-justified > .active > a:focus { - border: 1px solid #4e5d6c; -} -@media (min-width: 768px) { - .nav-tabs-justified > li > a { - border-bottom: 1px solid #4e5d6c; - border-radius: 0 0 0 0; - } - .nav-tabs-justified > .active > a, - .nav-tabs-justified > .active > a:hover, - .nav-tabs-justified > .active > a:focus { - border-bottom-color: #4e5d6c; - } -} -.tab-content > .tab-pane { - display: none; -} -.tab-content > .active { - display: block; -} -.nav-tabs .dropdown-menu { - margin-top: -1px; - border-top-right-radius: 0; - border-top-left-radius: 0; -} -.navbar { - position: fixed; - top: 0px; - right: 0px; - left: 0px; - z-index: 1000; - padding: 0px 3px; - font-size: 24px; - background-color: #000; - box-shadow: 0px 0px 0px 3px rgba(0, 0, 0, 0.2); -} -@media (min-width: 768px) { - .navbar { - border-radius: 0; - } -} -@media (min-width: 768px) { - .navbar-header { - float: left; - } -} -.navbar-collapse { - overflow-x: visible; - padding-right: 15px; - padding-left: 15px; - border-top: 1px solid transparent; - -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1); - -webkit-overflow-scrolling: touch; -} -.navbar-collapse.in { - overflow-y: auto; -} -@media (min-width: 768px) { - .navbar-collapse { - width: auto; - border-top: 0; - -webkit-box-shadow: none; - box-shadow: none; - } - .navbar-collapse.collapse { - display: block !important; - height: auto !important; - padding-bottom: 0; - overflow: visible !important; - } - .navbar-collapse.in { - overflow-y: visible; - } - .navbar-fixed-top .navbar-collapse, - .navbar-static-top .navbar-collapse, - .navbar-fixed-bottom .navbar-collapse { - padding-left: 0; - padding-right: 0; - } -} -.navbar-fixed-top .navbar-collapse, -.navbar-fixed-bottom .navbar-collapse { - max-height: 340px; -} -@media (max-device-width: 480px) and (orientation: landscape) { - .navbar-fixed-top .navbar-collapse, - .navbar-fixed-bottom .navbar-collapse { - max-height: 200px; - } -} -.container > .navbar-header, -.container-fluid > .navbar-header, -.container > .navbar-collapse, -.container-fluid > .navbar-collapse { - margin-right: -15px; - margin-left: -15px; -} -@media (min-width: 768px) { - .container > .navbar-header, - .container-fluid > .navbar-header, - .container > .navbar-collapse, - .container-fluid > .navbar-collapse { - margin-right: 0; - margin-left: 0; - } -} -.navbar-static-top { - z-index: 1000; - border-width: 0 0 1px; -} -@media (min-width: 768px) { - .navbar-static-top { - border-radius: 0; - } -} -.navbar-fixed-top, -.navbar-fixed-bottom { - position: fixed; - right: 0; - left: 0; - z-index: 1030; -} -@media (min-width: 768px) { - .navbar-fixed-top, - .navbar-fixed-bottom { - border-radius: 0; - } -} -.navbar-fixed-top { - top: 0; - border-width: 0 0 1px; -} -.navbar-fixed-bottom { - bottom: 0; - margin-bottom: 0; - border-width: 1px 0 0; -} -.navbar-brand { - float: left; - padding: 9.5px 15px; - font-size: 19px; - line-height: 21px; - height: 40px; -} -.navbar-brand:hover, -.navbar-brand:focus { - text-decoration: none; -} -.navbar-brand > img { - display: block; -} -@media (min-width: 768px) { - .navbar > .container .navbar-brand, - .navbar > .container-fluid .navbar-brand { - margin-left: -15px; - } -} -.navbar-toggle { - position: relative; - float: right; - margin-right: 15px; - padding: 9px 10px; - margin-top: 3px; - margin-bottom: 3px; - background-color: transparent; - background-image: none; - border: 1px solid transparent; - border-radius: 0; -} -.navbar-toggle:focus { - outline: 0; -} -.navbar-toggle .icon-bar { - display: block; - width: 22px; - height: 2px; - border-radius: 1px; -} -.navbar-toggle .icon-bar + .icon-bar { - margin-top: 4px; -} -@media (min-width: 768px) { - .navbar-toggle { - display: none; - } -} -.navbar-nav { - margin: 4.75px -15px; -} -.navbar-nav > li > a { - padding-top: 10px; - padding-bottom: 10px; - line-height: 21px; -} -@media (max-width: 767px) { - .navbar-nav .open .dropdown-menu { - position: static; - float: none; - width: auto; - margin-top: 0; - background-color: transparent; - border: 0; - -webkit-box-shadow: none; - box-shadow: none; - } - .navbar-nav .open .dropdown-menu > li > a, - .navbar-nav .open .dropdown-menu .dropdown-header { - padding: 5px 15px 5px 25px; - } - .navbar-nav .open .dropdown-menu > li > a { - line-height: 21px; - } - .navbar-nav .open .dropdown-menu > li > a:hover, - .navbar-nav .open .dropdown-menu > li > a:focus { - background-image: none; - } -} -@media (min-width: 768px) { - .navbar-nav { - float: left; - margin: 0; - } - .navbar-nav > li { - float: left; - } - .navbar-nav > li > a { - padding-top: 9.5px; - padding-bottom: 9.5px; - } -} -.navbar-form { - margin-left: -15px; - margin-right: -15px; - padding: 10px 15px; - border-top: 1px solid transparent; - border-bottom: 1px solid transparent; - -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); - margin-top: 0.5px; - margin-bottom: 0.5px; -} -@media (min-width: 768px) { - .navbar-form .form-group { - display: inline-block; - margin-bottom: 0; - vertical-align: middle; - } - .navbar-form .form-control { - display: inline-block; - width: auto; - vertical-align: middle; - } - .navbar-form .form-control-static { - display: inline-block; - } - .navbar-form .input-group { - display: inline-table; - vertical-align: middle; - } - .navbar-form .input-group .input-group-addon, - .navbar-form .input-group .input-group-btn, - .navbar-form .input-group .form-control { - width: auto; - } - .navbar-form .input-group > .form-control { - width: 100%; - } - .navbar-form .control-label { - margin-bottom: 0; - vertical-align: middle; - } - .navbar-form .radio, - .navbar-form .checkbox { - display: inline-block; - margin-top: 0; - margin-bottom: 0; - vertical-align: middle; - } - .navbar-form .radio label, - .navbar-form .checkbox label { - padding-left: 0; - } - .navbar-form .radio input[type="radio"], - .navbar-form .checkbox input[type="checkbox"] { - position: relative; - margin-left: 0; - } - .navbar-form .has-feedback .form-control-feedback { - top: 0; - } -} -@media (max-width: 767px) { - .navbar-form .form-group { - margin-bottom: 5px; - } - .navbar-form .form-group:last-child { - margin-bottom: 0; - } -} -@media (min-width: 768px) { - .navbar-form { - width: auto; - border: 0; - margin-left: 0; - margin-right: 0; - padding-top: 0; - padding-bottom: 0; - -webkit-box-shadow: none; - box-shadow: none; - } -} -.navbar-nav > li > .dropdown-menu { - margin-top: 0; - border-top-right-radius: 0; - border-top-left-radius: 0; -} -.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu { - margin-bottom: 0; - border-top-right-radius: 0; - border-top-left-radius: 0; - border-bottom-right-radius: 0; - border-bottom-left-radius: 0; -} -.navbar-btn { - margin-top: 0.5px; - margin-bottom: 0.5px; -} -.navbar-btn.btn-sm { - margin-top: 5px; - margin-bottom: 5px; -} -.navbar-btn.btn-xs { - margin-top: 9px; - margin-bottom: 9px; -} -.navbar-text { - margin-top: 9.5px; - margin-bottom: 9.5px; -} -@media (min-width: 768px) { - .navbar-text { - float: left; - margin-left: 15px; - margin-right: 15px; - } -} -@media (min-width: 768px) { - .navbar-left { - float: left !important; - } - .navbar-right { - float: right !important; - margin-right: -15px; - } - .navbar-right ~ .navbar-right { - margin-right: 0; - } -} -.navbar-default { - background-color: #0a0a0a; - border-color: transparent; -} -.navbar-default .navbar-brand { - color: #2828; -} -.navbar-default .navbar-brand:hover, -.navbar-default .navbar-brand:focus { - color: #ebebeb; - background-color: transparent; -} -.navbar-default .navbar-text { - color: #ebebeb; -} -.navbar-default .navbar-nav > li > a { - color: #ebebeb; -} -.navbar-default .navbar-nav > li > a:hover, -.navbar-default .navbar-nav > li > a:focus { - color: #eeeeee; - background-color: #282828; -} -.navbar-default .navbar-nav > .active > a, -.navbar-default .navbar-nav > .active > a:hover, -.navbar-default .navbar-nav > .active > a:focus { - color: #f9be03; - background-color: #282828; -} -.navbar-default .navbar-nav > .disabled > a, -.navbar-default .navbar-nav > .disabled > a:hover, -.navbar-default .navbar-nav > .disabled > a:focus { - color: #cccccc; - background-color: transparent; -} -.navbar-default .navbar-toggle { - border-color: transparent; -} -.navbar-default .navbar-toggle:hover, -.navbar-default .navbar-toggle:focus { - background-color: #485563; -} -.navbar-default .navbar-toggle .icon-bar { - background-color: #ebebeb; -} -.navbar-default .navbar-collapse, -.navbar-default .navbar-form { - border-color: transparent; -} -.navbar-default .navbar-nav > .open > a, -.navbar-default .navbar-nav > .open > a:hover, -.navbar-default .navbar-nav > .open > a:focus { - background-color: #f9be03; - color: #282828; -} -@media (max-width: 767px) { - .navbar-default .navbar-nav .open .dropdown-menu > li > a { - color: #ebebeb; - } - .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover, - .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus { - color: #ebebeb; - background-color: #485563; - } - .navbar-default .navbar-nav .open .dropdown-menu > .active > a, - .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover, - .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus { - color: #ebebeb; - background-color: #485563; - } - .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a, - .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover, - .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus { - color: #cccccc; - background-color: transparent; - } -} -.navbar-default .navbar-link { - color: #ebebeb; -} -.navbar-default .navbar-link:hover { - color: #ebebeb; -} -.navbar-default .btn-link { - color: #ebebeb; -} -.navbar-default .btn-link:hover, -.navbar-default .btn-link:focus { - color: #ebebeb; -} -.navbar-default .btn-link[disabled]:hover, -fieldset[disabled] .navbar-default .btn-link:hover, -.navbar-default .btn-link[disabled]:focus, -fieldset[disabled] .navbar-default .btn-link:focus { - color: #cccccc; -} -.navbar-inverse { - background-color: #df691a; - border-color: transparent; -} -.navbar-inverse .navbar-brand { - color: #ebebeb; -} -.navbar-inverse .navbar-brand:hover, -.navbar-inverse .navbar-brand:focus { - color: #ebebeb; - background-color: transparent; -} -.navbar-inverse .navbar-text { - color: #ebebeb; -} -.navbar-inverse .navbar-nav > li > a { - color: #ebebeb; -} -.navbar-inverse .navbar-nav > li > a:hover, -.navbar-inverse .navbar-nav > li > a:focus { - color: #ebebeb; - background-color: #c85e17; -} -.navbar-inverse .navbar-nav > .active > a, -.navbar-inverse .navbar-nav > .active > a:hover, -.navbar-inverse .navbar-nav > .active > a:focus { - color: #ebebeb; - background-color: #c85e17; -} -.navbar-inverse .navbar-nav > .disabled > a, -.navbar-inverse .navbar-nav > .disabled > a:hover, -.navbar-inverse .navbar-nav > .disabled > a:focus { - color: #444444; - background-color: transparent; -} -.navbar-inverse .navbar-toggle { - border-color: transparent; -} -.navbar-inverse .navbar-toggle:hover, -.navbar-inverse .navbar-toggle:focus { - background-color: #c85e17; -} -.navbar-inverse .navbar-toggle .icon-bar { - background-color: #ebebeb; -} -.navbar-inverse .navbar-collapse, -.navbar-inverse .navbar-form { - border-color: #bf5a16; -} -.navbar-inverse .navbar-nav > .open > a, -.navbar-inverse .navbar-nav > .open > a:hover, -.navbar-inverse .navbar-nav > .open > a:focus { - background-color: #c85e17; - color: #ebebeb; -} -@media (max-width: 767px) { - .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header { - border-color: transparent; - } - .navbar-inverse .navbar-nav .open .dropdown-menu .divider { - background-color: transparent; - } - .navbar-inverse .navbar-nav .open .dropdown-menu > li > a { - color: #ebebeb; - } - .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover, - .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus { - color: #ebebeb; - background-color: #c85e17; - } - .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a, - .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover, - .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus { - color: #ebebeb; - background-color: #c85e17; - } - .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a, - .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover, - .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus { - color: #444444; - background-color: transparent; - } -} -.navbar-inverse .navbar-link { - color: #ebebeb; -} -.navbar-inverse .navbar-link:hover { - color: #ebebeb; -} -.navbar-inverse .btn-link { - color: #ebebeb; -} -.navbar-inverse .btn-link:hover, -.navbar-inverse .btn-link:focus { - color: #ebebeb; -} -.navbar-inverse .btn-link[disabled]:hover, -fieldset[disabled] .navbar-inverse .btn-link:hover, -.navbar-inverse .btn-link[disabled]:focus, -fieldset[disabled] .navbar-inverse .btn-link:focus { - color: #444444; -} -.breadcrumb { - padding: 8px 15px; - margin-bottom: 21px; - list-style: none; - background-color: #4e5d6c; - border-radius: 0; -} -.breadcrumb > li { - display: inline-block; -} -.breadcrumb > li + li:before { - content: "/\00a0"; - padding: 0 5px; - color: #ebebeb; -} -.breadcrumb > .active { - color: #ebebeb; -} -.pagination { - display: inline-block; - padding-left: 0; - margin: 21px 0; - border-radius: 0; -} -.pagination > li { - display: inline; -} -.pagination > li > a, -.pagination > li > span { - position: relative; - float: left; - padding: 8px 16px; - line-height: 1.42857143; - text-decoration: none; - color: #ebebeb; - background-color: #282828; - border: 1px solid transparent; - margin-left: -1px; -} -.pagination > li:first-child > a, -.pagination > li:first-child > span { - margin-left: 0; - border-bottom-left-radius: 0; - border-top-left-radius: 0; -} -.pagination > li:last-child > a, -.pagination > li:last-child > span { - border-bottom-right-radius: 0; - border-top-right-radius: 0; -} -.pagination > li > a:hover, -.pagination > li > span:hover, -.pagination > li > a:focus, -.pagination > li > span:focus { - z-index: 2; - color: #ebebeb; - background-color: #333333; - border-color: transparent; -} -.pagination > .active > a, -.pagination > .active > span, -.pagination > .active > a:hover, -.pagination > .active > span:hover, -.pagination > .active > a:focus, -.pagination > .active > span:focus { - z-index: 3; - color: #ebebeb; - background-color: #df691a; - border-color: transparent; - cursor: default; -} -.pagination > .disabled > span, -.pagination > .disabled > span:hover, -.pagination > .disabled > span:focus, -.pagination > .disabled > a, -.pagination > .disabled > a:hover, -.pagination > .disabled > a:focus { - color: #fefefe; - background-color: #333333; - border-color: transparent; - cursor: not-allowed; -} -.pagination-lg > li > a, -.pagination-lg > li > span { - padding: 12px 24px; - font-size: 19px; - line-height: 1.3333333; -} -.pagination-lg > li:first-child > a, -.pagination-lg > li:first-child > span { - border-bottom-left-radius: 0; - border-top-left-radius: 0; -} -.pagination-lg > li:last-child > a, -.pagination-lg > li:last-child > span { - border-bottom-right-radius: 0; - border-top-right-radius: 0; -} -.pagination-sm > li > a, -.pagination-sm > li > span { - padding: 5px 10px; - font-size: 12px; - line-height: 1.5; -} -.pagination-sm > li:first-child > a, -.pagination-sm > li:first-child > span { - border-bottom-left-radius: 0; - border-top-left-radius: 0; -} -.pagination-sm > li:last-child > a, -.pagination-sm > li:last-child > span { - border-bottom-right-radius: 0; - border-top-right-radius: 0; -} -.pager { - padding-left: 0; - margin: 21px 0; - list-style: none; - text-align: center; -} -.pager li { - display: inline; -} -.pager li > a, -.pager li > span { - display: inline-block; - padding: 5px 14px; - background-color: #4e5d6c; - border: 1px solid transparent; - border-radius: 15px; -} -.pager li > a:hover, -.pager li > a:focus { - text-decoration: none; - background-color: #485563; -} -.pager .next > a, -.pager .next > span { - float: right; -} -.pager .previous > a, -.pager .previous > span { - float: left; -} -.pager .disabled > a, -.pager .disabled > a:hover, -.pager .disabled > a:focus, -.pager .disabled > span { - color: #323c46; - background-color: #4e5d6c; - cursor: not-allowed; -} -.label { - display: inline; - padding: .2em .6em .3em; - font-size: 75%; - font-weight: bold; - line-height: 1; - color: #ffffff; - text-align: center; - white-space: nowrap; - vertical-align: baseline; - border-radius: .25em; -} -a.label:hover, -a.label:focus { - color: #ffffff; - text-decoration: none; - cursor: pointer; -} -.label:empty { - display: none; -} -.btn .label { - position: relative; - top: -1px; -} -.label-default { - background-color: #4e5d6c; -} -.label-default[href]:hover, -.label-default[href]:focus { - background-color: #39444e; -} -.label-primary { - background-color: #df691a; -} -.label-primary[href]:hover, -.label-primary[href]:focus { - background-color: #b15315; -} -.label-success { - background-color: #5cb85c; -} -.label-success[href]:hover, -.label-success[href]:focus { - background-color: #449d44; -} -.label-info { - background-color: #5bc0de; -} -.label-info[href]:hover, -.label-info[href]:focus { - background-color: #31b0d5; -} -.label-warning { - background-color: #f0ad4e; -} -.label-warning[href]:hover, -.label-warning[href]:focus { - background-color: #ec971f; -} -.label-danger { - background-color: #d9534f; -} -.label-danger[href]:hover, -.label-danger[href]:focus { - background-color: #c9302c; -} -.badge { - display: inline-block; - min-width: 10px; - padding: 3px 7px; - font-size: 12px; - font-weight: 300; - color: #ebebeb; - line-height: 1; - vertical-align: middle; - white-space: nowrap; - text-align: center; - background-color: #4e5d6c; - border-radius: 10px; -} -.badge:empty { - display: none; -} -.btn .badge { - position: relative; - top: -1px; -} -.btn-xs .badge, -.btn-group-xs > .btn .badge { - top: 0; - padding: 1px 5px; -} -a.badge:hover, -a.badge:focus { - color: #ffffff; - text-decoration: none; - cursor: pointer; -} -.list-group-item.active > .badge, -.nav-pills > .active > a > .badge { - color: #df691a; - background-color: #ffffff; -} -.list-group-item > .badge { - float: right; -} -.list-group-item > .badge + .badge { - margin-right: 5px; -} -.nav-pills > li > a > .badge { - margin-left: 3px; -} -.jumbotron { - padding-top: 30px; - padding-bottom: 30px; - margin-bottom: 30px; - color: inherit; - background-color: #4e5d6c; -} -.jumbotron h1, -.jumbotron .h1 { - color: inherit; -} -.jumbotron p { - margin-bottom: 15px; - font-size: 23px; - font-weight: 200; -} -.jumbotron > hr { - border-top-color: #39444e; -} -.container .jumbotron, -.container-fluid .jumbotron { - border-radius: 0; - padding-left: 15px; - padding-right: 15px; -} -.jumbotron .container { - max-width: 100%; -} -@media screen and (min-width: 768px) { - .jumbotron { - padding-top: 48px; - padding-bottom: 48px; - } - .container .jumbotron, - .container-fluid .jumbotron { - padding-left: 60px; - padding-right: 60px; - } - .jumbotron h1, - .jumbotron .h1 { - font-size: 68px; - } -} -.thumbnail { - display: block; - padding: 4px; - margin-bottom: 21px; - line-height: 1.42857143; - background-color: #2b3e50; - border: 1px solid #dddddd; - border-radius: 0; - -webkit-transition: border 0.2s ease-in-out; - -o-transition: border 0.2s ease-in-out; - transition: border 0.2s ease-in-out; -} -.thumbnail > img, -.thumbnail a > img { - margin-left: auto; - margin-right: auto; -} -a.thumbnail:hover, -a.thumbnail:focus, -a.thumbnail.active { - border-color: #df691a; -} -.thumbnail .caption { - padding: 9px; - color: #ebebeb; -} -.alert { - padding: 15px; - margin-bottom: 21px; - border: 1px solid transparent; - border-radius: 0; -} -.alert h4 { - margin-top: 0; - color: inherit; -} -.alert .alert-link { - font-weight: bold; -} -.alert > p, -.alert > ul { - margin-bottom: 0; -} -.alert > p + p { - margin-top: 5px; -} -.alert-dismissable, -.alert-dismissible { - padding-right: 35px; -} -.alert-dismissable .close, -.alert-dismissible .close { - position: relative; - top: -2px; - right: -21px; - color: inherit; -} -.alert-success { - background-color: #5cb85c; - border-color: transparent; - color: #ebebeb; -} -.alert-success hr { - border-top-color: rgba(0, 0, 0, 0); -} -.alert-success .alert-link { - color: #d2d2d2; -} -.alert-info { - background-color: #5bc0de; - border-color: transparent; - color: #ebebeb; -} -.alert-info hr { - border-top-color: rgba(0, 0, 0, 0); -} -.alert-info .alert-link { - color: #d2d2d2; -} -.alert-warning { - background-color: #f0ad4e; - border-color: transparent; - color: #ebebeb; -} -.alert-warning hr { - border-top-color: rgba(0, 0, 0, 0); -} -.alert-warning .alert-link { - color: #d2d2d2; -} -.alert-danger { - background-color: #d9534f; - border-color: transparent; - color: #ebebeb; -} -.alert-danger hr { - border-top-color: rgba(0, 0, 0, 0); -} -.alert-danger .alert-link { - color: #d2d2d2; -} -@-webkit-keyframes progress-bar-stripes { - from { - background-position: 40px 0; - } - to { - background-position: 0 0; - } -} -@-o-keyframes progress-bar-stripes { - from { - background-position: 40px 0; - } - to { - background-position: 0 0; - } -} -@keyframes progress-bar-stripes { - from { - background-position: 40px 0; - } - to { - background-position: 0 0; - } -} -.progress { - overflow: hidden; - height: 21px; - margin-bottom: 21px; - background-color: #4e5d6c; - border-radius: 0; - -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); - box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); -} -.progress-bar { - float: left; - width: 0%; - height: 100%; - font-size: 12px; - line-height: 21px; - color: #ffffff; - text-align: center; - background-color: #df691a; - -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); - box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); - -webkit-transition: width 0.6s ease; - -o-transition: width 0.6s ease; - transition: width 0.6s ease; -} -.progress-striped .progress-bar, -.progress-bar-striped { - background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - -webkit-background-size: 40px 40px; - background-size: 40px 40px; -} -.progress.active .progress-bar, -.progress-bar.active { - -webkit-animation: progress-bar-stripes 2s linear infinite; - -o-animation: progress-bar-stripes 2s linear infinite; - animation: progress-bar-stripes 2s linear infinite; -} -.progress-bar-success { - background-color: #5cb85c; -} -.progress-striped .progress-bar-success { - background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); -} -.progress-bar-info { - background-color: #5bc0de; -} -.progress-striped .progress-bar-info { - background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); -} -.progress-bar-warning { - background-color: #f0ad4e; -} -.progress-striped .progress-bar-warning { - background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); -} -.progress-bar-danger { - background-color: #d9534f; -} -.progress-striped .progress-bar-danger { - background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); -} -.media { - margin-top: 15px; -} -.media:first-child { - margin-top: 0; -} -.media, -.media-body { - zoom: 1; - overflow: hidden; -} -.media-body { - width: 10000px; -} -.media-object { - display: block; -} -.media-object.img-thumbnail { - max-width: none; -} -.media-right, -.media > .pull-right { - padding-left: 10px; -} -.media-left, -.media > .pull-left { - padding-right: 10px; -} -.media-left, -.media-right, -.media-body { - display: table-cell; - vertical-align: top; -} -.media-middle { - vertical-align: middle; -} -.media-bottom { - vertical-align: bottom; -} -.media-heading { - margin-top: 0; - margin-bottom: 5px; -} -.media-list { - padding-left: 0; - list-style: none; -} -.list-group { - margin-bottom: 20px; - padding-left: 0; -} -.list-group-item { - position: relative; - display: block; - padding: 10px 15px; - margin-bottom: -1px; - background-color: #282828; - border: 1px solid transparent; -} -.list-group-item:first-child { - border-top-right-radius: 0; - border-top-left-radius: 0; -} -.list-group-item:last-child { - margin-bottom: 0; - border-bottom-right-radius: 0; - border-bottom-left-radius: 0; -} -a.list-group-item, -button.list-group-item { - color: #ebebeb; -} -a.list-group-item .list-group-item-heading, -button.list-group-item .list-group-item-heading { - color: #ebebeb; -} -a.list-group-item:hover, -button.list-group-item:hover, -a.list-group-item:focus, -button.list-group-item:focus { - text-decoration: none; - color: #ebebeb; - background-color: #333333; -} -button.list-group-item { - width: 100%; - text-align: left; -} -.list-group-item.disabled, -.list-group-item.disabled:hover, -.list-group-item.disabled:focus { - background-color: #ebebeb; - color: #4e5d6c; - cursor: not-allowed; -} -.list-group-item.disabled .list-group-item-heading, -.list-group-item.disabled:hover .list-group-item-heading, -.list-group-item.disabled:focus .list-group-item-heading { - color: inherit; -} -.list-group-item.disabled .list-group-item-text, -.list-group-item.disabled:hover .list-group-item-text, -.list-group-item.disabled:focus .list-group-item-text { - color: #4e5d6c; -} -.list-group-item.active, -.list-group-item.active:hover, -.list-group-item.active:focus { - z-index: 2; - color: #ffffff; - background-color: #df691a; - border-color: #df691a; -} -.list-group-item.active .list-group-item-heading, -.list-group-item.active:hover .list-group-item-heading, -.list-group-item.active:focus .list-group-item-heading, -.list-group-item.active .list-group-item-heading > small, -.list-group-item.active:hover .list-group-item-heading > small, -.list-group-item.active:focus .list-group-item-heading > small, -.list-group-item.active .list-group-item-heading > .small, -.list-group-item.active:hover .list-group-item-heading > .small, -.list-group-item.active:focus .list-group-item-heading > .small { - color: inherit; -} -.list-group-item.active .list-group-item-text, -.list-group-item.active:hover .list-group-item-text, -.list-group-item.active:focus .list-group-item-text { - color: #f9decc; -} -.list-group-item-success { - color: #ebebeb; - background-color: #5cb85c; -} -a.list-group-item-success, -button.list-group-item-success { - color: #ebebeb; -} -a.list-group-item-success .list-group-item-heading, -button.list-group-item-success .list-group-item-heading { - color: inherit; -} -a.list-group-item-success:hover, -button.list-group-item-success:hover, -a.list-group-item-success:focus, -button.list-group-item-success:focus { - color: #ebebeb; - background-color: #4cae4c; -} -a.list-group-item-success.active, -button.list-group-item-success.active, -a.list-group-item-success.active:hover, -button.list-group-item-success.active:hover, -a.list-group-item-success.active:focus, -button.list-group-item-success.active:focus { - color: #fff; - background-color: #ebebeb; - border-color: #ebebeb; -} -.list-group-item-info { - color: #ebebeb; - background-color: #5bc0de; -} -a.list-group-item-info, -button.list-group-item-info { - color: #ebebeb; -} -a.list-group-item-info .list-group-item-heading, -button.list-group-item-info .list-group-item-heading { - color: inherit; -} -a.list-group-item-info:hover, -button.list-group-item-info:hover, -a.list-group-item-info:focus, -button.list-group-item-info:focus { - color: #ebebeb; - background-color: #46b8da; -} -a.list-group-item-info.active, -button.list-group-item-info.active, -a.list-group-item-info.active:hover, -button.list-group-item-info.active:hover, -a.list-group-item-info.active:focus, -button.list-group-item-info.active:focus { - color: #fff; - background-color: #ebebeb; - border-color: #ebebeb; -} -.list-group-item-warning { - color: #ebebeb; - background-color: #f0ad4e; -} -a.list-group-item-warning, -button.list-group-item-warning { - color: #ebebeb; -} -a.list-group-item-warning .list-group-item-heading, -button.list-group-item-warning .list-group-item-heading { - color: inherit; -} -a.list-group-item-warning:hover, -button.list-group-item-warning:hover, -a.list-group-item-warning:focus, -button.list-group-item-warning:focus { - color: #ebebeb; - background-color: #eea236; -} -a.list-group-item-warning.active, -button.list-group-item-warning.active, -a.list-group-item-warning.active:hover, -button.list-group-item-warning.active:hover, -a.list-group-item-warning.active:focus, -button.list-group-item-warning.active:focus { - color: #fff; - background-color: #ebebeb; - border-color: #ebebeb; -} -.list-group-item-danger { - color: #ebebeb; - background-color: #d9534f; -} -a.list-group-item-danger, -button.list-group-item-danger { - color: #ebebeb; -} -a.list-group-item-danger .list-group-item-heading, -button.list-group-item-danger .list-group-item-heading { - color: inherit; -} -a.list-group-item-danger:hover, -button.list-group-item-danger:hover, -a.list-group-item-danger:focus, -button.list-group-item-danger:focus { - color: #ebebeb; - background-color: #d43f3a; -} -a.list-group-item-danger.active, -button.list-group-item-danger.active, -a.list-group-item-danger.active:hover, -button.list-group-item-danger.active:hover, -a.list-group-item-danger.active:focus, -button.list-group-item-danger.active:focus { - color: #fff; - background-color: #ebebeb; - border-color: #ebebeb; -} -.list-group-item-heading { - margin-top: 0; - margin-bottom: 5px; -} -.list-group-item-text { - margin-bottom: 0; - line-height: 1.3; -} -.panel { - margin-bottom: 21px; - background-color: #4e5d6c; - border: 1px solid transparent; - border-radius: 0; - -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05); - box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05); -} -.panel-body { - padding: 15px; -} -.panel-heading { - padding: 10px 15px; - border-bottom: 1px solid transparent; - border-top-right-radius: -1; - border-top-left-radius: -1; -} -.panel-heading > .dropdown .dropdown-toggle { - color: inherit; -} -.panel-title { - margin-top: 0; - margin-bottom: 0; - font-size: 17px; - color: inherit; -} -.panel-title > a, -.panel-title > small, -.panel-title > .small, -.panel-title > small > a, -.panel-title > .small > a { - color: inherit; -} -.panel-footer { - padding: 10px 15px; - background-color: #485563; - border-top: 1px solid transparent; - border-bottom-right-radius: -1; - border-bottom-left-radius: -1; -} -.panel > .list-group, -.panel > .panel-collapse > .list-group { - margin-bottom: 0; -} -.panel > .list-group .list-group-item, -.panel > .panel-collapse > .list-group .list-group-item { - border-width: 1px 0; - border-radius: 0; -} -.panel > .list-group:first-child .list-group-item:first-child, -.panel > .panel-collapse > .list-group:first-child .list-group-item:first-child { - border-top: 0; - border-top-right-radius: -1; - border-top-left-radius: -1; -} -.panel > .list-group:last-child .list-group-item:last-child, -.panel > .panel-collapse > .list-group:last-child .list-group-item:last-child { - border-bottom: 0; - border-bottom-right-radius: -1; - border-bottom-left-radius: -1; -} -.panel > .panel-heading + .panel-collapse > .list-group .list-group-item:first-child { - border-top-right-radius: 0; - border-top-left-radius: 0; -} -.panel-heading + .list-group .list-group-item:first-child { - border-top-width: 0; -} -.list-group + .panel-footer { - border-top-width: 0; -} -.panel > .table, -.panel > .table-responsive > .table, -.panel > .panel-collapse > .table { - margin-bottom: 0; -} -.panel > .table caption, -.panel > .table-responsive > .table caption, -.panel > .panel-collapse > .table caption { - padding-left: 15px; - padding-right: 15px; -} -.panel > .table:first-child, -.panel > .table-responsive:first-child > .table:first-child { - border-top-right-radius: -1; - border-top-left-radius: -1; -} -.panel > .table:first-child > thead:first-child > tr:first-child, -.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child, -.panel > .table:first-child > tbody:first-child > tr:first-child, -.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child { - border-top-left-radius: -1; - border-top-right-radius: -1; -} -.panel > .table:first-child > thead:first-child > tr:first-child td:first-child, -.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:first-child, -.panel > .table:first-child > tbody:first-child > tr:first-child td:first-child, -.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:first-child, -.panel > .table:first-child > thead:first-child > tr:first-child th:first-child, -.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:first-child, -.panel > .table:first-child > tbody:first-child > tr:first-child th:first-child, -.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:first-child { - border-top-left-radius: -1; -} -.panel > .table:first-child > thead:first-child > tr:first-child td:last-child, -.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:last-child, -.panel > .table:first-child > tbody:first-child > tr:first-child td:last-child, -.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:last-child, -.panel > .table:first-child > thead:first-child > tr:first-child th:last-child, -.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:last-child, -.panel > .table:first-child > tbody:first-child > tr:first-child th:last-child, -.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:last-child { - border-top-right-radius: -1; -} -.panel > .table:last-child, -.panel > .table-responsive:last-child > .table:last-child { - border-bottom-right-radius: -1; - border-bottom-left-radius: -1; -} -.panel > .table:last-child > tbody:last-child > tr:last-child, -.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child, -.panel > .table:last-child > tfoot:last-child > tr:last-child, -.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child { - border-bottom-left-radius: -1; - border-bottom-right-radius: -1; -} -.panel > .table:last-child > tbody:last-child > tr:last-child td:first-child, -.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:first-child, -.panel > .table:last-child > tfoot:last-child > tr:last-child td:first-child, -.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:first-child, -.panel > .table:last-child > tbody:last-child > tr:last-child th:first-child, -.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:first-child, -.panel > .table:last-child > tfoot:last-child > tr:last-child th:first-child, -.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:first-child { - border-bottom-left-radius: -1; -} -.panel > .table:last-child > tbody:last-child > tr:last-child td:last-child, -.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:last-child, -.panel > .table:last-child > tfoot:last-child > tr:last-child td:last-child, -.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:last-child, -.panel > .table:last-child > tbody:last-child > tr:last-child th:last-child, -.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:last-child, -.panel > .table:last-child > tfoot:last-child > tr:last-child th:last-child, -.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:last-child { - border-bottom-right-radius: -1; -} -.panel > .panel-body + .table, -.panel > .panel-body + .table-responsive, -.panel > .table + .panel-body, -.panel > .table-responsive + .panel-body { - border-top: 1px solid #4e5d6c; -} -.panel > .table > tbody:first-child > tr:first-child th, -.panel > .table > tbody:first-child > tr:first-child td { - border-top: 0; -} -.panel > .table-bordered, -.panel > .table-responsive > .table-bordered { - border: 0; -} -.panel > .table-bordered > thead > tr > th:first-child, -.panel > .table-responsive > .table-bordered > thead > tr > th:first-child, -.panel > .table-bordered > tbody > tr > th:first-child, -.panel > .table-responsive > .table-bordered > tbody > tr > th:first-child, -.panel > .table-bordered > tfoot > tr > th:first-child, -.panel > .table-responsive > .table-bordered > tfoot > tr > th:first-child, -.panel > .table-bordered > thead > tr > td:first-child, -.panel > .table-responsive > .table-bordered > thead > tr > td:first-child, -.panel > .table-bordered > tbody > tr > td:first-child, -.panel > .table-responsive > .table-bordered > tbody > tr > td:first-child, -.panel > .table-bordered > tfoot > tr > td:first-child, -.panel > .table-responsive > .table-bordered > tfoot > tr > td:first-child { - border-left: 0; -} -.panel > .table-bordered > thead > tr > th:last-child, -.panel > .table-responsive > .table-bordered > thead > tr > th:last-child, -.panel > .table-bordered > tbody > tr > th:last-child, -.panel > .table-responsive > .table-bordered > tbody > tr > th:last-child, -.panel > .table-bordered > tfoot > tr > th:last-child, -.panel > .table-responsive > .table-bordered > tfoot > tr > th:last-child, -.panel > .table-bordered > thead > tr > td:last-child, -.panel > .table-responsive > .table-bordered > thead > tr > td:last-child, -.panel > .table-bordered > tbody > tr > td:last-child, -.panel > .table-responsive > .table-bordered > tbody > tr > td:last-child, -.panel > .table-bordered > tfoot > tr > td:last-child, -.panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child { - border-right: 0; -} -.panel > .table-bordered > thead > tr:first-child > td, -.panel > .table-responsive > .table-bordered > thead > tr:first-child > td, -.panel > .table-bordered > tbody > tr:first-child > td, -.panel > .table-responsive > .table-bordered > tbody > tr:first-child > td, -.panel > .table-bordered > thead > tr:first-child > th, -.panel > .table-responsive > .table-bordered > thead > tr:first-child > th, -.panel > .table-bordered > tbody > tr:first-child > th, -.panel > .table-responsive > .table-bordered > tbody > tr:first-child > th { - border-bottom: 0; -} -.panel > .table-bordered > tbody > tr:last-child > td, -.panel > .table-responsive > .table-bordered > tbody > tr:last-child > td, -.panel > .table-bordered > tfoot > tr:last-child > td, -.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > td, -.panel > .table-bordered > tbody > tr:last-child > th, -.panel > .table-responsive > .table-bordered > tbody > tr:last-child > th, -.panel > .table-bordered > tfoot > tr:last-child > th, -.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > th { - border-bottom: 0; -} -.panel > .table-responsive { - border: 0; - margin-bottom: 0; -} -.panel-group { - margin-bottom: 21px; -} -.panel-group .panel { - margin-bottom: 0; - border-radius: 0; -} -.panel-group .panel + .panel { - margin-top: 5px; -} -.panel-group .panel-heading { - border-bottom: 0; -} -.panel-group .panel-heading + .panel-collapse > .panel-body, -.panel-group .panel-heading + .panel-collapse > .list-group { - border-top: 1px solid transparent; -} -.panel-group .panel-footer { - border-top: 0; -} -.panel-group .panel-footer + .panel-collapse .panel-body { - border-bottom: 1px solid transparent; -} -.panel-default { - border-color: transparent; -} -.panel-default > .panel-heading { - color: #333333; - background-color: #f5f5f5; - border-color: transparent; -} -.panel-default > .panel-heading + .panel-collapse > .panel-body { - border-top-color: transparent; -} -.panel-default > .panel-heading .badge { - color: #f5f5f5; - background-color: #333333; -} -.panel-default > .panel-footer + .panel-collapse > .panel-body { - border-bottom-color: transparent; -} -.panel-primary { - border-color: transparent; -} -.panel-primary > .panel-heading { - color: #ffffff; - background-color: #df691a; - border-color: transparent; -} -.panel-primary > .panel-heading + .panel-collapse > .panel-body { - border-top-color: transparent; -} -.panel-primary > .panel-heading .badge { - color: #df691a; - background-color: #ffffff; -} -.panel-primary > .panel-footer + .panel-collapse > .panel-body { - border-bottom-color: transparent; -} -.panel-success { - border-color: transparent; -} -.panel-success > .panel-heading { - color: #ebebeb; - background-color: #5cb85c; - border-color: transparent; -} -.panel-success > .panel-heading + .panel-collapse > .panel-body { - border-top-color: transparent; -} -.panel-success > .panel-heading .badge { - color: #5cb85c; - background-color: #ebebeb; -} -.panel-success > .panel-footer + .panel-collapse > .panel-body { - border-bottom-color: transparent; -} -.panel-info { - border-color: transparent; -} -.panel-info > .panel-heading { - color: #ebebeb; - background-color: #5bc0de; - border-color: transparent; -} -.panel-info > .panel-heading + .panel-collapse > .panel-body { - border-top-color: transparent; -} -.panel-info > .panel-heading .badge { - color: #5bc0de; - background-color: #ebebeb; -} -.panel-info > .panel-footer + .panel-collapse > .panel-body { - border-bottom-color: transparent; -} -.panel-warning { - border-color: transparent; -} -.panel-warning > .panel-heading { - color: #ebebeb; - background-color: #f0ad4e; - border-color: transparent; -} -.panel-warning > .panel-heading + .panel-collapse > .panel-body { - border-top-color: transparent; -} -.panel-warning > .panel-heading .badge { - color: #f0ad4e; - background-color: #ebebeb; -} -.panel-warning > .panel-footer + .panel-collapse > .panel-body { - border-bottom-color: transparent; -} -.panel-danger { - border-color: transparent; -} -.panel-danger > .panel-heading { - color: #ebebeb; - background-color: #d9534f; - border-color: transparent; -} -.panel-danger > .panel-heading + .panel-collapse > .panel-body { - border-top-color: transparent; -} -.panel-danger > .panel-heading .badge { - color: #d9534f; - background-color: #ebebeb; -} -.panel-danger > .panel-footer + .panel-collapse > .panel-body { - border-bottom-color: transparent; -} -.embed-responsive { - position: relative; - display: block; - height: 0; - padding: 0; - overflow: hidden; -} -.embed-responsive .embed-responsive-item, -.embed-responsive iframe, -.embed-responsive embed, -.embed-responsive object, -.embed-responsive video { - position: absolute; - top: 0; - left: 0; - bottom: 0; - height: 100%; - width: 100%; - border: 0; -} -.embed-responsive-16by9 { - padding-bottom: 56.25%; -} -.embed-responsive-4by3 { - padding-bottom: 75%; -} -.well { - min-height: 20px; - padding: 19px; - margin-bottom: 20px; - background-color: #4e5d6c; - border: 1px solid transparent; - border-radius: 0; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); -} -.well blockquote { - border-color: #ddd; - border-color: rgba(0, 0, 0, 0.15); -} -.well-lg { - padding: 24px; - border-radius: 0; -} -.well-sm { - padding: 9px; - border-radius: 0; -} -.close { - float: right; - font-size: 22.5px; - font-weight: bold; - line-height: 1; - color: #ebebeb; - text-shadow: none; - opacity: 0.2; - filter: alpha(opacity=20); -} -.close:hover, -.close:focus { - color: #ebebeb; - text-decoration: none; - cursor: pointer; - opacity: 0.5; - filter: alpha(opacity=50); -} -button.close { - padding: 0; - cursor: pointer; - background: transparent; - border: 0; - -webkit-appearance: none; -} -.modal-open { - overflow: hidden; -} -.modal { - display: none; - overflow: hidden; - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: 1050; - -webkit-overflow-scrolling: touch; - outline: 0; -} -.modal.fade .modal-dialog { - -webkit-transform: translate(0, -25%); - -ms-transform: translate(0, -25%); - -o-transform: translate(0, -25%); - transform: translate(0, -25%); - -webkit-transition: -webkit-transform 0.3s ease-out; - -o-transition: -o-transform 0.3s ease-out; - transition: transform 0.3s ease-out; -} -.modal.in .modal-dialog { - -webkit-transform: translate(0, 0); - -ms-transform: translate(0, 0); - -o-transform: translate(0, 0); - transform: translate(0, 0); -} -.modal-open .modal { - overflow-x: hidden; - overflow-y: auto; -} -.modal-dialog { - position: relative; - width: auto; - margin: 10px; -} -.modal-content { - position: relative; - background-color: #4e5d6c; - border: 1px solid transparent; - border-radius: 0; - -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5); - box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5); - -webkit-background-clip: padding-box; - background-clip: padding-box; - outline: 0; -} -.modal-backdrop { - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: 1040; - background-color: #000000; -} -.modal-backdrop.fade { - opacity: 0; - filter: alpha(opacity=0); -} -.modal-backdrop.in { - opacity: 0.5; - filter: alpha(opacity=50); -} -.modal-header { - padding: 15px; - border-bottom: 1px solid #2b3e50; -} -.modal-header .close { - margin-top: -2px; -} -.modal-title { - margin: 0; - line-height: 1.42857143; -} -.modal-body { - position: relative; - padding: 20px; -} -.modal-footer { - padding: 20px; - text-align: right; - border-top: 1px solid #2b3e50; -} -.modal-footer .btn + .btn { - margin-left: 5px; - margin-bottom: 0; -} -.modal-footer .btn-group .btn + .btn { - margin-left: -1px; -} -.modal-footer .btn-block + .btn-block { - margin-left: 0; -} -.modal-scrollbar-measure { - position: absolute; - top: -9999px; - width: 50px; - height: 50px; - overflow: scroll; -} -@media (min-width: 768px) { - .modal-dialog { - width: 600px; - margin: 30px auto; - } - .modal-content { - -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); - box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); - } - .modal-sm { - width: 300px; - } -} -@media (min-width: 992px) { - .modal-lg { - width: 900px; - } -} -.tooltip { - position: absolute; - z-index: 1070; - display: block; - font-family: "Lato", "Helvetica Neue", Helvetica, Arial, sans-serif; - font-style: normal; - font-weight: normal; - letter-spacing: normal; - line-break: auto; - line-height: 1.42857143; - text-align: left; - text-align: start; - text-decoration: none; - text-shadow: none; - text-transform: none; - white-space: normal; - word-break: normal; - word-spacing: normal; - word-wrap: normal; - font-size: 12px; - opacity: 0; - filter: alpha(opacity=0); -} -.tooltip.in { - opacity: 0.9; - filter: alpha(opacity=90); -} -.tooltip.top { - margin-top: -3px; - padding: 5px 0; -} -.tooltip.right { - margin-left: 3px; - padding: 0 5px; -} -.tooltip.bottom { - margin-top: 3px; - padding: 5px 0; -} -.tooltip.left { - margin-left: -3px; - padding: 0 5px; -} -.tooltip-inner { - max-width: 200px; - padding: 3px 8px; - color: #ffffff; - text-align: center; - background-color: #000000; - border-radius: 0; -} -.tooltip-arrow { - position: absolute; - width: 0; - height: 0; - border-color: transparent; - border-style: solid; -} -.tooltip.top .tooltip-arrow { - bottom: 0; - left: 50%; - margin-left: -5px; - border-width: 5px 5px 0; - border-top-color: #000000; -} -.tooltip.top-left .tooltip-arrow { - bottom: 0; - right: 5px; - margin-bottom: -5px; - border-width: 5px 5px 0; - border-top-color: #000000; -} -.tooltip.top-right .tooltip-arrow { - bottom: 0; - left: 5px; - margin-bottom: -5px; - border-width: 5px 5px 0; - border-top-color: #000000; -} -.tooltip.right .tooltip-arrow { - top: 50%; - left: 0; - margin-top: -5px; - border-width: 5px 5px 5px 0; - border-right-color: #000000; -} -.tooltip.left .tooltip-arrow { - top: 50%; - right: 0; - margin-top: -5px; - border-width: 5px 0 5px 5px; - border-left-color: #000000; -} -.tooltip.bottom .tooltip-arrow { - top: 0; - left: 50%; - margin-left: -5px; - border-width: 0 5px 5px; - border-bottom-color: #000000; -} -.tooltip.bottom-left .tooltip-arrow { - top: 0; - right: 5px; - margin-top: -5px; - border-width: 0 5px 5px; - border-bottom-color: #000000; -} -.tooltip.bottom-right .tooltip-arrow { - top: 0; - left: 5px; - margin-top: -5px; - border-width: 0 5px 5px; - border-bottom-color: #000000; -} -.popover { - position: absolute; - top: 0; - left: 0; - z-index: 1060; - display: none; - max-width: 276px; - padding: 1px; - font-family: "Lato", "Helvetica Neue", Helvetica, Arial, sans-serif; - font-style: normal; - font-weight: normal; - letter-spacing: normal; - line-break: auto; - line-height: 1.42857143; - text-align: left; - text-align: start; - text-decoration: none; - text-shadow: none; - text-transform: none; - white-space: normal; - word-break: normal; - word-spacing: normal; - word-wrap: normal; - font-size: 15px; - background-color: #4e5d6c; - -webkit-background-clip: padding-box; - background-clip: padding-box; - border: 1px solid transparent; - border-radius: 0; - -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); - box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); -} -.popover.top { - margin-top: -10px; -} -.popover.right { - margin-left: 10px; -} -.popover.bottom { - margin-top: 10px; -} -.popover.left { - margin-left: -10px; -} -.popover-title { - margin: 0; - padding: 8px 14px; - font-size: 15px; - background-color: #485563; - border-bottom: 1px solid #3d4954; - border-radius: -1 -1 0 0; -} -.popover-content { - padding: 9px 14px; -} -.popover > .arrow, -.popover > .arrow:after { - position: absolute; - display: block; - width: 0; - height: 0; - border-color: transparent; - border-style: solid; -} -.popover > .arrow { - border-width: 11px; -} -.popover > .arrow:after { - border-width: 10px; - content: ""; -} -.popover.top > .arrow { - left: 50%; - margin-left: -11px; - border-bottom-width: 0; - border-top-color: transparent; - bottom: -11px; -} -.popover.top > .arrow:after { - content: " "; - bottom: 1px; - margin-left: -10px; - border-bottom-width: 0; - border-top-color: #4e5d6c; -} -.popover.right > .arrow { - top: 50%; - left: -11px; - margin-top: -11px; - border-left-width: 0; - border-right-color: transparent; -} -.popover.right > .arrow:after { - content: " "; - left: 1px; - bottom: -10px; - border-left-width: 0; - border-right-color: #4e5d6c; -} -.popover.bottom > .arrow { - left: 50%; - margin-left: -11px; - border-top-width: 0; - border-bottom-color: transparent; - top: -11px; -} -.popover.bottom > .arrow:after { - content: " "; - top: 1px; - margin-left: -10px; - border-top-width: 0; - border-bottom-color: #4e5d6c; -} -.popover.left > .arrow { - top: 50%; - right: -11px; - margin-top: -11px; - border-right-width: 0; - border-left-color: transparent; -} -.popover.left > .arrow:after { - content: " "; - right: 1px; - border-right-width: 0; - border-left-color: #4e5d6c; - bottom: -10px; -} -.carousel { - position: relative; -} -.carousel-inner { - position: relative; - overflow: hidden; - width: 100%; -} -.carousel-inner > .item { - display: none; - position: relative; - -webkit-transition: 0.6s ease-in-out left; - -o-transition: 0.6s ease-in-out left; - transition: 0.6s ease-in-out left; -} -.carousel-inner > .item > img, -.carousel-inner > .item > a > img { - line-height: 1; -} -@media all and (transform-3d), (-webkit-transform-3d) { - .carousel-inner > .item { - -webkit-transition: -webkit-transform 0.6s ease-in-out; - -o-transition: -o-transform 0.6s ease-in-out; - transition: transform 0.6s ease-in-out; - -webkit-backface-visibility: hidden; - backface-visibility: hidden; - -webkit-perspective: 1000px; - perspective: 1000px; - } - .carousel-inner > .item.next, - .carousel-inner > .item.active.right { - -webkit-transform: translate3d(100%, 0, 0); - transform: translate3d(100%, 0, 0); - left: 0; - } - .carousel-inner > .item.prev, - .carousel-inner > .item.active.left { - -webkit-transform: translate3d(-100%, 0, 0); - transform: translate3d(-100%, 0, 0); - left: 0; - } - .carousel-inner > .item.next.left, - .carousel-inner > .item.prev.right, - .carousel-inner > .item.active { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - left: 0; - } -} -.carousel-inner > .active, -.carousel-inner > .next, -.carousel-inner > .prev { - display: block; -} -.carousel-inner > .active { - left: 0; -} -.carousel-inner > .next, -.carousel-inner > .prev { - position: absolute; - top: 0; - width: 100%; -} -.carousel-inner > .next { - left: 100%; -} -.carousel-inner > .prev { - left: -100%; -} -.carousel-inner > .next.left, -.carousel-inner > .prev.right { - left: 0; -} -.carousel-inner > .active.left { - left: -100%; -} -.carousel-inner > .active.right { - left: 100%; -} -.carousel-control { - position: absolute; - top: 0; - left: 0; - bottom: 0; - width: 15%; - opacity: 0.5; - filter: alpha(opacity=50); - font-size: 20px; - color: #ffffff; - text-align: center; - text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6); - background-color: rgba(0, 0, 0, 0); -} -.carousel-control.left { - background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%); - background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%); - background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, 0.5)), to(rgba(0, 0, 0, 0.0001))); - background-image: linear-gradient(to right, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1); -} -.carousel-control.right { - left: auto; - right: 0; - background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%); - background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%); - background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, 0.0001)), to(rgba(0, 0, 0, 0.5))); - background-image: linear-gradient(to right, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1); -} -.carousel-control:hover, -.carousel-control:focus { - outline: 0; - color: #ffffff; - text-decoration: none; - opacity: 0.9; - filter: alpha(opacity=90); -} -.carousel-control .icon-prev, -.carousel-control .icon-next, -.carousel-control .glyphicon-chevron-left, -.carousel-control .glyphicon-chevron-right { - position: absolute; - top: 50%; - margin-top: -10px; - z-index: 5; - display: inline-block; -} -.carousel-control .icon-prev, -.carousel-control .glyphicon-chevron-left { - left: 50%; - margin-left: -10px; -} -.carousel-control .icon-next, -.carousel-control .glyphicon-chevron-right { - right: 50%; - margin-right: -10px; -} -.carousel-control .icon-prev, -.carousel-control .icon-next { - width: 20px; - height: 20px; - line-height: 1; - font-family: serif; -} -.carousel-control .icon-prev:before { - content: '\2039'; -} -.carousel-control .icon-next:before { - content: '\203a'; -} -.carousel-indicators { - position: absolute; - bottom: 10px; - left: 50%; - z-index: 15; - width: 60%; - margin-left: -30%; - padding-left: 0; - list-style: none; - text-align: center; -} -.carousel-indicators li { - display: inline-block; - width: 10px; - height: 10px; - margin: 1px; - text-indent: -999px; - border: 1px solid #ffffff; - border-radius: 10px; - cursor: pointer; - background-color: #000 \9; - background-color: rgba(0, 0, 0, 0); -} -.carousel-indicators .active { - margin: 0; - width: 12px; - height: 12px; - background-color: #ffffff; -} -.carousel-caption { - position: absolute; - left: 15%; - right: 15%; - bottom: 20px; - z-index: 10; - padding-top: 20px; - padding-bottom: 20px; - color: #ffffff; - text-align: center; - text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6); -} -.carousel-caption .btn { - text-shadow: none; -} -@media screen and (min-width: 768px) { - .carousel-control .glyphicon-chevron-left, - .carousel-control .glyphicon-chevron-right, - .carousel-control .icon-prev, - .carousel-control .icon-next { - width: 30px; - height: 30px; - margin-top: -10px; - font-size: 30px; - } - .carousel-control .glyphicon-chevron-left, - .carousel-control .icon-prev { - margin-left: -10px; - } - .carousel-control .glyphicon-chevron-right, - .carousel-control .icon-next { - margin-right: -10px; - } - .carousel-caption { - left: 20%; - right: 20%; - padding-bottom: 30px; - } - .carousel-indicators { - bottom: 20px; - } -} -.clearfix:before, -.clearfix:after, -.dl-horizontal dd:before, -.dl-horizontal dd:after, -.container:before, -.container:after, -.container-fluid:before, -.container-fluid:after, -.row:before, -.row:after, -.form-horizontal .form-group:before, -.form-horizontal .form-group:after, -.btn-toolbar:before, -.btn-toolbar:after, -.btn-group-vertical > .btn-group:before, -.btn-group-vertical > .btn-group:after, -.nav:before, -.nav:after, -.navbar:before, -.navbar:after, -.navbar-header:before, -.navbar-header:after, -.navbar-collapse:before, -.navbar-collapse:after, -.pager:before, -.pager:after, -.panel-body:before, -.panel-body:after, -.modal-header:before, -.modal-header:after, -.modal-footer:before, -.modal-footer:after { - content: " "; - display: table; -} -.clearfix:after, -.dl-horizontal dd:after, -.container:after, -.container-fluid:after, -.row:after, -.form-horizontal .form-group:after, -.btn-toolbar:after, -.btn-group-vertical > .btn-group:after, -.nav:after, -.navbar:after, -.navbar-header:after, -.navbar-collapse:after, -.pager:after, -.panel-body:after, -.modal-header:after, -.modal-footer:after { - clear: both; -} -.center-block { - display: block; - margin-left: auto; - margin-right: auto; -} -.pull-right { - float: right !important; -} -.pull-left { - float: left !important; -} -.hide { - display: none !important; -} -.show { - display: block !important; -} -.invisible { - visibility: hidden; -} -.text-hide { - font: 0/0 a; - color: transparent; - text-shadow: none; - background-color: transparent; - border: 0; -} -.hidden { - display: none !important; -} -.affix { - position: fixed; -} -@-ms-viewport { - width: device-width; -} -.visible-xs, -.visible-sm, -.visible-md, -.visible-lg { - display: none !important; -} -.visible-xs-block, -.visible-xs-inline, -.visible-xs-inline-block, -.visible-sm-block, -.visible-sm-inline, -.visible-sm-inline-block, -.visible-md-block, -.visible-md-inline, -.visible-md-inline-block, -.visible-lg-block, -.visible-lg-inline, -.visible-lg-inline-block { - display: none !important; -} -@media (max-width: 767px) { - .visible-xs { - display: block !important; - } - table.visible-xs { - display: table !important; - } - tr.visible-xs { - display: table-row !important; - } - th.visible-xs, - td.visible-xs { - display: table-cell !important; - } -} -@media (max-width: 767px) { - .visible-xs-block { - display: block !important; - } -} -@media (max-width: 767px) { - .visible-xs-inline { - display: inline !important; - } -} -@media (max-width: 767px) { - .visible-xs-inline-block { - display: inline-block !important; - } -} -@media (min-width: 768px) and (max-width: 991px) { - .visible-sm { - display: block !important; - } - table.visible-sm { - display: table !important; - } - tr.visible-sm { - display: table-row !important; - } - th.visible-sm, - td.visible-sm { - display: table-cell !important; - } -} -@media (min-width: 768px) and (max-width: 991px) { - .visible-sm-block { - display: block !important; - } -} -@media (min-width: 768px) and (max-width: 991px) { - .visible-sm-inline { - display: inline !important; - } -} -@media (min-width: 768px) and (max-width: 991px) { - .visible-sm-inline-block { - display: inline-block !important; - } -} -@media (min-width: 992px) and (max-width: 1199px) { - .visible-md { - display: block !important; - } - table.visible-md { - display: table !important; - } - tr.visible-md { - display: table-row !important; - } - th.visible-md, - td.visible-md { - display: table-cell !important; - } -} -@media (min-width: 992px) and (max-width: 1199px) { - .visible-md-block { - display: block !important; - } -} -@media (min-width: 992px) and (max-width: 1199px) { - .visible-md-inline { - display: inline !important; - } -} -@media (min-width: 992px) and (max-width: 1199px) { - .visible-md-inline-block { - display: inline-block !important; - } -} -@media (min-width: 1200px) { - .visible-lg { - display: block !important; - } - table.visible-lg { - display: table !important; - } - tr.visible-lg { - display: table-row !important; - } - th.visible-lg, - td.visible-lg { - display: table-cell !important; - } -} -@media (min-width: 1200px) { - .visible-lg-block { - display: block !important; - } -} -@media (min-width: 1200px) { - .visible-lg-inline { - display: inline !important; - } -} -@media (min-width: 1200px) { - .visible-lg-inline-block { - display: inline-block !important; - } -} -@media (max-width: 767px) { - .hidden-xs { - display: none !important; - } -} -@media (min-width: 768px) and (max-width: 991px) { - .hidden-sm { - display: none !important; - } -} -@media (min-width: 992px) and (max-width: 1199px) { - .hidden-md { - display: none !important; - } -} -@media (min-width: 1200px) { - .hidden-lg { - display: none !important; - } -} -.visible-print { - display: none !important; -} -@media print { - .visible-print { - display: block !important; - } - table.visible-print { - display: table !important; - } - tr.visible-print { - display: table-row !important; - } - th.visible-print, - td.visible-print { - display: table-cell !important; - } -} -.visible-print-block { - display: none !important; -} -@media print { - .visible-print-block { - display: block !important; - } -} -.visible-print-inline { - display: none !important; -} -@media print { - .visible-print-inline { - display: inline !important; - } -} -.visible-print-inline-block { - display: none !important; -} -@media print { - .visible-print-inline-block { - display: inline-block !important; - } -} -@media print { - .hidden-print { - display: none !important; - } -} -.navbar { - -webkit-box-shadow: none; - box-shadow: none; - border: none; - font-size: 12px; -} -.navbar-default .badge { - background-color: #fff; - color: #4e5d6c; -} -.navbar-inverse .badge { - background-color: #fff; - color: #df691a; -} -.btn-default:hover { - background-color: #485563; -} -.btn-sm, -.btn-xs { - font-size: 12px; -} -.text-primary, -.text-primary:hover { - color: #df691a; -} -.text-success, -.text-success:hover { - color: #5cb85c; -} -.text-danger, -.text-danger:hover { - color: #d9534f; -} -.text-warning, -.text-warning:hover { - color: #f0ad4e; -} -.text-info, -.text-info:hover { - color: #5bc0de; -} -.page-header { - border-bottom-color: #4e5d6c; -} -.dropdown-menu { - border: none; - margin: 0; - -webkit-box-shadow: none; - box-shadow: none; -} -.dropdown-menu > li > a { - font-size: 12px; -} -.btn-group.open .dropdown-toggle { - -webkit-box-shadow: none; - box-shadow: none; -} -.dropdown-header { - font-size: 12px; -} -table, -.table { - font-size: 12px; -} -table a:not(.btn), -.table a:not(.btn) { - color: #fff; - text-decoration: underline; -} -table .dropdown-menu a, -.table .dropdown-menu a { - text-decoration: none; -} -table .text-muted, -.table .text-muted { - color: #4e5d6c; -} -table > thead > tr > th, -.table > thead > tr > th, -table > tbody > tr > th, -.table > tbody > tr > th, -table > tfoot > tr > th, -.table > tfoot > tr > th, -table > thead > tr > td, -.table > thead > tr > td, -table > tbody > tr > td, -.table > tbody > tr > td, -table > tfoot > tr > td, -.table > tfoot > tr > td { - border-color: transparent; -} -input, -textarea { - color: #2b3e50; -} -label, -.radio label, -.checkbox label, -.help-block { - font-size: 12px; -} -.input-addon, -.input-group-addon { - color: #df691a; -} -.has-warning .help-block, -.has-warning .control-label, -.has-warning .radio, -.has-warning .checkbox, -.has-warning .radio-inline, -.has-warning .checkbox-inline, -.has-warning.radio label, -.has-warning.checkbox label, -.has-warning.radio-inline label, -.has-warning.checkbox-inline label, -.has-warning .form-control-feedback { - color: #f0ad4e; -} -.has-warning .input-group-addon { - border: none; -} -.has-error .help-block, -.has-error .control-label, -.has-error .radio, -.has-error .checkbox, -.has-error .radio-inline, -.has-error .checkbox-inline, -.has-error.radio label, -.has-error.checkbox label, -.has-error.radio-inline label, -.has-error.checkbox-inline label, -.has-error .form-control-feedback { - color: #d9534f; -} -.has-error .input-group-addon { - border: none; -} -.has-success .help-block, -.has-success .control-label, -.has-success .radio, -.has-success .checkbox, -.has-success .radio-inline, -.has-success .checkbox-inline, -.has-success.radio label, -.has-success.checkbox label, -.has-success.radio-inline label, -.has-success.checkbox-inline label, -.has-success .form-control-feedback { - color: #5cb85c; -} -.has-success .input-group-addon { - border: none; -} -.form-control:focus { - -webkit-box-shadow: none; - box-shadow: none; -} -.has-warning .form-control:focus, -.has-error .form-control:focus, -.has-success .form-control:focus { - -webkit-box-shadow: none; - box-shadow: none; -} -.nav .open > a, -.nav .open > a:hover, -.nav .open > a:focus { - border-color: transparent; -} -.nav-tabs > li > a { - color: #ebebeb; -} -.nav-pills > li > a { - color: #ebebeb; -} -.pager a { - color: #ebebeb; -} -.alert { - color: #fff; -} -.alert a, -.alert .alert-link { - color: #fff; -} -.close { - opacity: 0.4; -} -.close:hover, -.close:focus { - opacity: 1; -} -.well { - -webkit-box-shadow: none; - box-shadow: none; -} -a.list-group-item.active, -a.list-group-item.active:hover, -a.list-group-item.active:focus { - border: none; -} -a.list-group-item-success.active { - background-color: #5cb85c; -} -a.list-group-item-success.active:hover, -a.list-group-item-success.active:focus { - background-color: #4cae4c; -} -a.list-group-item-warning.active { - background-color: #f0ad4e; -} -a.list-group-item-warning.active:hover, -a.list-group-item-warning.active:focus { - background-color: #eea236; -} -a.list-group-item-danger.active { - background-color: #d9534f; -} -a.list-group-item-danger.active:hover, -a.list-group-item-danger.active:focus { - background-color: #d43f3a; -} -.panel { - border: none; -} -.panel-default > .panel-heading { - background-color: #485563; - color: #ebebeb; -} -.thumbnail { - background-color: #4e5d6c; - border: none; -} -.modal { - padding: 0; -} -.modal-header, -.modal-footer { - background-color: #485563; - border: none; - border-radius: 0; -} -.popover-title { - border: none; -} +@import url("https://fonts.googleapis.com/css?family=Lato:300,400,700"); +/*! + * bootswatch v3.3.6 + * Homepage: http://bootswatch.com + * Copyright 2012-2016 Thomas Park + * Licensed under MIT + * Based on Bootstrap +*/ +/*! + * Bootstrap v3.3.6 (http://getbootstrap.com) + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ +/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */ +html { + font-family: sans-serif; + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; +} +body { + margin: 60; +} +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +main, +menu, +nav, +section, +summary { + display: block; +} +audio, +canvas, +progress, +video { + display: inline-block; + vertical-align: baseline; +} +audio:not([controls]) { + display: none; + height: 0; +} +[hidden], +template { + display: none; +} +a { + background-color: transparent; +} +a:active, +a:hover { + outline: 0; +} +abbr[title] { + border-bottom: 1px dotted; +} +b, +strong { + font-weight: bold; +} +dfn { + font-style: italic; +} +h1 { + font-size: 2em; + margin: 0.67em 0; +} +mark { + background: #ff0; + color: #000; +} +small { + font-size: 80%; +} +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} +sup { + top: -0.5em; +} +sub { + bottom: -0.25em; +} +img { + border: 0; +} +svg:not(:root) { + overflow: hidden; +} +figure { + margin: 1em 40px; +} +hr { + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; + height: 0; +} +pre { + overflow: auto; +} +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em; +} +button, +input, +optgroup, +select, +textarea { + color: inherit; + font: inherit; + margin: 0; +} +button { + overflow: visible; +} +button, +select { + text-transform: none; +} +button, +html input[type="button"], +input[type="reset"], +input[type="submit"] { + -webkit-appearance: button; + cursor: pointer; +} +button[disabled], +html input[disabled] { + cursor: default; +} +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; +} +input { + line-height: normal; +} +input[type="checkbox"], +input[type="radio"] { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + padding: 0; +} +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + height: auto; +} +input[type="search"] { + -webkit-appearance: textfield; + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; +} +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; +} +legend { + border: 0; + padding: 0; +} +textarea { + overflow: auto; +} +optgroup { + font-weight: bold; +} +table { + border-collapse: collapse; + border-spacing: 0; +} +td, +th { + padding: 0; +} +/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */ +@media print { + *, + *:before, + *:after { + background: transparent !important; + color: #000 !important; + -webkit-box-shadow: none !important; + box-shadow: none !important; + text-shadow: none !important; + } + a, + a:visited { + text-decoration: underline; + } + a[href]:after { + content: " (" attr(href) ")"; + } + abbr[title]:after { + content: " (" attr(title) ")"; + } + a[href^="#"]:after, + a[href^="javascript:"]:after { + content: ""; + } + pre, + blockquote { + border: 1px solid #999; + page-break-inside: avoid; + } + thead { + display: table-header-group; + } + tr, + img { + page-break-inside: avoid; + } + img { + max-width: 100% !important; + } + p, + h2, + h3 { + orphans: 3; + widows: 3; + } + h2, + h3 { + page-break-after: avoid; + } + .navbar { + display: none; + } + .btn > .caret, + .dropup > .btn > .caret { + border-top-color: #000 !important; + } + .label { + border: 1px solid #000; + } + .table { + border-collapse: collapse !important; + } + .table td, + .table th { + background-color: #fff !important; + } + .table-bordered th, + .table-bordered td { + border: 1px solid #ddd !important; + } +} +@font-face { + font-family: 'Glyphicons Halflings'; + src: url('../Content/fonts/glyphicons-halflings-regular.eot'); + src: url('../Content/fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../Content/fonts/glyphicons-halflings-regular.woff2') format('woff2'), url('../Content/fonts/glyphicons-halflings-regular.woff') format('woff'), url('../Content/fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('../Content/fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg'); +} +.glyphicon { + position: relative; + top: 1px; + display: inline-block; + font-family: 'Glyphicons Halflings'; + font-style: normal; + font-weight: normal; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +.glyphicon-asterisk:before { + content: "\002a"; +} +.glyphicon-plus:before { + content: "\002b"; +} +.glyphicon-euro:before, +.glyphicon-eur:before { + content: "\20ac"; +} +.glyphicon-minus:before { + content: "\2212"; +} +.glyphicon-cloud:before { + content: "\2601"; +} +.glyphicon-envelope:before { + content: "\2709"; +} +.glyphicon-pencil:before { + content: "\270f"; +} +.glyphicon-glass:before { + content: "\e001"; +} +.glyphicon-music:before { + content: "\e002"; +} +.glyphicon-search:before { + content: "\e003"; +} +.glyphicon-heart:before { + content: "\e005"; +} +.glyphicon-star:before { + content: "\e006"; +} +.glyphicon-star-empty:before { + content: "\e007"; +} +.glyphicon-user:before { + content: "\e008"; +} +.glyphicon-film:before { + content: "\e009"; +} +.glyphicon-th-large:before { + content: "\e010"; +} +.glyphicon-th:before { + content: "\e011"; +} +.glyphicon-th-list:before { + content: "\e012"; +} +.glyphicon-ok:before { + content: "\e013"; +} +.glyphicon-remove:before { + content: "\e014"; +} +.glyphicon-zoom-in:before { + content: "\e015"; +} +.glyphicon-zoom-out:before { + content: "\e016"; +} +.glyphicon-off:before { + content: "\e017"; +} +.glyphicon-signal:before { + content: "\e018"; +} +.glyphicon-cog:before { + content: "\e019"; +} +.glyphicon-trash:before { + content: "\e020"; +} +.glyphicon-home:before { + content: "\e021"; +} +.glyphicon-file:before { + content: "\e022"; +} +.glyphicon-time:before { + content: "\e023"; +} +.glyphicon-road:before { + content: "\e024"; +} +.glyphicon-download-alt:before { + content: "\e025"; +} +.glyphicon-download:before { + content: "\e026"; +} +.glyphicon-upload:before { + content: "\e027"; +} +.glyphicon-inbox:before { + content: "\e028"; +} +.glyphicon-play-circle:before { + content: "\e029"; +} +.glyphicon-repeat:before { + content: "\e030"; +} +.glyphicon-refresh:before { + content: "\e031"; +} +.glyphicon-list-alt:before { + content: "\e032"; +} +.glyphicon-lock:before { + content: "\e033"; +} +.glyphicon-flag:before { + content: "\e034"; +} +.glyphicon-headphones:before { + content: "\e035"; +} +.glyphicon-volume-off:before { + content: "\e036"; +} +.glyphicon-volume-down:before { + content: "\e037"; +} +.glyphicon-volume-up:before { + content: "\e038"; +} +.glyphicon-qrcode:before { + content: "\e039"; +} +.glyphicon-barcode:before { + content: "\e040"; +} +.glyphicon-tag:before { + content: "\e041"; +} +.glyphicon-tags:before { + content: "\e042"; +} +.glyphicon-book:before { + content: "\e043"; +} +.glyphicon-bookmark:before { + content: "\e044"; +} +.glyphicon-print:before { + content: "\e045"; +} +.glyphicon-camera:before { + content: "\e046"; +} +.glyphicon-font:before { + content: "\e047"; +} +.glyphicon-bold:before { + content: "\e048"; +} +.glyphicon-italic:before { + content: "\e049"; +} +.glyphicon-text-height:before { + content: "\e050"; +} +.glyphicon-text-width:before { + content: "\e051"; +} +.glyphicon-align-left:before { + content: "\e052"; +} +.glyphicon-align-center:before { + content: "\e053"; +} +.glyphicon-align-right:before { + content: "\e054"; +} +.glyphicon-align-justify:before { + content: "\e055"; +} +.glyphicon-list:before { + content: "\e056"; +} +.glyphicon-indent-left:before { + content: "\e057"; +} +.glyphicon-indent-right:before { + content: "\e058"; +} +.glyphicon-facetime-video:before { + content: "\e059"; +} +.glyphicon-picture:before { + content: "\e060"; +} +.glyphicon-map-marker:before { + content: "\e062"; +} +.glyphicon-adjust:before { + content: "\e063"; +} +.glyphicon-tint:before { + content: "\e064"; +} +.glyphicon-edit:before { + content: "\e065"; +} +.glyphicon-share:before { + content: "\e066"; +} +.glyphicon-check:before { + content: "\e067"; +} +.glyphicon-move:before { + content: "\e068"; +} +.glyphicon-step-backward:before { + content: "\e069"; +} +.glyphicon-fast-backward:before { + content: "\e070"; +} +.glyphicon-backward:before { + content: "\e071"; +} +.glyphicon-play:before { + content: "\e072"; +} +.glyphicon-pause:before { + content: "\e073"; +} +.glyphicon-stop:before { + content: "\e074"; +} +.glyphicon-forward:before { + content: "\e075"; +} +.glyphicon-fast-forward:before { + content: "\e076"; +} +.glyphicon-step-forward:before { + content: "\e077"; +} +.glyphicon-eject:before { + content: "\e078"; +} +.glyphicon-chevron-left:before { + content: "\e079"; +} +.glyphicon-chevron-right:before { + content: "\e080"; +} +.glyphicon-plus-sign:before { + content: "\e081"; +} +.glyphicon-minus-sign:before { + content: "\e082"; +} +.glyphicon-remove-sign:before { + content: "\e083"; +} +.glyphicon-ok-sign:before { + content: "\e084"; +} +.glyphicon-question-sign:before { + content: "\e085"; +} +.glyphicon-info-sign:before { + content: "\e086"; +} +.glyphicon-screenshot:before { + content: "\e087"; +} +.glyphicon-remove-circle:before { + content: "\e088"; +} +.glyphicon-ok-circle:before { + content: "\e089"; +} +.glyphicon-ban-circle:before { + content: "\e090"; +} +.glyphicon-arrow-left:before { + content: "\e091"; +} +.glyphicon-arrow-right:before { + content: "\e092"; +} +.glyphicon-arrow-up:before { + content: "\e093"; +} +.glyphicon-arrow-down:before { + content: "\e094"; +} +.glyphicon-share-alt:before { + content: "\e095"; +} +.glyphicon-resize-full:before { + content: "\e096"; +} +.glyphicon-resize-small:before { + content: "\e097"; +} +.glyphicon-exclamation-sign:before { + content: "\e101"; +} +.glyphicon-gift:before { + content: "\e102"; +} +.glyphicon-leaf:before { + content: "\e103"; +} +.glyphicon-fire:before { + content: "\e104"; +} +.glyphicon-eye-open:before { + content: "\e105"; +} +.glyphicon-eye-close:before { + content: "\e106"; +} +.glyphicon-warning-sign:before { + content: "\e107"; +} +.glyphicon-plane:before { + content: "\e108"; +} +.glyphicon-calendar:before { + content: "\e109"; +} +.glyphicon-random:before { + content: "\e110"; +} +.glyphicon-comment:before { + content: "\e111"; +} +.glyphicon-magnet:before { + content: "\e112"; +} +.glyphicon-chevron-up:before { + content: "\e113"; +} +.glyphicon-chevron-down:before { + content: "\e114"; +} +.glyphicon-retweet:before { + content: "\e115"; +} +.glyphicon-shopping-cart:before { + content: "\e116"; +} +.glyphicon-folder-close:before { + content: "\e117"; +} +.glyphicon-folder-open:before { + content: "\e118"; +} +.glyphicon-resize-vertical:before { + content: "\e119"; +} +.glyphicon-resize-horizontal:before { + content: "\e120"; +} +.glyphicon-hdd:before { + content: "\e121"; +} +.glyphicon-bullhorn:before { + content: "\e122"; +} +.glyphicon-bell:before { + content: "\e123"; +} +.glyphicon-certificate:before { + content: "\e124"; +} +.glyphicon-thumbs-up:before { + content: "\e125"; +} +.glyphicon-thumbs-down:before { + content: "\e126"; +} +.glyphicon-hand-right:before { + content: "\e127"; +} +.glyphicon-hand-left:before { + content: "\e128"; +} +.glyphicon-hand-up:before { + content: "\e129"; +} +.glyphicon-hand-down:before { + content: "\e130"; +} +.glyphicon-circle-arrow-right:before { + content: "\e131"; +} +.glyphicon-circle-arrow-left:before { + content: "\e132"; +} +.glyphicon-circle-arrow-up:before { + content: "\e133"; +} +.glyphicon-circle-arrow-down:before { + content: "\e134"; +} +.glyphicon-globe:before { + content: "\e135"; +} +.glyphicon-wrench:before { + content: "\e136"; +} +.glyphicon-tasks:before { + content: "\e137"; +} +.glyphicon-filter:before { + content: "\e138"; +} +.glyphicon-briefcase:before { + content: "\e139"; +} +.glyphicon-fullscreen:before { + content: "\e140"; +} +.glyphicon-dashboard:before { + content: "\e141"; +} +.glyphicon-paperclip:before { + content: "\e142"; +} +.glyphicon-heart-empty:before { + content: "\e143"; +} +.glyphicon-link:before { + content: "\e144"; +} +.glyphicon-phone:before { + content: "\e145"; +} +.glyphicon-pushpin:before { + content: "\e146"; +} +.glyphicon-usd:before { + content: "\e148"; +} +.glyphicon-gbp:before { + content: "\e149"; +} +.glyphicon-sort:before { + content: "\e150"; +} +.glyphicon-sort-by-alphabet:before { + content: "\e151"; +} +.glyphicon-sort-by-alphabet-alt:before { + content: "\e152"; +} +.glyphicon-sort-by-order:before { + content: "\e153"; +} +.glyphicon-sort-by-order-alt:before { + content: "\e154"; +} +.glyphicon-sort-by-attributes:before { + content: "\e155"; +} +.glyphicon-sort-by-attributes-alt:before { + content: "\e156"; +} +.glyphicon-unchecked:before { + content: "\e157"; +} +.glyphicon-expand:before { + content: "\e158"; +} +.glyphicon-collapse-down:before { + content: "\e159"; +} +.glyphicon-collapse-up:before { + content: "\e160"; +} +.glyphicon-log-in:before { + content: "\e161"; +} +.glyphicon-flash:before { + content: "\e162"; +} +.glyphicon-log-out:before { + content: "\e163"; +} +.glyphicon-new-window:before { + content: "\e164"; +} +.glyphicon-record:before { + content: "\e165"; +} +.glyphicon-save:before { + content: "\e166"; +} +.glyphicon-open:before { + content: "\e167"; +} +.glyphicon-saved:before { + content: "\e168"; +} +.glyphicon-import:before { + content: "\e169"; +} +.glyphicon-export:before { + content: "\e170"; +} +.glyphicon-send:before { + content: "\e171"; +} +.glyphicon-floppy-disk:before { + content: "\e172"; +} +.glyphicon-floppy-saved:before { + content: "\e173"; +} +.glyphicon-floppy-remove:before { + content: "\e174"; +} +.glyphicon-floppy-save:before { + content: "\e175"; +} +.glyphicon-floppy-open:before { + content: "\e176"; +} +.glyphicon-credit-card:before { + content: "\e177"; +} +.glyphicon-transfer:before { + content: "\e178"; +} +.glyphicon-cutlery:before { + content: "\e179"; +} +.glyphicon-header:before { + content: "\e180"; +} +.glyphicon-compressed:before { + content: "\e181"; +} +.glyphicon-earphone:before { + content: "\e182"; +} +.glyphicon-phone-alt:before { + content: "\e183"; +} +.glyphicon-tower:before { + content: "\e184"; +} +.glyphicon-stats:before { + content: "\e185"; +} +.glyphicon-sd-video:before { + content: "\e186"; +} +.glyphicon-hd-video:before { + content: "\e187"; +} +.glyphicon-subtitles:before { + content: "\e188"; +} +.glyphicon-sound-stereo:before { + content: "\e189"; +} +.glyphicon-sound-dolby:before { + content: "\e190"; +} +.glyphicon-sound-5-1:before { + content: "\e191"; +} +.glyphicon-sound-6-1:before { + content: "\e192"; +} +.glyphicon-sound-7-1:before { + content: "\e193"; +} +.glyphicon-copyright-mark:before { + content: "\e194"; +} +.glyphicon-registration-mark:before { + content: "\e195"; +} +.glyphicon-cloud-download:before { + content: "\e197"; +} +.glyphicon-cloud-upload:before { + content: "\e198"; +} +.glyphicon-tree-conifer:before { + content: "\e199"; +} +.glyphicon-tree-deciduous:before { + content: "\e200"; +} +.glyphicon-cd:before { + content: "\e201"; +} +.glyphicon-save-file:before { + content: "\e202"; +} +.glyphicon-open-file:before { + content: "\e203"; +} +.glyphicon-level-up:before { + content: "\e204"; +} +.glyphicon-copy:before { + content: "\e205"; +} +.glyphicon-paste:before { + content: "\e206"; +} +.glyphicon-alert:before { + content: "\e209"; +} +.glyphicon-equalizer:before { + content: "\e210"; +} +.glyphicon-king:before { + content: "\e211"; +} +.glyphicon-queen:before { + content: "\e212"; +} +.glyphicon-pawn:before { + content: "\e213"; +} +.glyphicon-bishop:before { + content: "\e214"; +} +.glyphicon-knight:before { + content: "\e215"; +} +.glyphicon-baby-formula:before { + content: "\e216"; +} +.glyphicon-tent:before { + content: "\26fa"; +} +.glyphicon-blackboard:before { + content: "\e218"; +} +.glyphicon-bed:before { + content: "\e219"; +} +.glyphicon-apple:before { + content: "\f8ff"; +} +.glyphicon-erase:before { + content: "\e221"; +} +.glyphicon-hourglass:before { + content: "\231b"; +} +.glyphicon-lamp:before { + content: "\e223"; +} +.glyphicon-duplicate:before { + content: "\e224"; +} +.glyphicon-piggy-bank:before { + content: "\e225"; +} +.glyphicon-scissors:before { + content: "\e226"; +} +.glyphicon-bitcoin:before { + content: "\e227"; +} +.glyphicon-btc:before { + content: "\e227"; +} +.glyphicon-xbt:before { + content: "\e227"; +} +.glyphicon-yen:before { + content: "\00a5"; +} +.glyphicon-jpy:before { + content: "\00a5"; +} +.glyphicon-ruble:before { + content: "\20bd"; +} +.glyphicon-rub:before { + content: "\20bd"; +} +.glyphicon-scale:before { + content: "\e230"; +} +.glyphicon-ice-lolly:before { + content: "\e231"; +} +.glyphicon-ice-lolly-tasted:before { + content: "\e232"; +} +.glyphicon-education:before { + content: "\e233"; +} +.glyphicon-option-horizontal:before { + content: "\e234"; +} +.glyphicon-option-vertical:before { + content: "\e235"; +} +.glyphicon-menu-hamburger:before { + content: "\e236"; +} +.glyphicon-modal-window:before { + content: "\e237"; +} +.glyphicon-oil:before { + content: "\e238"; +} +.glyphicon-grain:before { + content: "\e239"; +} +.glyphicon-sunglasses:before { + content: "\e240"; +} +.glyphicon-text-size:before { + content: "\e241"; +} +.glyphicon-text-color:before { + content: "\e242"; +} +.glyphicon-text-background:before { + content: "\e243"; +} +.glyphicon-object-align-top:before { + content: "\e244"; +} +.glyphicon-object-align-bottom:before { + content: "\e245"; +} +.glyphicon-object-align-horizontal:before { + content: "\e246"; +} +.glyphicon-object-align-left:before { + content: "\e247"; +} +.glyphicon-object-align-vertical:before { + content: "\e248"; +} +.glyphicon-object-align-right:before { + content: "\e249"; +} +.glyphicon-triangle-right:before { + content: "\e250"; +} +.glyphicon-triangle-left:before { + content: "\e251"; +} +.glyphicon-triangle-bottom:before { + content: "\e252"; +} +.glyphicon-triangle-top:before { + content: "\e253"; +} +.glyphicon-console:before { + content: "\e254"; +} +.glyphicon-superscript:before { + content: "\e255"; +} +.glyphicon-subscript:before { + content: "\e256"; +} +.glyphicon-menu-left:before { + content: "\e257"; +} +.glyphicon-menu-right:before { + content: "\e258"; +} +.glyphicon-menu-down:before { + content: "\e259"; +} +.glyphicon-menu-up:before { + content: "\e260"; +} +* { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +*:before, +*:after { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +html { + font-size: 16px; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} +body { + font-family:Open Sans Regular,Helvetica Neue,Helvetica,Arial,sans-serif; + font-size: 15px; + line-height: 1.42857143; + color: #eee; + background-color: #1f1f1f; +} +input, +button, +select, +textarea { + font-family: inherit; + font-size: inherit; + line-height: inherit; +} +a { + color: #df691a; + text-decoration: none; +} +a:hover, +a:focus { + color: #df691a; + text-decoration: underline; +} +a:focus { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +figure { + margin: 0; +} +img { + vertical-align: middle; +} +.img-responsive, +.thumbnail > img, +.thumbnail a > img, +.carousel-inner > .item > img, +.carousel-inner > .item > a > img { + display: block; + max-width: 100%; + height: auto; +} +.img-rounded { + border-radius: 0; +} +.img-thumbnail { + padding: 4px; + line-height: 1.42857143; + background-color: #2b3e50; + border: 1px solid #dddddd; + border-radius: 0; + -webkit-transition: all 0.2s ease-in-out; + -o-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out; + display: inline-block; + max-width: 100%; + height: auto; +} +.img-circle { + border-radius: 50%; +} +hr { + margin-top: 21px; + margin-bottom: 21px; + border: 0; + border-top: 1px dashed #5cb85c; +} +.sr-only { + position: absolute; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} +.sr-only-focusable:active, +.sr-only-focusable:focus { + position: static; + width: auto; + height: auto; + margin: 0; + overflow: visible; + clip: auto; +} +[role="button"] { + cursor: pointer; +} +h1, +h2, +h3, +h4, +h5, +h6, +.h1, +.h2, +.h3, +.h4, +.h5, +.h6 { + font-family: inherit; + font-weight: 400; + line-height: 1.1; + color: inherit; +} +h1 small, +h2 small, +h3 small, +h4 small, +h5 small, +h6 small, +.h1 small, +.h2 small, +.h3 small, +.h4 small, +.h5 small, +.h6 small, +h1 .small, +h2 .small, +h3 .small, +h4 .small, +h5 .small, +h6 .small, +.h1 .small, +.h2 .small, +.h3 .small, +.h4 .small, +.h5 .small, +.h6 .small { + font-weight: normal; + line-height: 1; + color: #ebebeb; +} +h1, +.h1, +h2, +.h2, +h3, +.h3 { + margin-top: 21px; + margin-bottom: 10.5px; +} +h1 small, +.h1 small, +h2 small, +.h2 small, +h3 small, +.h3 small, +h1 .small, +.h1 .small, +h2 .small, +.h2 .small, +h3 .small, +.h3 .small { + font-size: 65%; +} +h4, +.h4, +h5, +.h5, +h6, +.h6 { + margin-top: 10.5px; + margin-bottom: 10.5px; +} +h4 small, +.h4 small, +h5 small, +.h5 small, +h6 small, +.h6 small, +h4 .small, +.h4 .small, +h5 .small, +.h5 .small, +h6 .small, +.h6 .small { + font-size: 75%; +} +h1, +.h1 { + font-size: 39px; +} +h2, +.h2 { + font-size: 32px; +} +h3, +.h3 { + font-size: 26px; +} +h4, +.h4 { + font-size: 19px; +} +h5, +.h5 { + font-size: 15px; +} +h6, +.h6 { + font-size: 13px; +} +p { + margin: 0 0 10.5px; +} +.lead { + margin-bottom: 21px; + font-size: 17px; + font-weight: 300; + line-height: 1.4; +} +@media (min-width: 768px) { + .lead { + font-size: 22.5px; + } +} +small, +.small { + font-size: 80%; +} +mark, +.mark { + background-color: #f0ad4e; + padding: .2em; +} +.text-left { + text-align: left; +} +.text-right { + text-align: right; +} +.text-center { + text-align: center; +} +.text-justify { + text-align: justify; +} +.text-nowrap { + white-space: nowrap; +} +.text-lowercase { + text-transform: lowercase; +} +.text-uppercase { + text-transform: uppercase; +} +.text-capitalize { + text-transform: capitalize; +} +.text-muted { + color: #4e5d6c; +} +.text-primary { + color: #df691a; +} +a.text-primary:hover, +a.text-primary:focus { + color: #b15315; +} +.text-success { + color: #ebebeb; +} +a.text-success:hover, +a.text-success:focus { + color: #d2d2d2; +} +.text-info { + color: #ebebeb; +} +a.text-info:hover, +a.text-info:focus { + color: #d2d2d2; +} +.text-warning { + color: #ebebeb; +} +a.text-warning:hover, +a.text-warning:focus { + color: #d2d2d2; +} +.text-danger { + color: #ebebeb; +} +a.text-danger:hover, +a.text-danger:focus { + color: #d2d2d2; +} +.bg-primary { + color: #fff; + background-color: #df691a; +} +a.bg-primary:hover, +a.bg-primary:focus { + background-color: #b15315; +} +.bg-success { + background-color: #5cb85c; +} +a.bg-success:hover, +a.bg-success:focus { + background-color: #449d44; +} +.bg-info { + background-color: #5bc0de; +} +a.bg-info:hover, +a.bg-info:focus { + background-color: #31b0d5; +} +.bg-warning { + background-color: #f0ad4e; +} +a.bg-warning:hover, +a.bg-warning:focus { + background-color: #ec971f; +} +.bg-danger { + background-color: #d9534f; +} +a.bg-danger:hover, +a.bg-danger:focus { + background-color: #c9302c; +} +.page-header { + padding-bottom: 9.5px; + margin: 42px 0 21px; + border-bottom: 1px solid #ebebeb; +} +ul, +ol { + margin-top: 0; + margin-bottom: 10.5px; +} +ul ul, +ol ul, +ul ol, +ol ol { + margin-bottom: 0; +} +.list-unstyled { + padding-left: 0; + list-style: none; +} +.list-inline { + padding-left: 0; + list-style: none; + margin-left: -5px; +} +.list-inline > li { + display: inline-block; + padding-left: 5px; + padding-right: 5px; +} +dl { + margin-top: 0; + margin-bottom: 21px; +} +dt, +dd { + line-height: 1.42857143; +} +dt { + font-weight: bold; +} +dd { + margin-left: 0; +} +@media (min-width: 768px) { + .dl-horizontal dt { + float: left; + width: 160px; + clear: left; + text-align: right; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .dl-horizontal dd { + margin-left: 180px; + } +} +abbr[title], +abbr[data-original-title] { + cursor: help; + border-bottom: 1px dotted #4e5d6c; +} +.initialism { + font-size: 90%; + text-transform: uppercase; +} +blockquote { + padding: 10.5px 21px; + margin: 0 0 21px; + font-size: 18.75px; + border-left: 5px solid #4e5d6c; +} +blockquote p:last-child, +blockquote ul:last-child, +blockquote ol:last-child { + margin-bottom: 0; +} +blockquote footer, +blockquote small, +blockquote .small { + display: block; + font-size: 80%; + line-height: 1.42857143; + color: #ebebeb; +} +blockquote footer:before, +blockquote small:before, +blockquote .small:before { + content: '\2014 \00A0'; +} +.blockquote-reverse, +blockquote.pull-right { + padding-right: 15px; + padding-left: 0; + border-right: 5px solid #4e5d6c; + border-left: 0; + text-align: right; +} +.blockquote-reverse footer:before, +blockquote.pull-right footer:before, +.blockquote-reverse small:before, +blockquote.pull-right small:before, +.blockquote-reverse .small:before, +blockquote.pull-right .small:before { + content: ''; +} +.blockquote-reverse footer:after, +blockquote.pull-right footer:after, +.blockquote-reverse small:after, +blockquote.pull-right small:after, +.blockquote-reverse .small:after, +blockquote.pull-right .small:after { + content: '\00A0 \2014'; +} +address { + margin-bottom: 21px; + font-style: normal; + line-height: 1.42857143; +} +code, +kbd, +pre, +samp { + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; +} +code { + padding: 2px 4px; + font-size: 90%; + color: #c7254e; + background-color: #f9f2f4; + border-radius: 0; +} +kbd { + padding: 2px 4px; + font-size: 90%; + color: #ffffff; + background-color: #333333; + border-radius: 0; + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); +} +kbd kbd { + padding: 0; + font-size: 100%; + font-weight: bold; + -webkit-box-shadow: none; + box-shadow: none; +} +pre { + display: block; + padding: 10px; + margin: 0 0 10.5px; + font-size: 14px; + line-height: 1.42857143; + word-break: break-all; + word-wrap: break-word; + color: #333333; + background-color: #f5f5f5; + border: 1px solid #cccccc; + border-radius: 0; +} +pre code { + padding: 0; + font-size: inherit; + color: inherit; + white-space: pre-wrap; + background-color: transparent; + border-radius: 0; +} +.pre-scrollable { + max-height: 340px; + overflow-y: scroll; +} +.container { + margin-right: auto; + margin-left: auto; + padding-left: .9375rem; + padding-right: .9375rem; +} +@media (min-width: 768px) { + .container { + width: 750px; + } +} +@media (min-width: 992px) { + .container { + width: 970px; + } +} +@media (min-width: 1200px) { + .container { + width: 1170px; + } +} +.container-fluid { + margin-right: auto; + margin-left: auto; + padding-left: 15px; + padding-right: 15px; +} +.row { + margin-left: -15px; + margin-right: -15px; +} +.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 { + position: relative; + min-height: 1px; + padding-left: 15px; + padding-right: 15px; +} +.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12 { + float: left; +} +.col-xs-12 { + width: 100%; +} +.col-xs-11 { + width: 91.66666667%; +} +.col-xs-10 { + width: 83.33333333%; +} +.col-xs-9 { + width: 75%; +} +.col-xs-8 { + width: 66.66666667%; +} +.col-xs-7 { + width: 58.33333333%; +} +.col-xs-6 { + width: 50%; +} +.col-xs-5 { + width: 41.66666667%; +} +.col-xs-4 { + width: 33.33333333%; +} +.col-xs-3 { + width: 25%; +} +.col-xs-2 { + width: 16.66666667%; +} +.col-xs-1 { + width: 8.33333333%; +} +.col-xs-pull-12 { + right: 100%; +} +.col-xs-pull-11 { + right: 91.66666667%; +} +.col-xs-pull-10 { + right: 83.33333333%; +} +.col-xs-pull-9 { + right: 75%; +} +.col-xs-pull-8 { + right: 66.66666667%; +} +.col-xs-pull-7 { + right: 58.33333333%; +} +.col-xs-pull-6 { + right: 50%; +} +.col-xs-pull-5 { + right: 41.66666667%; +} +.col-xs-pull-4 { + right: 33.33333333%; +} +.col-xs-pull-3 { + right: 25%; +} +.col-xs-pull-2 { + right: 16.66666667%; +} +.col-xs-pull-1 { + right: 8.33333333%; +} +.col-xs-pull-0 { + right: auto; +} +.col-xs-push-12 { + left: 100%; +} +.col-xs-push-11 { + left: 91.66666667%; +} +.col-xs-push-10 { + left: 83.33333333%; +} +.col-xs-push-9 { + left: 75%; +} +.col-xs-push-8 { + left: 66.66666667%; +} +.col-xs-push-7 { + left: 58.33333333%; +} +.col-xs-push-6 { + left: 50%; +} +.col-xs-push-5 { + left: 41.66666667%; +} +.col-xs-push-4 { + left: 33.33333333%; +} +.col-xs-push-3 { + left: 25%; +} +.col-xs-push-2 { + left: 16.66666667%; +} +.col-xs-push-1 { + left: 8.33333333%; +} +.col-xs-push-0 { + left: auto; +} +.col-xs-offset-12 { + margin-left: 100%; +} +.col-xs-offset-11 { + margin-left: 91.66666667%; +} +.col-xs-offset-10 { + margin-left: 83.33333333%; +} +.col-xs-offset-9 { + margin-left: 75%; +} +.col-xs-offset-8 { + margin-left: 66.66666667%; +} +.col-xs-offset-7 { + margin-left: 58.33333333%; +} +.col-xs-offset-6 { + margin-left: 50%; +} +.col-xs-offset-5 { + margin-left: 41.66666667%; +} +.col-xs-offset-4 { + margin-left: 33.33333333%; +} +.col-xs-offset-3 { + margin-left: 25%; +} +.col-xs-offset-2 { + margin-left: 16.66666667%; +} +.col-xs-offset-1 { + margin-left: 8.33333333%; +} +.col-xs-offset-0 { + margin-left: 0%; +} +@media (min-width: 768px) { + .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12 { + float: left; + } + .col-sm-12 { + width: 100%; + } + .col-sm-11 { + width: 91.66666667%; + } + .col-sm-10 { + width: 83.33333333%; + } + .col-sm-9 { + width: 75%; + } + .col-sm-8 { + width: 66.66666667%; + } + .col-sm-7 { + width: 58.33333333%; + } + .col-sm-6 { + width: 50%; + } + .col-sm-5 { + width: 41.66666667%; + } + .col-sm-4 { + width: 33.33333333%; + } + .col-sm-3 { + width: 25%; + } + .col-sm-2 { + width: 16.66666667%; + } + .col-sm-1 { + width: 8.33333333%; + } + .col-sm-pull-12 { + right: 100%; + } + .col-sm-pull-11 { + right: 91.66666667%; + } + .col-sm-pull-10 { + right: 83.33333333%; + } + .col-sm-pull-9 { + right: 75%; + } + .col-sm-pull-8 { + right: 66.66666667%; + } + .col-sm-pull-7 { + right: 58.33333333%; + } + .col-sm-pull-6 { + right: 50%; + } + .col-sm-pull-5 { + right: 41.66666667%; + } + .col-sm-pull-4 { + right: 33.33333333%; + } + .col-sm-pull-3 { + right: 25%; + } + .col-sm-pull-2 { + right: 16.66666667%; + } + .col-sm-pull-1 { + right: 8.33333333%; + } + .col-sm-pull-0 { + right: auto; + } + .col-sm-push-12 { + left: 100%; + } + .col-sm-push-11 { + left: 91.66666667%; + } + .col-sm-push-10 { + left: 83.33333333%; + } + .col-sm-push-9 { + left: 75%; + } + .col-sm-push-8 { + left: 66.66666667%; + } + .col-sm-push-7 { + left: 58.33333333%; + } + .col-sm-push-6 { + left: 50%; + } + .col-sm-push-5 { + left: 41.66666667%; + } + .col-sm-push-4 { + left: 33.33333333%; + } + .col-sm-push-3 { + left: 25%; + } + .col-sm-push-2 { + left: 16.66666667%; + } + .col-sm-push-1 { + left: 8.33333333%; + } + .col-sm-push-0 { + left: auto; + } + .col-sm-offset-12 { + margin-left: 100%; + } + .col-sm-offset-11 { + margin-left: 91.66666667%; + } + .col-sm-offset-10 { + margin-left: 83.33333333%; + } + .col-sm-offset-9 { + margin-left: 75%; + } + .col-sm-offset-8 { + margin-left: 66.66666667%; + } + .col-sm-offset-7 { + margin-left: 58.33333333%; + } + .col-sm-offset-6 { + margin-left: 50%; + } + .col-sm-offset-5 { + margin-left: 41.66666667%; + } + .col-sm-offset-4 { + margin-left: 33.33333333%; + } + .col-sm-offset-3 { + margin-left: 25%; + } + .col-sm-offset-2 { + margin-left: 16.66666667%; + } + .col-sm-offset-1 { + margin-left: 8.33333333%; + } + .col-sm-offset-0 { + margin-left: 0%; + } +} +@media (min-width: 992px) { + .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12 { + float: left; + } + .col-md-12 { + width: 100%; + } + .col-md-11 { + width: 91.66666667%; + } + .col-md-10 { + width: 83.33333333%; + } + .col-md-9 { + width: 75%; + } + .col-md-8 { + width: 66.66666667%; + } + .col-md-7 { + width: 58.33333333%; + } + .col-md-6 { + width: 50%; + } + .col-md-5 { + width: 41.66666667%; + } + .col-md-4 { + width: 33.33333333%; + } + .col-md-3 { + width: 25%; + } + .col-md-2 { + width: 16.66666667%; + } + .col-md-1 { + width: 8.33333333%; + } + .col-md-pull-12 { + right: 100%; + } + .col-md-pull-11 { + right: 91.66666667%; + } + .col-md-pull-10 { + right: 83.33333333%; + } + .col-md-pull-9 { + right: 75%; + } + .col-md-pull-8 { + right: 66.66666667%; + } + .col-md-pull-7 { + right: 58.33333333%; + } + .col-md-pull-6 { + right: 50%; + } + .col-md-pull-5 { + right: 41.66666667%; + } + .col-md-pull-4 { + right: 33.33333333%; + } + .col-md-pull-3 { + right: 25%; + } + .col-md-pull-2 { + right: 16.66666667%; + } + .col-md-pull-1 { + right: 8.33333333%; + } + .col-md-pull-0 { + right: auto; + } + .col-md-push-12 { + left: 100%; + } + .col-md-push-11 { + left: 91.66666667%; + } + .col-md-push-10 { + left: 83.33333333%; + } + .col-md-push-9 { + left: 75%; + } + .col-md-push-8 { + left: 66.66666667%; + } + .col-md-push-7 { + left: 58.33333333%; + } + .col-md-push-6 { + left: 50%; + } + .col-md-push-5 { + left: 41.66666667%; + } + .col-md-push-4 { + left: 33.33333333%; + } + .col-md-push-3 { + left: 25%; + } + .col-md-push-2 { + left: 16.66666667%; + } + .col-md-push-1 { + left: 8.33333333%; + } + .col-md-push-0 { + left: auto; + } + .col-md-offset-12 { + margin-left: 100%; + } + .col-md-offset-11 { + margin-left: 91.66666667%; + } + .col-md-offset-10 { + margin-left: 83.33333333%; + } + .col-md-offset-9 { + margin-left: 75%; + } + .col-md-offset-8 { + margin-left: 66.66666667%; + } + .col-md-offset-7 { + margin-left: 58.33333333%; + } + .col-md-offset-6 { + margin-left: 50%; + } + .col-md-offset-5 { + margin-left: 41.66666667%; + } + .col-md-offset-4 { + margin-left: 33.33333333%; + } + .col-md-offset-3 { + margin-left: 25%; + } + .col-md-offset-2 { + margin-left: 16.66666667%; + } + .col-md-offset-1 { + margin-left: 8.33333333%; + } + .col-md-offset-0 { + margin-left: 0%; + } +} +@media (min-width: 1200px) { + .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12 { + float: left; + } + .col-lg-12 { + width: 100%; + } + .col-lg-11 { + width: 91.66666667%; + } + .col-lg-10 { + width: 83.33333333%; + } + .col-lg-9 { + width: 75%; + } + .col-lg-8 { + width: 66.66666667%; + } + .col-lg-7 { + width: 58.33333333%; + } + .col-lg-6 { + width: 50%; + } + .col-lg-5 { + width: 41.66666667%; + } + .col-lg-4 { + width: 33.33333333%; + } + .col-lg-3 { + width: 25%; + } + .col-lg-2 { + width: 16.66666667%; + } + .col-lg-1 { + width: 8.33333333%; + } + .col-lg-pull-12 { + right: 100%; + } + .col-lg-pull-11 { + right: 91.66666667%; + } + .col-lg-pull-10 { + right: 83.33333333%; + } + .col-lg-pull-9 { + right: 75%; + } + .col-lg-pull-8 { + right: 66.66666667%; + } + .col-lg-pull-7 { + right: 58.33333333%; + } + .col-lg-pull-6 { + right: 50%; + } + .col-lg-pull-5 { + right: 41.66666667%; + } + .col-lg-pull-4 { + right: 33.33333333%; + } + .col-lg-pull-3 { + right: 25%; + } + .col-lg-pull-2 { + right: 16.66666667%; + } + .col-lg-pull-1 { + right: 8.33333333%; + } + .col-lg-pull-0 { + right: auto; + } + .col-lg-push-12 { + left: 100%; + } + .col-lg-push-11 { + left: 91.66666667%; + } + .col-lg-push-10 { + left: 83.33333333%; + } + .col-lg-push-9 { + left: 75%; + } + .col-lg-push-8 { + left: 66.66666667%; + } + .col-lg-push-7 { + left: 58.33333333%; + } + .col-lg-push-6 { + left: 50%; + } + .col-lg-push-5 { + left: 41.66666667%; + } + .col-lg-push-4 { + left: 33.33333333%; + } + .col-lg-push-3 { + left: 25%; + } + .col-lg-push-2 { + left: 16.66666667%; + } + .col-lg-push-1 { + left: 8.33333333%; + } + .col-lg-push-0 { + left: auto; + } + .col-lg-offset-12 { + margin-left: 100%; + } + .col-lg-offset-11 { + margin-left: 91.66666667%; + } + .col-lg-offset-10 { + margin-left: 83.33333333%; + } + .col-lg-offset-9 { + margin-left: 75%; + } + .col-lg-offset-8 { + margin-left: 66.66666667%; + } + .col-lg-offset-7 { + margin-left: 58.33333333%; + } + .col-lg-offset-6 { + margin-left: 50%; + } + .col-lg-offset-5 { + margin-left: 41.66666667%; + } + .col-lg-offset-4 { + margin-left: 33.33333333%; + } + .col-lg-offset-3 { + margin-left: 25%; + } + .col-lg-offset-2 { + margin-left: 16.66666667%; + } + .col-lg-offset-1 { + margin-left: 8.33333333%; + } + .col-lg-offset-0 { + margin-left: 0%; + } +} +table { + background-color: transparent; +} +caption { + padding-top: 6px; + padding-bottom: 6px; + color: #4e5d6c; + text-align: left; +} +th { + text-align: left; +} +.table { + width: 100%; + max-width: 100%; + margin-bottom: 21px; +} +.table > thead > tr > th, +.table > tbody > tr > th, +.table > tfoot > tr > th, +.table > thead > tr > td, +.table > tbody > tr > td, +.table > tfoot > tr > td { + padding: 6px; + line-height: 1.42857143; + vertical-align: top; + border-top: 1px solid #4e5d6c; +} +.table > thead > tr > th { + vertical-align: bottom; + border-bottom: 2px solid #4e5d6c; +} +.table > caption + thead > tr:first-child > th, +.table > colgroup + thead > tr:first-child > th, +.table > thead:first-child > tr:first-child > th, +.table > caption + thead > tr:first-child > td, +.table > colgroup + thead > tr:first-child > td, +.table > thead:first-child > tr:first-child > td { + border-top: 0; +} +.table > tbody + tbody { + border-top: 2px solid #4e5d6c; +} +.table .table { + background-color: #2b3e50; +} +.table-condensed > thead > tr > th, +.table-condensed > tbody > tr > th, +.table-condensed > tfoot > tr > th, +.table-condensed > thead > tr > td, +.table-condensed > tbody > tr > td, +.table-condensed > tfoot > tr > td { + padding: 3px; +} +.table-bordered { + border: 1px solid #4e5d6c; +} +.table-bordered > thead > tr > th, +.table-bordered > tbody > tr > th, +.table-bordered > tfoot > tr > th, +.table-bordered > thead > tr > td, +.table-bordered > tbody > tr > td, +.table-bordered > tfoot > tr > td { + border: 1px solid #4e5d6c; +} +.table-bordered > thead > tr > th, +.table-bordered > thead > tr > td { + border-bottom-width: 2px; +} +.table-striped > tbody > tr:nth-of-type(odd) { + background-color: #333333; +} +.table-hover > tbody > tr:hover { + background-color: #282828; +} +table col[class*="col-"] { + position: static; + float: none; + display: table-column; +} +table td[class*="col-"], +table th[class*="col-"] { + position: static; + float: none; + display: table-cell; +} +.table > thead > tr > td.active, +.table > tbody > tr > td.active, +.table > tfoot > tr > td.active, +.table > thead > tr > th.active, +.table > tbody > tr > th.active, +.table > tfoot > tr > th.active, +.table > thead > tr.active > td, +.table > tbody > tr.active > td, +.table > tfoot > tr.active > td, +.table > thead > tr.active > th, +.table > tbody > tr.active > th, +.table > tfoot > tr.active > th { + background-color: #485563; +} +.table-hover > tbody > tr > td.active:hover, +.table-hover > tbody > tr > th.active:hover, +.table-hover > tbody > tr.active:hover > td, +.table-hover > tbody > tr:hover > .active, +.table-hover > tbody > tr.active:hover > th { + background-color: #3d4954; +} +.table > thead > tr > td.success, +.table > tbody > tr > td.success, +.table > tfoot > tr > td.success, +.table > thead > tr > th.success, +.table > tbody > tr > th.success, +.table > tfoot > tr > th.success, +.table > thead > tr.success > td, +.table > tbody > tr.success > td, +.table > tfoot > tr.success > td, +.table > thead > tr.success > th, +.table > tbody > tr.success > th, +.table > tfoot > tr.success > th { + background-color: #5cb85c; +} +.table-hover > tbody > tr > td.success:hover, +.table-hover > tbody > tr > th.success:hover, +.table-hover > tbody > tr.success:hover > td, +.table-hover > tbody > tr:hover > .success, +.table-hover > tbody > tr.success:hover > th { + background-color: #4cae4c; +} +.table > thead > tr > td.info, +.table > tbody > tr > td.info, +.table > tfoot > tr > td.info, +.table > thead > tr > th.info, +.table > tbody > tr > th.info, +.table > tfoot > tr > th.info, +.table > thead > tr.info > td, +.table > tbody > tr.info > td, +.table > tfoot > tr.info > td, +.table > thead > tr.info > th, +.table > tbody > tr.info > th, +.table > tfoot > tr.info > th { + background-color: #5bc0de; +} +.table-hover > tbody > tr > td.info:hover, +.table-hover > tbody > tr > th.info:hover, +.table-hover > tbody > tr.info:hover > td, +.table-hover > tbody > tr:hover > .info, +.table-hover > tbody > tr.info:hover > th { + background-color: #46b8da; +} +.table > thead > tr > td.warning, +.table > tbody > tr > td.warning, +.table > tfoot > tr > td.warning, +.table > thead > tr > th.warning, +.table > tbody > tr > th.warning, +.table > tfoot > tr > th.warning, +.table > thead > tr.warning > td, +.table > tbody > tr.warning > td, +.table > tfoot > tr.warning > td, +.table > thead > tr.warning > th, +.table > tbody > tr.warning > th, +.table > tfoot > tr.warning > th { + background-color: #f0ad4e; +} +.table-hover > tbody > tr > td.warning:hover, +.table-hover > tbody > tr > th.warning:hover, +.table-hover > tbody > tr.warning:hover > td, +.table-hover > tbody > tr:hover > .warning, +.table-hover > tbody > tr.warning:hover > th { + background-color: #eea236; +} +.table > thead > tr > td.danger, +.table > tbody > tr > td.danger, +.table > tfoot > tr > td.danger, +.table > thead > tr > th.danger, +.table > tbody > tr > th.danger, +.table > tfoot > tr > th.danger, +.table > thead > tr.danger > td, +.table > tbody > tr.danger > td, +.table > tfoot > tr.danger > td, +.table > thead > tr.danger > th, +.table > tbody > tr.danger > th, +.table > tfoot > tr.danger > th { + background-color: #d9534f; +} +.table-hover > tbody > tr > td.danger:hover, +.table-hover > tbody > tr > th.danger:hover, +.table-hover > tbody > tr.danger:hover > td, +.table-hover > tbody > tr:hover > .danger, +.table-hover > tbody > tr.danger:hover > th { + background-color: #d43f3a; +} +.table-responsive { + overflow-x: auto; + min-height: 0.01%; +} +@media screen and (max-width: 767px) { + .table-responsive { + width: 100%; + margin-bottom: 15.75px; + overflow-y: hidden; + -ms-overflow-style: -ms-autohiding-scrollbar; + border: 1px solid #4e5d6c; + } + .table-responsive > .table { + margin-bottom: 0; + } + .table-responsive > .table > thead > tr > th, + .table-responsive > .table > tbody > tr > th, + .table-responsive > .table > tfoot > tr > th, + .table-responsive > .table > thead > tr > td, + .table-responsive > .table > tbody > tr > td, + .table-responsive > .table > tfoot > tr > td { + white-space: nowrap; + } + .table-responsive > .table-bordered { + border: 0; + } + .table-responsive > .table-bordered > thead > tr > th:first-child, + .table-responsive > .table-bordered > tbody > tr > th:first-child, + .table-responsive > .table-bordered > tfoot > tr > th:first-child, + .table-responsive > .table-bordered > thead > tr > td:first-child, + .table-responsive > .table-bordered > tbody > tr > td:first-child, + .table-responsive > .table-bordered > tfoot > tr > td:first-child { + border-left: 0; + } + .table-responsive > .table-bordered > thead > tr > th:last-child, + .table-responsive > .table-bordered > tbody > tr > th:last-child, + .table-responsive > .table-bordered > tfoot > tr > th:last-child, + .table-responsive > .table-bordered > thead > tr > td:last-child, + .table-responsive > .table-bordered > tbody > tr > td:last-child, + .table-responsive > .table-bordered > tfoot > tr > td:last-child { + border-right: 0; + } + .table-responsive > .table-bordered > tbody > tr:last-child > th, + .table-responsive > .table-bordered > tfoot > tr:last-child > th, + .table-responsive > .table-bordered > tbody > tr:last-child > td, + .table-responsive > .table-bordered > tfoot > tr:last-child > td { + border-bottom: 0; + } +} +fieldset { + padding: 15px; + margin: 0; + border: 0; + min-width: 0; +} +legend { + display: block; + width: 100%; + padding: 0; + margin-bottom: 21px; + font-size: 22.5px; + line-height: inherit; + color: #ebebeb; + border: 0; + border-bottom: 1px solid #333333; +} +label { + display: inline-block; + max-width: 100%; + margin-bottom: 5px; + font-weight: bold; +} +input[type="search"] { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +input[type="radio"], +input[type="checkbox"] { + margin: 4px 0 0; + margin-top: 1px \9; + line-height: normal; +} +input[type="file"] { + display: block; +} +input[type="range"] { + display: block; + width: 100%; +} +select[multiple], +select[size] { + height: auto; +} +input[type="file"]:focus, +input[type="radio"]:focus, +input[type="checkbox"]:focus { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +output { + display: block; + padding-top: 9px; + font-size: 15px; + line-height: 1.42857143; + color: #2b3e50; +} +.form-control { + display: block; + width: 100%; + height: 39px; + padding: 8px 16px; + font-size: 15px; + line-height: 1.42857143; + color: #fefefe; + background-color: #333333; + background-image: none; + border: 1px solid transparent; + border-radius: .25rem; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s; + -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; + transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; +} +.form-control:focus { + border-color: transparent; + outline: 0; + -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(0, 0, 0, 0.6); + box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(0, 0, 0, 0.6); +} +.form-control::-moz-placeholder { + color: #cccccc; + opacity: 1; +} +.form-control:-ms-input-placeholder { + color: #cccccc; +} +.form-control::-webkit-input-placeholder { + color: #cccccc; +} +.form-control::-ms-expand { + border: 0; + background-color: transparent; +} +.form-control[disabled], +.form-control[readonly], +fieldset[disabled] .form-control { + background-color: #ebebeb; + opacity: 1; +} +.form-control[disabled], +fieldset[disabled] .form-control { + cursor: not-allowed; +} +textarea.form-control { + height: auto; +} +input[type="search"] { + -webkit-appearance: none; +} +@media screen and (-webkit-min-device-pixel-ratio: 0) { + input[type="date"].form-control, + input[type="time"].form-control, + input[type="datetime-local"].form-control, + input[type="month"].form-control { + line-height: 39px; + } + input[type="date"].input-sm, + input[type="time"].input-sm, + input[type="datetime-local"].input-sm, + input[type="month"].input-sm, + .input-group-sm input[type="date"], + .input-group-sm input[type="time"], + .input-group-sm input[type="datetime-local"], + .input-group-sm input[type="month"] { + line-height: 30px; + } + input[type="date"].input-lg, + input[type="time"].input-lg, + input[type="datetime-local"].input-lg, + input[type="month"].input-lg, + .input-group-lg input[type="date"], + .input-group-lg input[type="time"], + .input-group-lg input[type="datetime-local"], + .input-group-lg input[type="month"] { + line-height: 52px; + } +} +.form-group { + margin-bottom: 15px; +} +.radio, +.checkbox { + position: relative; + display: block; + margin-top: 10px; + margin-bottom: 10px; +} +.radio label, +.checkbox label { + min-height: 21px; + padding-left: 20px; + margin-bottom: 0; + font-weight: normal; + cursor: pointer; +} +.radio input[type="radio"], +.radio-inline input[type="radio"], +.checkbox input[type="checkbox"], +.checkbox-inline input[type="checkbox"] { + position: absolute; + margin-left: -0px; + margin-top: 4px \9; +} +.radio + .radio, +.checkbox + .checkbox { + margin-top: -5px; +} +.radio-inline, +.checkbox-inline { + position: relative; + display: inline-block; + padding-left: 20px; + margin-bottom: 0; + vertical-align: middle; + font-weight: normal; + cursor: pointer; +} +.radio-inline + .radio-inline, +.checkbox-inline + .checkbox-inline { + margin-top: 0; + margin-left: 10px; +} +input[type="radio"][disabled], +input[type="checkbox"][disabled], +input[type="radio"].disabled, +input[type="checkbox"].disabled, +fieldset[disabled] input[type="radio"], +fieldset[disabled] input[type="checkbox"] { + cursor: not-allowed; +} +.radio-inline.disabled, +.checkbox-inline.disabled, +fieldset[disabled] .radio-inline, +fieldset[disabled] .checkbox-inline { + cursor: not-allowed; +} +.radio.disabled label, +.checkbox.disabled label, +fieldset[disabled] .radio label, +fieldset[disabled] .checkbox label { + cursor: not-allowed; +} +.form-control-static { + padding-top: 9px; + padding-bottom: 9px; + margin-bottom: 0; + min-height: 36px; +} +.form-control-static.input-lg, +.form-control-static.input-sm { + padding-left: 0; + padding-right: 0; +} +.input-sm { + height: 30px; + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 0; +} +select.input-sm { + height: 30px; + line-height: 30px; +} +textarea.input-sm, +select[multiple].input-sm { + height: auto; +} +.form-group-sm .form-control { + height: 30px; + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 0; +} +.form-group-sm select.form-control { + height: 30px; + line-height: 30px; +} +.form-group-sm textarea.form-control, +.form-group-sm select[multiple].form-control { + height: auto; +} +.form-group-sm .form-control-static { + height: 30px; + min-height: 33px; + padding: 6px 10px; + font-size: 12px; + line-height: 1.5; +} +.input-lg { + height: 52px; + padding: 12px 24px; + font-size: 19px; + line-height: 1.3333333; + border-radius: 0; +} +select.input-lg { + height: 52px; + line-height: 52px; +} +textarea.input-lg, +select[multiple].input-lg { + height: auto; +} +.form-group-lg .form-control { + height: 52px; + padding: 12px 24px; + font-size: 19px; + line-height: 1.3333333; + border-radius: 0; +} +.form-group-lg select.form-control { + height: 52px; + line-height: 52px; +} +.form-group-lg textarea.form-control, +.form-group-lg select[multiple].form-control { + height: auto; +} +.form-group-lg .form-control-static { + height: 52px; + min-height: 40px; + padding: 13px 24px; + font-size: 19px; + line-height: 1.3333333; +} +.has-feedback { + position: relative; +} +.has-feedback .form-control { + padding-right: 48.75px; +} +.form-control-feedback { + position: absolute; + top: 0; + right: 0; + z-index: 2; + display: block; + width: 39px; + height: 39px; + line-height: 39px; + text-align: center; + pointer-events: none; +} +.input-lg + .form-control-feedback, +.input-group-lg + .form-control-feedback, +.form-group-lg .form-control + .form-control-feedback { + width: 52px; + height: 52px; + line-height: 52px; +} +.input-sm + .form-control-feedback, +.input-group-sm + .form-control-feedback, +.form-group-sm .form-control + .form-control-feedback { + width: 30px; + height: 30px; + line-height: 30px; +} +.has-success .help-block, +.has-success .control-label, +.has-success .radio, +.has-success .checkbox, +.has-success .radio-inline, +.has-success .checkbox-inline, +.has-success.radio label, +.has-success.checkbox label, +.has-success.radio-inline label, +.has-success.checkbox-inline label { + color: #ebebeb; +} +.has-success .form-control { + border-color: #ebebeb; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} +.has-success .form-control:focus { + border-color: #d2d2d2; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ffffff; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ffffff; +} +.has-success .input-group-addon { + color: #ebebeb; + border-color: #ebebeb; + background-color: #5cb85c; +} +.has-success .form-control-feedback { + color: #ebebeb; +} +.has-warning .help-block, +.has-warning .control-label, +.has-warning .radio, +.has-warning .checkbox, +.has-warning .radio-inline, +.has-warning .checkbox-inline, +.has-warning.radio label, +.has-warning.checkbox label, +.has-warning.radio-inline label, +.has-warning.checkbox-inline label { + color: #ebebeb; +} +.has-warning .form-control { + border-color: #ebebeb; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} +.has-warning .form-control:focus { + border-color: #d2d2d2; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ffffff; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ffffff; +} +.has-warning .input-group-addon { + color: #ebebeb; + border-color: #ebebeb; + background-color: #f0ad4e; +} +.has-warning .form-control-feedback { + color: #ebebeb; +} +.has-error .help-block, +.has-error .control-label, +.has-error .radio, +.has-error .checkbox, +.has-error .radio-inline, +.has-error .checkbox-inline, +.has-error.radio label, +.has-error.checkbox label, +.has-error.radio-inline label, +.has-error.checkbox-inline label { + color: #ebebeb; +} +.has-error .form-control { + border-color: #ebebeb; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} +.has-error .form-control:focus { + border-color: #d2d2d2; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ffffff; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ffffff; +} +.has-error .input-group-addon { + color: #ebebeb; + border-color: #ebebeb; + background-color: #d9534f; +} +.has-error .form-control-feedback { + color: #ebebeb; +} +.has-feedback label ~ .form-control-feedback { + top: 26px; +} +.has-feedback label.sr-only ~ .form-control-feedback { + top: 0; +} +.help-block { + display: block; + margin-top: 5px; + margin-bottom: 10px; + color: #ffffff; +} +@media (min-width: 768px) { + .form-inline .form-group { + display: inline-block; + margin-bottom: 0; + vertical-align: middle; + } + .form-inline .form-control { + display: inline-block; + width: auto; + vertical-align: middle; + } + .form-inline .form-control-static { + display: inline-block; + } + .form-inline .input-group { + display: inline-table; + vertical-align: middle; + } + .form-inline .input-group .input-group-addon, + .form-inline .input-group .input-group-btn, + .form-inline .input-group .form-control { + width: auto; + } + .form-inline .input-group > .form-control { + width: 100%; + } + .form-inline .control-label { + margin-bottom: 0; + vertical-align: middle; + } + .form-inline .radio, + .form-inline .checkbox { + display: inline-block; + margin-top: 0; + margin-bottom: 0; + vertical-align: middle; + } + .form-inline .radio label, + .form-inline .checkbox label { + padding-left: 0; + } + .form-inline .radio input[type="radio"], + .form-inline .checkbox input[type="checkbox"] { + position: relative; + margin-left: 0; + } + .form-inline .has-feedback .form-control-feedback { + top: 0; + } +} +.form-horizontal .radio, +.form-horizontal .checkbox, +.form-horizontal .radio-inline, +.form-horizontal .checkbox-inline { + margin-top: -15px; + margin-bottom: 0; + padding-top: 9px; +} +.form-horizontal .radio, +.form-horizontal .checkbox { + min-height: 30px; +} +.form-horizontal .form-group { + margin-left: -15px; + margin-right: -15px; +} +@media (min-width: 768px) { + .form-horizontal .control-label { + text-align: right; + margin-bottom: 0; + padding-top: 9px; + } +} +.form-horizontal .has-feedback .form-control-feedback { + right: 15px; +} +@media (min-width: 768px) { + .form-horizontal .form-group-lg .control-label { + padding-top: 13px; + font-size: 19px; + } +} +@media (min-width: 768px) { + .form-horizontal .form-group-sm .control-label { + padding-top: 6px; + font-size: 12px; + } +} +.btn { + display: inline-block; + margin-bottom: 0; + font-weight: normal; + text-align: center; + vertical-align: middle; + -ms-touch-action: manipulation; + touch-action: manipulation; + cursor: pointer; + background-image: none; + border: 1px solid transparent; + white-space: nowrap; + padding: 8px 16px; + font-size: 15px; + line-height: 1.42857143; + border-radius: 0; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} +.btn:focus, +.btn:active:focus, +.btn.active:focus, +.btn.focus, +.btn:active.focus, +.btn.active.focus { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +.btn:hover, +.btn:focus, +.btn.focus { + color: #ffffff; + text-decoration: none; +} +.btn:active, +.btn.active { + outline: 0; + background-image: none; + -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); +} +.btn.disabled, +.btn[disabled], +fieldset[disabled] .btn { + cursor: not-allowed; + opacity: 0.65; + filter: alpha(opacity=65); + -webkit-box-shadow: none; + box-shadow: none; +} +a.btn.disabled, +fieldset[disabled] a.btn { + pointer-events: none; +} +.btn-default { + color: #ffffff; + background-color: #4e5d6c; + border-color: transparent; +} +.btn-default:focus, +.btn-default.focus { + color: #ffffff; + background-color: #39444e; + border-color: rgba(0, 0, 0, 0); +} +.btn-default:hover { + color: #ffffff; + background-color: #39444e; + border-color: rgba(0, 0, 0, 0); +} +.btn-default:active, +.btn-default.active, +.open > .dropdown-toggle.btn-default { + color: #ffffff; + background-color: #39444e; + border-color: rgba(0, 0, 0, 0); +} +.btn-default:active:hover, +.btn-default.active:hover, +.open > .dropdown-toggle.btn-default:hover, +.btn-default:active:focus, +.btn-default.active:focus, +.open > .dropdown-toggle.btn-default:focus, +.btn-default:active.focus, +.btn-default.active.focus, +.open > .dropdown-toggle.btn-default.focus { + color: #ffffff; + background-color: #2a323a; + border-color: rgba(0, 0, 0, 0); +} +.btn-default:active, +.btn-default.active, +.open > .dropdown-toggle.btn-default { + background-image: none; +} +.btn-default.disabled:hover, +.btn-default[disabled]:hover, +fieldset[disabled] .btn-default:hover, +.btn-default.disabled:focus, +.btn-default[disabled]:focus, +fieldset[disabled] .btn-default:focus, +.btn-default.disabled.focus, +.btn-default[disabled].focus, +fieldset[disabled] .btn-default.focus { + background-color: #4e5d6c; + border-color: transparent; +} +.btn-default .badge { + color: #4e5d6c; + background-color: #ffffff; +} +.btn-primary { + color: #ffffff; + background-color: #df691a; + border-color: transparent; +} +.btn-primary:focus, +.btn-primary.focus { + color: #ffffff; + background-color: #b15315; + border-color: rgba(0, 0, 0, 0); +} +.btn-primary:hover { + color: #ffffff; + background-color: #b15315; + border-color: rgba(0, 0, 0, 0); +} +.btn-primary:active, +.btn-primary.active, +.open > .dropdown-toggle.btn-primary { + color: #ffffff; + background-color: #b15315; + border-color: rgba(0, 0, 0, 0); +} +.btn-primary:active:hover, +.btn-primary.active:hover, +.open > .dropdown-toggle.btn-primary:hover, +.btn-primary:active:focus, +.btn-primary.active:focus, +.open > .dropdown-toggle.btn-primary:focus, +.btn-primary:active.focus, +.btn-primary.active.focus, +.open > .dropdown-toggle.btn-primary.focus { + color: #ffffff; + background-color: #914411; + border-color: rgba(0, 0, 0, 0); +} +.btn-primary:active, +.btn-primary.active, +.open > .dropdown-toggle.btn-primary { + background-image: none; +} +.btn-primary.disabled:hover, +.btn-primary[disabled]:hover, +fieldset[disabled] .btn-primary:hover, +.btn-primary.disabled:focus, +.btn-primary[disabled]:focus, +fieldset[disabled] .btn-primary:focus, +.btn-primary.disabled.focus, +.btn-primary[disabled].focus, +fieldset[disabled] .btn-primary.focus { + background-color: #df691a; + border-color: transparent; +} +.btn-primary .badge { + color: #df691a; + background-color: #ffffff; +} +.btn-success { + color: #ffffff; + background-color: #5cb85c; + border-color: transparent; +} +.btn-success:focus, +.btn-success.focus { + color: #ffffff; + background-color: #449d44; + border-color: rgba(0, 0, 0, 0); +} +.btn-success:hover { + color: #ffffff; + background-color: #449d44; + border-color: rgba(0, 0, 0, 0); +} +.btn-success:active, +.btn-success.active, +.open > .dropdown-toggle.btn-success { + color: #ffffff; + background-color: #449d44; + border-color: rgba(0, 0, 0, 0); +} +.btn-success:active:hover, +.btn-success.active:hover, +.open > .dropdown-toggle.btn-success:hover, +.btn-success:active:focus, +.btn-success.active:focus, +.open > .dropdown-toggle.btn-success:focus, +.btn-success:active.focus, +.btn-success.active.focus, +.open > .dropdown-toggle.btn-success.focus { + color: #ffffff; + background-color: #398439; + border-color: rgba(0, 0, 0, 0); +} +.btn-success:active, +.btn-success.active, +.open > .dropdown-toggle.btn-success { + background-image: none; +} +.btn-success.disabled:hover, +.btn-success[disabled]:hover, +fieldset[disabled] .btn-success:hover, +.btn-success.disabled:focus, +.btn-success[disabled]:focus, +fieldset[disabled] .btn-success:focus, +.btn-success.disabled.focus, +.btn-success[disabled].focus, +fieldset[disabled] .btn-success.focus { + background-color: #5cb85c; + border-color: transparent; +} +.btn-success .badge { + color: #5cb85c; + background-color: #ffffff; +} +.btn-info { + color: #ffffff; + background-color: #5bc0de; + border-color: transparent; +} +.btn-info:focus, +.btn-info.focus { + color: #ffffff; + background-color: #31b0d5; + border-color: rgba(0, 0, 0, 0); +} +.btn-info:hover { + color: #ffffff; + background-color: #31b0d5; + border-color: rgba(0, 0, 0, 0); +} +.btn-info:active, +.btn-info.active, +.open > .dropdown-toggle.btn-info { + color: #ffffff; + background-color: #31b0d5; + border-color: rgba(0, 0, 0, 0); +} +.btn-info:active:hover, +.btn-info.active:hover, +.open > .dropdown-toggle.btn-info:hover, +.btn-info:active:focus, +.btn-info.active:focus, +.open > .dropdown-toggle.btn-info:focus, +.btn-info:active.focus, +.btn-info.active.focus, +.open > .dropdown-toggle.btn-info.focus { + color: #ffffff; + background-color: #269abc; + border-color: rgba(0, 0, 0, 0); +} +.btn-info:active, +.btn-info.active, +.open > .dropdown-toggle.btn-info { + background-image: none; +} +.btn-info.disabled:hover, +.btn-info[disabled]:hover, +fieldset[disabled] .btn-info:hover, +.btn-info.disabled:focus, +.btn-info[disabled]:focus, +fieldset[disabled] .btn-info:focus, +.btn-info.disabled.focus, +.btn-info[disabled].focus, +fieldset[disabled] .btn-info.focus { + background-color: #5bc0de; + border-color: transparent; +} +.btn-info .badge { + color: #5bc0de; + background-color: #ffffff; +} +.btn-warning { + color: #ffffff; + background-color: #f0ad4e; + border-color: transparent; +} +.btn-warning:focus, +.btn-warning.focus { + color: #ffffff; + background-color: #ec971f; + border-color: rgba(0, 0, 0, 0); +} +.btn-warning:hover { + color: #ffffff; + background-color: #ec971f; + border-color: rgba(0, 0, 0, 0); +} +.btn-warning:active, +.btn-warning.active, +.open > .dropdown-toggle.btn-warning { + color: #ffffff; + background-color: #ec971f; + border-color: rgba(0, 0, 0, 0); +} +.btn-warning:active:hover, +.btn-warning.active:hover, +.open > .dropdown-toggle.btn-warning:hover, +.btn-warning:active:focus, +.btn-warning.active:focus, +.open > .dropdown-toggle.btn-warning:focus, +.btn-warning:active.focus, +.btn-warning.active.focus, +.open > .dropdown-toggle.btn-warning.focus { + color: #ffffff; + background-color: #d58512; + border-color: rgba(0, 0, 0, 0); +} +.btn-warning:active, +.btn-warning.active, +.open > .dropdown-toggle.btn-warning { + background-image: none; +} +.btn-warning.disabled:hover, +.btn-warning[disabled]:hover, +fieldset[disabled] .btn-warning:hover, +.btn-warning.disabled:focus, +.btn-warning[disabled]:focus, +fieldset[disabled] .btn-warning:focus, +.btn-warning.disabled.focus, +.btn-warning[disabled].focus, +fieldset[disabled] .btn-warning.focus { + background-color: #f0ad4e; + border-color: transparent; +} +.btn-warning .badge { + color: #f0ad4e; + background-color: #ffffff; +} +.btn-danger { + color: #ffffff; + background-color: #d9534f; + border-color: transparent; +} +.btn-danger:focus, +.btn-danger.focus { + color: #ffffff; + background-color: #c9302c; + border-color: rgba(0, 0, 0, 0); +} +.btn-danger:hover { + color: #ffffff; + background-color: #c9302c; + border-color: rgba(0, 0, 0, 0); +} +.btn-danger:active, +.btn-danger.active, +.open > .dropdown-toggle.btn-danger { + color: #ffffff; + background-color: #c9302c; + border-color: rgba(0, 0, 0, 0); +} +.btn-danger:active:hover, +.btn-danger.active:hover, +.open > .dropdown-toggle.btn-danger:hover, +.btn-danger:active:focus, +.btn-danger.active:focus, +.open > .dropdown-toggle.btn-danger:focus, +.btn-danger:active.focus, +.btn-danger.active.focus, +.open > .dropdown-toggle.btn-danger.focus { + color: #ffffff; + background-color: #ac2925; + border-color: rgba(0, 0, 0, 0); +} +.btn-danger:active, +.btn-danger.active, +.open > .dropdown-toggle.btn-danger { + background-image: none; +} +.btn-danger.disabled:hover, +.btn-danger[disabled]:hover, +fieldset[disabled] .btn-danger:hover, +.btn-danger.disabled:focus, +.btn-danger[disabled]:focus, +fieldset[disabled] .btn-danger:focus, +.btn-danger.disabled.focus, +.btn-danger[disabled].focus, +fieldset[disabled] .btn-danger.focus { + background-color: #d9534f; + border-color: transparent; +} +.btn-danger .badge { + color: #d9534f; + background-color: #ffffff; +} +.btn-link { + color: #df691a; + font-weight: normal; + border-radius: 0; +} +.btn-link, +.btn-link:active, +.btn-link.active, +.btn-link[disabled], +fieldset[disabled] .btn-link { + background-color: transparent; + -webkit-box-shadow: none; + box-shadow: none; +} +.btn-link, +.btn-link:hover, +.btn-link:focus, +.btn-link:active { + border-color: transparent; +} +.btn-link:hover, +.btn-link:focus { + color: #df691a; + text-decoration: underline; + background-color: transparent; +} +.btn-link[disabled]:hover, +fieldset[disabled] .btn-link:hover, +.btn-link[disabled]:focus, +fieldset[disabled] .btn-link:focus { + color: #4e5d6c; + text-decoration: none; +} +.btn-lg, +.btn-group-lg > .btn { + padding: 12px 24px; + font-size: 19px; + line-height: 1.3333333; + border-radius: 0; +} +.btn-sm, +.btn-group-sm > .btn { + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 0; +} +.btn-xs, +.btn-group-xs > .btn { + padding: 1px 5px; + font-size: 12px; + line-height: 1.5; + border-radius: 0; +} +.btn-block { + display: block; + width: 100%; +} +.btn-block + .btn-block { + margin-top: 5px; +} +input[type="submit"].btn-block, +input[type="reset"].btn-block, +input[type="button"].btn-block { + width: 100%; +} +.fade { + opacity: 0; + -webkit-transition: opacity 0.15s linear; + -o-transition: opacity 0.15s linear; + transition: opacity 0.15s linear; +} +.fade.in { + opacity: 1; +} +.collapse { + display: none; +} +.collapse.in { + display: block; +} +tr.collapse.in { + display: table-row; +} +tbody.collapse.in { + display: table-row-group; +} +.collapsing { + position: relative; + height: 0; + overflow: hidden; + -webkit-transition-property: height, visibility; + -o-transition-property: height, visibility; + transition-property: height, visibility; + -webkit-transition-duration: 0.35s; + -o-transition-duration: 0.35s; + transition-duration: 0.35s; + -webkit-transition-timing-function: ease; + -o-transition-timing-function: ease; + transition-timing-function: ease; +} +.caret { + display: inline-block; + width: 0; + height: 0; + margin-left: 2px; + vertical-align: middle; + border-top: 4px dashed; + border-top: 4px solid \9; + border-right: 4px solid transparent; + border-left: 4px solid transparent; +} +.dropup, +.dropdown { + position: relative; +} +.dropdown-toggle:focus { + outline: 0; +} +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + display: none; + float: left; + min-width: 160px; + padding: 5px 0; + margin: 2px 0 0; + list-style: none; + font-size: 15px; + text-align: left; + background-color: #282828; + border: 1px solid transparent; + border-radius: 0; + -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); + -webkit-background-clip: padding-box; + background-clip: padding-box; +} +.dropdown-menu.pull-right { + right: 0; + left: auto; +} +.dropdown-menu .divider { + height: 1px; + margin: 9.5px 0; + overflow: hidden; + background-color: #333333; +} +.dropdown-menu > li > a { + display: block; + padding: 3px 20px; + clear: both; + font-weight: normal; + line-height: 1.42857143; + color: #ebebeb; + white-space: nowrap; +} +.dropdown-menu > li > a:hover, +.dropdown-menu > li > a:focus { + text-decoration: none; + color: #ebebeb; + background-color: #333333; +} +.dropdown-menu > .active > a, +.dropdown-menu > .active > a:hover, +.dropdown-menu > .active > a:focus { + color: #ffffff; + text-decoration: none; + outline: 0; + background-color: #df691a; +} +.dropdown-menu > .disabled > a, +.dropdown-menu > .disabled > a:hover, +.dropdown-menu > .disabled > a:focus { + color: #2b3e50; +} +.dropdown-menu > .disabled > a:hover, +.dropdown-menu > .disabled > a:focus { + text-decoration: none; + background-color: transparent; + background-image: none; + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + cursor: not-allowed; +} +.open > .dropdown-menu { + display: block; +} +.open > a { + outline: 0; +} +.dropdown-menu-right { + left: auto; + right: 0; +} +.dropdown-menu-left { + left: 0; + right: auto; +} +.dropdown-header { + display: block; + padding: 3px 20px; + font-size: 12px; + line-height: 1.42857143; + color: #2b3e50; + white-space: nowrap; +} +.dropdown-backdrop { + position: fixed; + left: 0; + right: 0; + bottom: 0; + top: 0; + z-index: 990; +} +.pull-right > .dropdown-menu { + right: 0; + left: auto; +} +.dropup .caret, +.navbar-fixed-bottom .dropdown .caret { + border-top: 0; + border-bottom: 4px dashed; + border-bottom: 4px solid \9; + content: ""; +} +.dropup .dropdown-menu, +.navbar-fixed-bottom .dropdown .dropdown-menu { + top: auto; + bottom: 100%; + margin-bottom: 2px; +} +@media (min-width: 768px) { + .navbar-right .dropdown-menu { + left: auto; + right: 0; + } + .navbar-right .dropdown-menu-left { + left: 0; + right: auto; + } +} +.btn-group, +.btn-group-vertical { + position: relative; + display: inline-block; + vertical-align: middle; +} +.btn-group > .btn, +.btn-group-vertical > .btn { + position: relative; + float: left; +} +.btn-group > .btn:hover, +.btn-group-vertical > .btn:hover, +.btn-group > .btn:focus, +.btn-group-vertical > .btn:focus, +.btn-group > .btn:active, +.btn-group-vertical > .btn:active, +.btn-group > .btn.active, +.btn-group-vertical > .btn.active { + z-index: 2; +} +.btn-group .btn + .btn, +.btn-group .btn + .btn-group, +.btn-group .btn-group + .btn, +.btn-group .btn-group + .btn-group { + margin-left: -1px; +} +.btn-toolbar { + margin-left: -5px; +} +.btn-toolbar .btn, +.btn-toolbar .btn-group, +.btn-toolbar .input-group { + float: left; +} +.btn-toolbar > .btn, +.btn-toolbar > .btn-group, +.btn-toolbar > .input-group { + margin-left: 5px; +} +.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) { + border-radius: 0; +} +.btn-group > .btn:first-child { + margin-left: 0; +} +.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) { + border-bottom-right-radius: 0; + border-top-right-radius: 0; +} +.btn-group > .btn:last-child:not(:first-child), +.btn-group > .dropdown-toggle:not(:first-child) { + border-bottom-left-radius: 0; + border-top-left-radius: 0; +} +.btn-group > .btn-group { + float: left; +} +.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} +.btn-group > .btn-group:first-child:not(:last-child) > .btn:last-child, +.btn-group > .btn-group:first-child:not(:last-child) > .dropdown-toggle { + border-bottom-right-radius: 0; + border-top-right-radius: 0; +} +.btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child { + border-bottom-left-radius: 0; + border-top-left-radius: 0; +} +.btn-group .dropdown-toggle:active, +.btn-group.open .dropdown-toggle { + outline: 0; +} +.btn-group > .btn + .dropdown-toggle { + padding-left: 8px; + padding-right: 8px; +} +.btn-group > .btn-lg + .dropdown-toggle { + padding-left: 12px; + padding-right: 12px; +} +.btn-group.open .dropdown-toggle { + -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); +} +.btn-group.open .dropdown-toggle.btn-link { + -webkit-box-shadow: none; + box-shadow: none; +} +.btn .caret { + margin-left: 0; +} +.btn-lg .caret { + border-width: 5px 5px 0; + border-bottom-width: 0; +} +.dropup .btn-lg .caret { + border-width: 0 5px 5px; +} +.btn-group-vertical > .btn, +.btn-group-vertical > .btn-group, +.btn-group-vertical > .btn-group > .btn { + display: block; + float: none; + width: 100%; + max-width: 100%; +} +.btn-group-vertical > .btn-group > .btn { + float: none; +} +.btn-group-vertical > .btn + .btn, +.btn-group-vertical > .btn + .btn-group, +.btn-group-vertical > .btn-group + .btn, +.btn-group-vertical > .btn-group + .btn-group { + margin-top: -1px; + margin-left: 0; +} +.btn-group-vertical > .btn:not(:first-child):not(:last-child) { + border-radius: 0; +} +.btn-group-vertical > .btn:first-child:not(:last-child) { + border-top-right-radius: 0; + border-top-left-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} +.btn-group-vertical > .btn:last-child:not(:first-child) { + border-top-right-radius: 0; + border-top-left-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} +.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} +.btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child, +.btn-group-vertical > .btn-group:first-child:not(:last-child) > .dropdown-toggle { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} +.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child { + border-top-right-radius: 0; + border-top-left-radius: 0; +} +.btn-group-justified { + display: table; + width: 100%; + table-layout: fixed; + border-collapse: separate; +} +.btn-group-justified > .btn, +.btn-group-justified > .btn-group { + float: none; + display: table-cell; + width: 1%; +} +.btn-group-justified > .btn-group .btn { + width: 100%; +} +.btn-group-justified > .btn-group .dropdown-menu { + left: auto; +} +[data-toggle="buttons"] > .btn input[type="radio"], +[data-toggle="buttons"] > .btn-group > .btn input[type="radio"], +[data-toggle="buttons"] > .btn input[type="checkbox"], +[data-toggle="buttons"] > .btn-group > .btn input[type="checkbox"] { + position: absolute; + clip: rect(0, 0, 0, 0); + pointer-events: none; +} +.input-group { + position: relative; + display: table; + border-collapse: separate; +} +.input-group[class*="col-"] { + float: none; + padding-left: 0; + padding-right: 0; +} +.input-group .form-control { + position: relative; + z-index: 2; + float: left; + width: 100%; + margin-bottom: 0; +} +.input-group .form-control:focus { + z-index: 3; +} +.input-group-lg > .form-control, +.input-group-lg > .input-group-addon, +.input-group-lg > .input-group-btn > .btn { + height: 52px; + padding: 12px 24px; + font-size: 19px; + line-height: 1.3333333; + border-radius: 0; +} +select.input-group-lg > .form-control, +select.input-group-lg > .input-group-addon, +select.input-group-lg > .input-group-btn > .btn { + height: 52px; + line-height: 52px; +} +textarea.input-group-lg > .form-control, +textarea.input-group-lg > .input-group-addon, +textarea.input-group-lg > .input-group-btn > .btn, +select[multiple].input-group-lg > .form-control, +select[multiple].input-group-lg > .input-group-addon, +select[multiple].input-group-lg > .input-group-btn > .btn { + height: auto; +} +.input-group-sm > .form-control, +.input-group-sm > .input-group-addon, +.input-group-sm > .input-group-btn > .btn { + height: 30px; + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 0; +} +select.input-group-sm > .form-control, +select.input-group-sm > .input-group-addon, +select.input-group-sm > .input-group-btn > .btn { + height: 30px; + line-height: 30px; +} +textarea.input-group-sm > .form-control, +textarea.input-group-sm > .input-group-addon, +textarea.input-group-sm > .input-group-btn > .btn, +select[multiple].input-group-sm > .form-control, +select[multiple].input-group-sm > .input-group-addon, +select[multiple].input-group-sm > .input-group-btn > .btn { + height: auto; +} +.input-group-addon, +.input-group-btn, +.input-group .form-control { + display: table-cell; +} +.input-group-addon:not(:first-child):not(:last-child), +.input-group-btn:not(:first-child):not(:last-child), +.input-group .form-control:not(:first-child):not(:last-child) { + border-radius: 0; +} +.input-group-addon, +.input-group-btn { + width: 1%; + white-space: nowrap; + vertical-align: middle; +} +.input-group-addon { + padding: 8px 16px; + font-size: 15px; + font-weight: normal; + line-height: 1; + color: #2b3e50; + text-align: center; + background-color: #333333; + border: 1px solid transparent; + border-radius: 0; +} +.input-group-addon.input-sm { + padding: 5px 10px; + font-size: 12px; + border-radius: 0; +} +.input-group-addon.input-lg { + padding: 12px 24px; + font-size: 19px; + border-radius: 0; +} +.input-group-addon input[type="radio"], +.input-group-addon input[type="checkbox"] { + margin-top: 0; +} +.input-group .form-control:first-child, +.input-group-addon:first-child, +.input-group-btn:first-child > .btn, +.input-group-btn:first-child > .btn-group > .btn, +.input-group-btn:first-child > .dropdown-toggle, +.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle), +.input-group-btn:last-child > .btn-group:not(:last-child) > .btn { + border-bottom-right-radius: 0; + border-top-right-radius: 0; +} +.input-group-addon:first-child { + border-right: 0; +} +.input-group .form-control:last-child, +.input-group-addon:last-child, +.input-group-btn:last-child > .btn, +.input-group-btn:last-child > .btn-group > .btn, +.input-group-btn:last-child > .dropdown-toggle, +.input-group-btn:first-child > .btn:not(:first-child), +.input-group-btn:first-child > .btn-group:not(:first-child) > .btn { + border-bottom-left-radius: 0; + border-top-left-radius: 0; +} +.input-group-addon:last-child { + border-left: 0; +} +.input-group-btn { + position: relative; + font-size: 0; + white-space: nowrap; +} +.input-group-btn > .btn { + position: relative; +} +.input-group-btn > .btn + .btn { + margin-left: -1px; +} +.input-group-btn > .btn:hover, +.input-group-btn > .btn:focus, +.input-group-btn > .btn:active { + z-index: 2; +} +.input-group-btn:first-child > .btn, +.input-group-btn:first-child > .btn-group { + margin-right: -1px; +} +.input-group-btn:last-child > .btn, +.input-group-btn:last-child > .btn-group { + z-index: 2; + margin-left: -1px; +} +.nav { + margin-bottom: 0; + padding-left: 0; + list-style: none; +} +.nav > li { + position: relative; + display: block; +} +.nav > li > a { + position: relative; + display: block; + padding: 10px 15px; +} +.nav > li > a:hover, +.nav > li > a:focus { + text-decoration: none; + background-color: #df691a; +} +.nav > li.disabled > a { + color: #4e5d6c; +} +.nav > li.disabled > a:hover, +.nav > li.disabled > a:focus { + color: #4e5d6c; + text-decoration: none; + background-color: transparent; + cursor: not-allowed; +} +.nav .open > a, +.nav .open > a:hover, +.nav .open > a:focus { + background-color: #4e5d6c; + border-color: #df691a; +} +.nav .nav-divider { + height: 1px; + margin: 9.5px 0; + overflow: hidden; + background-color: #e5e5e5; +} +.nav > li > a > img { + max-width: none; +} +.nav-tabs { + border-bottom: 1px solid transparent; +} +.nav-tabs > li { + float: left; + margin-bottom: -1px; +} +.nav-tabs > li > a { + margin-right: 2px; + line-height: 1.42857143; + border: 1px solid transparent; + border-radius: 0 0 0 0; +} +.nav-tabs > li > a:hover { + border-color: #df691a #df691a transparent; + + +} +.nav-tabs > li.active > a, +.nav-tabs > li.active > a:hover, +.nav-tabs > li.active > a:focus { + color: #ebebeb; + background-color: #df691a; + border: 1px solid #df691a; + border-bottom-color: transparent; + cursor: default; +} +.nav-tabs.nav-justified { + width: 100%; + border-bottom: 0; +} +.nav-tabs.nav-justified > li { + float: none; +} +.nav-tabs.nav-justified > li > a { + text-align: center; + margin-bottom: 5px; +} +.nav-tabs.nav-justified > .dropdown .dropdown-menu { + top: auto; + left: auto; +} +@media (min-width: 768px) { + .nav-tabs.nav-justified > li { + display: table-cell; + width: 1%; + } + .nav-tabs.nav-justified > li > a { + margin-bottom: 0; + } +} +.nav-tabs.nav-justified > li > a { + margin-right: 0; + border-radius: 0; +} +.nav-tabs.nav-justified > .active > a, +.nav-tabs.nav-justified > .active > a:hover, +.nav-tabs.nav-justified > .active > a:focus { + border: 1px solid #df691a; +} +@media (min-width: 768px) { + .nav-tabs.nav-justified > li > a { + border-bottom: 1px solid #4e5d6c; + border-radius: 0 0 0 0; + } + .nav-tabs.nav-justified > .active > a, + .nav-tabs.nav-justified > .active > a:hover, + .nav-tabs.nav-justified > .active > a:focus { + border-bottom-color: #4e5d6c; + } +} +.nav-pills > li { + float: left; +} +.nav-pills > li > a { + border-radius: 0; +} +.nav-pills > li + li { + margin-left: 2px; +} +.nav-pills > li.active > a, +.nav-pills > li.active > a:hover, +.nav-pills > li.active > a:focus { + color: #ffffff; + background-color: #df691a; +} +.nav-stacked > li { + float: none; +} +.nav-stacked > li + li { + margin-top: 2px; + margin-left: 0; +} +.nav-justified { + width: 100%; +} +.nav-justified > li { + float: none; +} +.nav-justified > li > a { + text-align: center; + margin-bottom: 5px; +} +.nav-justified > .dropdown .dropdown-menu { + top: auto; + left: auto; +} +@media (min-width: 768px) { + .nav-justified > li { + display: table-cell; + width: 1%; + } + .nav-justified > li > a { + margin-bottom: 0; + } +} +.nav-tabs-justified { + border-bottom: 0; +} +.nav-tabs-justified > li > a { + margin-right: 0; + border-radius: 0; +} +.nav-tabs-justified > .active > a, +.nav-tabs-justified > .active > a:hover, +.nav-tabs-justified > .active > a:focus { + border: 1px solid #4e5d6c; +} +@media (min-width: 768px) { + .nav-tabs-justified > li > a { + border-bottom: 1px solid #4e5d6c; + border-radius: 0 0 0 0; + } + .nav-tabs-justified > .active > a, + .nav-tabs-justified > .active > a:hover, + .nav-tabs-justified > .active > a:focus { + border-bottom-color: #4e5d6c; + } +} +.tab-content > .tab-pane { + display: none; +} +.tab-content > .active { + display: block; +} +.nav-tabs .dropdown-menu { + margin-top: -1px; + border-top-right-radius: 0; + border-top-left-radius: 0; +} +.navbar { + position: fixed; + top: 0px; + right: 0px; + left: 0px; + z-index: 1000; + padding: 0px 3px; + font-size: 24px; + background-color: #000; + box-shadow: 0px 0px 0px 3px rgba(0, 0, 0, 0.2); +} + + +@media (min-width: 768px) { + .navbar { + border-radius: 0; + } +} +@media (min-width: 768px) { + .navbar-header { + float: left; + } +} +.navbar-collapse { + overflow-x: visible; + padding-right: 15px; + padding-left: 15px; + border-top: 1px solid transparent; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1); + -webkit-overflow-scrolling: touch; +} +.navbar-collapse.in { + overflow-y: auto; +} +@media (min-width: 768px) { + .navbar-collapse { + width: auto; + border-top: 0; + -webkit-box-shadow: none; + box-shadow: none; + } + .navbar-collapse.collapse { + display: block !important; + height: auto !important; + padding-bottom: 0; + overflow: visible !important; + } + .navbar-collapse.in { + overflow-y: visible; + } + .navbar-fixed-top .navbar-collapse, + .navbar-static-top .navbar-collapse, + .navbar-fixed-bottom .navbar-collapse { + padding-left: 0; + padding-right: 0; + } +} +.navbar-fixed-top .navbar-collapse, +.navbar-fixed-bottom .navbar-collapse { + max-height: 340px; +} +@media (max-device-width: 480px) and (orientation: landscape) { + .navbar-fixed-top .navbar-collapse, + .navbar-fixed-bottom .navbar-collapse { + max-height: 200px; + } +} +.container > .navbar-header, +.container-fluid > .navbar-header, +.container > .navbar-collapse, +.container-fluid > .navbar-collapse { + margin-right: -15px; + margin-left: -15px; +} +@media (min-width: 768px) { + .container > .navbar-header, + .container-fluid > .navbar-header, + .container > .navbar-collapse, + .container-fluid > .navbar-collapse { + margin-right: 0; + margin-left: 0; + } +} +.navbar-static-top { + z-index: 1000; + border-width: 0 0 1px; +} +@media (min-width: 768px) { + .navbar-static-top { + border-radius: 0; + } +} +.navbar-fixed-top, +.navbar-fixed-bottom { + position: fixed; + right: 0; + left: 0; + z-index: 1030; +} +@media (min-width: 768px) { + .navbar-fixed-top, + .navbar-fixed-bottom { + border-radius: 0; + } +} +.navbar-fixed-top { + top: 0; + border-width: 0 0 1px; +} +.navbar-fixed-bottom { + bottom: 0; + margin-bottom: 0; + border-width: 1px 0 0; +} +.navbar-brand { + float: left; + padding: 9.5px 15px; + font-size: 19px; + line-height: 21px; + height: 40px; +} +.navbar-brand:hover, +.navbar-brand:focus { + text-decoration: none; +} +.navbar-brand > img { + display: block; +} +@media (min-width: 768px) { + .navbar > .container .navbar-brand, + .navbar > .container-fluid .navbar-brand { + margin-left: -15px; + } +} +.navbar-toggle { + position: relative; + float: right; + margin-right: 15px; + padding: 9px 10px; + margin-top: 3px; + margin-bottom: 3px; + background-color: transparent; + background-image: none; + border: 1px solid transparent; + border-radius: 0; +} +.navbar-toggle:focus { + outline: 0; +} +.navbar-toggle .icon-bar { + display: block; + width: 22px; + height: 2px; + border-radius: 1px; +} +.navbar-toggle .icon-bar + .icon-bar { + margin-top: 4px; +} +@media (min-width: 768px) { + .navbar-toggle { + display: none; + } +} +.navbar-nav { + margin: 4.75px -15px; +} +.navbar-nav > li > a { + padding-top: 10px; + padding-bottom: 10px; + line-height: 21px; +} +@media (max-width: 767px) { + .navbar-nav .open .dropdown-menu { + position: static; + float: none; + width: auto; + margin-top: 0; + background-color: transparent; + border: 0; + -webkit-box-shadow: none; + box-shadow: none; + } + .navbar-nav .open .dropdown-menu > li > a, + .navbar-nav .open .dropdown-menu .dropdown-header { + padding: 5px 15px 5px 25px; + } + .navbar-nav .open .dropdown-menu > li > a { + line-height: 21px; + } + .navbar-nav .open .dropdown-menu > li > a:hover, + .navbar-nav .open .dropdown-menu > li > a:focus { + background-image: none; + } +} +@media (min-width: 768px) { + .navbar-nav { + float: left; + margin: 0; + } + .navbar-nav > li { + float: left; + } + .navbar-nav > li > a { + padding-top: 9.5px; + padding-bottom: 9.5px; + } +} +.navbar-form { + margin-left: -15px; + margin-right: -15px; + padding: 10px 15px; + border-top: 1px solid transparent; + border-bottom: 1px solid transparent; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); + margin-top: 0.5px; + margin-bottom: 0.5px; +} +@media (min-width: 768px) { + .navbar-form .form-group { + display: inline-block; + margin-bottom: 0; + vertical-align: middle; + } + .navbar-form .form-control { + display: inline-block; + width: auto; + vertical-align: middle; + } + .navbar-form .form-control-static { + display: inline-block; + } + .navbar-form .input-group { + display: inline-table; + vertical-align: middle; + } + .navbar-form .input-group .input-group-addon, + .navbar-form .input-group .input-group-btn, + .navbar-form .input-group .form-control { + width: auto; + } + .navbar-form .input-group > .form-control { + width: 100%; + } + .navbar-form .control-label { + margin-bottom: 0; + vertical-align: middle; + } + .navbar-form .radio, + .navbar-form .checkbox { + display: inline-block; + margin-top: 0; + margin-bottom: 0; + vertical-align: middle; + } + .navbar-form .radio label, + .navbar-form .checkbox label { + padding-left: 0; + } + .navbar-form .radio input[type="radio"], + .navbar-form .checkbox input[type="checkbox"] { + position: relative; + margin-left: 0; + } + .navbar-form .has-feedback .form-control-feedback { + top: 0; + } +} +@media (max-width: 767px) { + .navbar-form .form-group { + margin-bottom: 5px; + } + .navbar-form .form-group:last-child { + margin-bottom: 0; + } +} +@media (min-width: 768px) { + .navbar-form { + width: auto; + border: 0; + margin-left: 0; + margin-right: 0; + padding-top: 0; + padding-bottom: 0; + -webkit-box-shadow: none; + box-shadow: none; + } +} +.navbar-nav > li > .dropdown-menu { + margin-top: 0; + border-top-right-radius: 0; + border-top-left-radius: 0; +} +.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu { + margin-bottom: 0; + border-top-right-radius: 0; + border-top-left-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} +.navbar-btn { + margin-top: 0.5px; + margin-bottom: 0.5px; +} +.navbar-btn.btn-sm { + margin-top: 5px; + margin-bottom: 5px; +} +.navbar-btn.btn-xs { + margin-top: 9px; + margin-bottom: 9px; +} +.navbar-text { + margin-top: 9.5px; + margin-bottom: 9.5px; +} +@media (min-width: 768px) { + .navbar-text { + float: left; + margin-left: 15px; + margin-right: 15px; + } +} +@media (min-width: 768px) { + .navbar-left { + float: left !important; + } + .navbar-right { + float: right !important; + margin-right: -15px; + } + .navbar-right ~ .navbar-right { + margin-right: 0; + } +} +.navbar-default { + background-color: #0a0a0a; + border-color: transparent; +} +.navbar-default .navbar-brand { + color: #DF691A; +} +.navbar-default .navbar-brand:hover, +.navbar-default .navbar-brand:focus { + color: #ebebeb; + background-color: transparent; +} +.navbar-default .navbar-text { + color: #ebebeb; +} +.navbar-default .navbar-nav > li > a { + color: #ebebeb; +} +.navbar-default .navbar-nav > li > a:hover, +.navbar-default .navbar-nav > li > a:focus { + color: #F0ad4e; + background-color: #282828; +} +.navbar-default .navbar-nav > .active > a, +.navbar-default .navbar-nav > .active > a:hover, +.navbar-default .navbar-nav > .active > a:focus { + color: #009688; + background-color: #282828; +} +.navbar-default .navbar-nav > .disabled > a, +.navbar-default .navbar-nav > .disabled > a:hover, +.navbar-default .navbar-nav > .disabled > a:focus { + color: #cccccc; + background-color: transparent; +} +.navbar-default .navbar-toggle { + border-color: transparent; +} +.navbar-default .navbar-toggle:hover, +.navbar-default .navbar-toggle:focus { + background-color: #485563; +} +.navbar-default .navbar-toggle .icon-bar { + background-color: #ebebeb; +} +.navbar-default .navbar-collapse, +.navbar-default .navbar-form { + border-color: transparent; +} +.navbar-default .navbar-nav > .open > a, +.navbar-default .navbar-nav > .open > a:hover, +.navbar-default .navbar-nav > .open > a:focus { + background-color: #f9be03; + color: #282828; +} +@media (max-width: 767px) { + .navbar-default .navbar-nav .open .dropdown-menu > li > a { + color: #ebebeb; + } + .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover, + .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus { + color: #ebebeb; + background-color: #485563; + } + .navbar-default .navbar-nav .open .dropdown-menu > .active > a, + .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover, + .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus { + color: #ebebeb; + background-color: #485563; + } + .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a, + .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover, + .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus { + color: #cccccc; + background-color: transparent; + } +} +.navbar-default .navbar-link { + color: #ebebeb; +} +.navbar-default .navbar-link:hover { + color: #ebebeb; +} +.navbar-default .btn-link { + color: #ebebeb; +} +.navbar-default .btn-link:hover, +.navbar-default .btn-link:focus { + color: #ebebeb; +} +.navbar-default .btn-link[disabled]:hover, +fieldset[disabled] .navbar-default .btn-link:hover, +.navbar-default .btn-link[disabled]:focus, +fieldset[disabled] .navbar-default .btn-link:focus { + color: #cccccc; +} +.navbar-inverse { + background-color: #df691a; + border-color: transparent; +} +.navbar-inverse .navbar-brand { + color: #ebebeb; +} +.navbar-inverse .navbar-brand:hover, +.navbar-inverse .navbar-brand:focus { + color: #ebebeb; + background-color: transparent; +} +.navbar-inverse .navbar-text { + color: #ebebeb; +} +.navbar-inverse .navbar-nav > li > a { + color: #ebebeb; +} +.navbar-inverse .navbar-nav > li > a:hover, +.navbar-inverse .navbar-nav > li > a:focus { + color: #ebebeb; + background-color: #c85e17; +} +.navbar-inverse .navbar-nav > .active > a, +.navbar-inverse .navbar-nav > .active > a:hover, +.navbar-inverse .navbar-nav > .active > a:focus { + color: #ebebeb; + background-color: #c85e17; +} +.navbar-inverse .navbar-nav > .disabled > a, +.navbar-inverse .navbar-nav > .disabled > a:hover, +.navbar-inverse .navbar-nav > .disabled > a:focus { + color: #444444; + background-color: transparent; +} +.navbar-inverse .navbar-toggle { + border-color: transparent; +} +.navbar-inverse .navbar-toggle:hover, +.navbar-inverse .navbar-toggle:focus { + background-color: #c85e17; +} +.navbar-inverse .navbar-toggle .icon-bar { + background-color: #ebebeb; +} +.navbar-inverse .navbar-collapse, +.navbar-inverse .navbar-form { + border-color: #bf5a16; +} +.navbar-inverse .navbar-nav > .open > a, +.navbar-inverse .navbar-nav > .open > a:hover, +.navbar-inverse .navbar-nav > .open > a:focus { + background-color: #c85e17; + color: #ebebeb; +} +@media (max-width: 767px) { + .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header { + border-color: transparent; + } + .navbar-inverse .navbar-nav .open .dropdown-menu .divider { + background-color: transparent; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > li > a { + color: #ebebeb; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus { + color: #ebebeb; + background-color: #c85e17; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a, + .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus { + color: #ebebeb; + background-color: #c85e17; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a, + .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus { + color: #444444; + background-color: transparent; + } +} +.navbar-inverse .navbar-link { + color: #ebebeb; +} +.navbar-inverse .navbar-link:hover { + color: #ebebeb; +} +.navbar-inverse .btn-link { + color: #ebebeb; +} +.navbar-inverse .btn-link:hover, +.navbar-inverse .btn-link:focus { + color: #ebebeb; +} +.navbar-inverse .btn-link[disabled]:hover, +fieldset[disabled] .navbar-inverse .btn-link:hover, +.navbar-inverse .btn-link[disabled]:focus, +fieldset[disabled] .navbar-inverse .btn-link:focus { + color: #444444; +} +.breadcrumb { + padding: 8px 15px; + margin-bottom: 21px; + list-style: none; + background-color: #4e5d6c; + border-radius: 0; +} +.breadcrumb > li { + display: inline-block; +} +.breadcrumb > li + li:before { + content: "/\00a0"; + padding: 0 5px; + color: #ebebeb; +} +.breadcrumb > .active { + color: #ebebeb; +} +.pagination { + display: inline-block; + padding-left: 0; + margin: 21px 0; + border-radius: 0; +} +.pagination > li { + display: inline; +} +.pagination > li > a, +.pagination > li > span { + position: relative; + float: left; + padding: 8px 16px; + line-height: 1.42857143; + text-decoration: none; + color: #ebebeb; + background-color: #282828; + border: 1px solid transparent; + margin-left: -1px; +} +.pagination > li:first-child > a, +.pagination > li:first-child > span { + margin-left: 0; + border-bottom-left-radius: 0; + border-top-left-radius: 0; +} +.pagination > li:last-child > a, +.pagination > li:last-child > span { + border-bottom-right-radius: 0; + border-top-right-radius: 0; +} +.pagination > li > a:hover, +.pagination > li > span:hover, +.pagination > li > a:focus, +.pagination > li > span:focus { + z-index: 2; + color: #ebebeb; + background-color: #333333; + border-color: transparent; +} +.pagination > .active > a, +.pagination > .active > span, +.pagination > .active > a:hover, +.pagination > .active > span:hover, +.pagination > .active > a:focus, +.pagination > .active > span:focus { + z-index: 3; + color: #ebebeb; + background-color: #df691a; + border-color: transparent; + cursor: default; +} +.pagination > .disabled > span, +.pagination > .disabled > span:hover, +.pagination > .disabled > span:focus, +.pagination > .disabled > a, +.pagination > .disabled > a:hover, +.pagination > .disabled > a:focus { + color: #fefefe; + background-color: #333333; + border-color: transparent; + cursor: not-allowed; +} +.pagination-lg > li > a, +.pagination-lg > li > span { + padding: 12px 24px; + font-size: 19px; + line-height: 1.3333333; +} +.pagination-lg > li:first-child > a, +.pagination-lg > li:first-child > span { + border-bottom-left-radius: 0; + border-top-left-radius: 0; +} +.pagination-lg > li:last-child > a, +.pagination-lg > li:last-child > span { + border-bottom-right-radius: 0; + border-top-right-radius: 0; +} +.pagination-sm > li > a, +.pagination-sm > li > span { + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; +} +.pagination-sm > li:first-child > a, +.pagination-sm > li:first-child > span { + border-bottom-left-radius: 0; + border-top-left-radius: 0; +} +.pagination-sm > li:last-child > a, +.pagination-sm > li:last-child > span { + border-bottom-right-radius: 0; + border-top-right-radius: 0; +} +.pager { + padding-left: 0; + margin: 21px 0; + list-style: none; + text-align: center; +} +.pager li { + display: inline; +} +.pager li > a, +.pager li > span { + display: inline-block; + padding: 5px 14px; + background-color: #4e5d6c; + border: 1px solid transparent; + border-radius: 15px; +} +.pager li > a:hover, +.pager li > a:focus { + text-decoration: none; + background-color: #485563; +} +.pager .next > a, +.pager .next > span { + float: right; +} +.pager .previous > a, +.pager .previous > span { + float: left; +} +.pager .disabled > a, +.pager .disabled > a:hover, +.pager .disabled > a:focus, +.pager .disabled > span { + color: #323c46; + background-color: #4e5d6c; + cursor: not-allowed; +} +.label { + display: inline; + padding: .2em .6em .3em; + font-size: 75%; + font-weight: bold; + line-height: 1; + color: #ffffff; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: .25em; +} +a.label:hover, +a.label:focus { + color: #ffffff; + text-decoration: none; + cursor: pointer; +} +.label:empty { + display: none; +} +.btn .label { + position: relative; + top: -1px; +} +.label-default { + background-color: #4e5d6c; +} +.label-default[href]:hover, +.label-default[href]:focus { + background-color: #39444e; +} +.label-primary { + background-color: #df691a; +} +.label-primary[href]:hover, +.label-primary[href]:focus { + background-color: #b15315; +} +.label-success { + background-color: #5cb85c; +} +.label-success[href]:hover, +.label-success[href]:focus { + background-color: #449d44; +} +.label-info { + background-color: #5bc0de; +} +.label-info[href]:hover, +.label-info[href]:focus { + background-color: #31b0d5; +} +.label-warning { + background-color: #f0ad4e; +} +.label-warning[href]:hover, +.label-warning[href]:focus { + background-color: #ec971f; +} +.label-danger { + background-color: #d9534f; +} +.label-danger[href]:hover, +.label-danger[href]:focus { + background-color: #c9302c; +} +.badge { + display: inline-block; + min-width: 10px; + padding: 3px 7px; + font-size: 12px; + font-weight: 300; + color: #ebebeb; + line-height: 1; + vertical-align: middle; + white-space: nowrap; + text-align: center; + background-color: #4e5d6c; + border-radius: 10px; +} +.badge:empty { + display: none; +} +.btn .badge { + position: relative; + top: -1px; +} +.btn-xs .badge, +.btn-group-xs > .btn .badge { + top: 0; + padding: 1px 5px; +} +a.badge:hover, +a.badge:focus { + color: #ffffff; + text-decoration: none; + cursor: pointer; +} +.list-group-item.active > .badge, +.nav-pills > .active > a > .badge { + color: #df691a; + background-color: #ffffff; +} +.list-group-item > .badge { + float: right; +} +.list-group-item > .badge + .badge { + margin-right: 5px; +} +.nav-pills > li > a > .badge { + margin-left: 3px; +} +.jumbotron { + padding-top: 30px; + padding-bottom: 30px; + margin-bottom: 30px; + color: inherit; + background-color: #4e5d6c; +} +.jumbotron h1, +.jumbotron .h1 { + color: inherit; +} +.jumbotron p { + margin-bottom: 15px; + font-size: 23px; + font-weight: 200; +} +.jumbotron > hr { + border-top-color: #39444e; +} +.container .jumbotron, +.container-fluid .jumbotron { + border-radius: 0; + padding-left: 15px; + padding-right: 15px; +} +.jumbotron .container { + max-width: 100%; +} +@media screen and (min-width: 768px) { + .jumbotron { + padding-top: 48px; + padding-bottom: 48px; + } + .container .jumbotron, + .container-fluid .jumbotron { + padding-left: 60px; + padding-right: 60px; + } + .jumbotron h1, + .jumbotron .h1 { + font-size: 68px; + } +} +.thumbnail { + display: block; + padding: 4px; + margin-bottom: 21px; + line-height: 1.42857143; + background-color: #2b3e50; + border: 1px solid #dddddd; + border-radius: 0; + -webkit-transition: border 0.2s ease-in-out; + -o-transition: border 0.2s ease-in-out; + transition: border 0.2s ease-in-out; +} +.thumbnail > img, +.thumbnail a > img { + margin-left: auto; + margin-right: auto; +} +a.thumbnail:hover, +a.thumbnail:focus, +a.thumbnail.active { + border-color: #df691a; +} +.thumbnail .caption { + padding: 9px; + color: #ebebeb; +} +.alert { + padding: 15px; + margin-bottom: 21px; + border: 1px solid transparent; + border-radius: 0; +} +.alert h4 { + margin-top: 0; + color: inherit; +} +.alert .alert-link { + font-weight: bold; +} +.alert > p, +.alert > ul { + margin-bottom: 0; +} +.alert > p + p { + margin-top: 5px; +} +.alert-dismissable, +.alert-dismissible { + padding-right: 35px; +} +.alert-dismissable .close, +.alert-dismissible .close { + position: relative; + top: -2px; + right: -21px; + color: inherit; +} +.alert-success { + background-color: #5cb85c; + border-color: transparent; + color: #ebebeb; +} +.alert-success hr { + border-top-color: rgba(0, 0, 0, 0); +} +.alert-success .alert-link { + color: #d2d2d2; +} +.alert-info { + background-color: #5bc0de; + border-color: transparent; + color: #ebebeb; +} +.alert-info hr { + border-top-color: rgba(0, 0, 0, 0); +} +.alert-info .alert-link { + color: #d2d2d2; +} +.alert-warning { + background-color: #f0ad4e; + border-color: transparent; + color: #ebebeb; +} +.alert-warning hr { + border-top-color: rgba(0, 0, 0, 0); +} +.alert-warning .alert-link { + color: #d2d2d2; +} +.alert-danger { + background-color: #d9534f; + border-color: transparent; + color: #ebebeb; +} +.alert-danger hr { + border-top-color: rgba(0, 0, 0, 0); +} +.alert-danger .alert-link { + color: #d2d2d2; +} +@-webkit-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} +@-o-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} +@keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} +.progress { + overflow: hidden; + height: 21px; + margin-bottom: 21px; + background-color: #4e5d6c; + border-radius: 0; + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); +} +.progress-bar { + float: left; + width: 0%; + height: 100%; + font-size: 12px; + line-height: 21px; + color: #ffffff; + text-align: center; + background-color: #df691a; + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + -webkit-transition: width 0.6s ease; + -o-transition: width 0.6s ease; + transition: width 0.6s ease; +} +.progress-striped .progress-bar, +.progress-bar-striped { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + -webkit-background-size: 40px 40px; + background-size: 40px 40px; +} +.progress.active .progress-bar, +.progress-bar.active { + -webkit-animation: progress-bar-stripes 2s linear infinite; + -o-animation: progress-bar-stripes 2s linear infinite; + animation: progress-bar-stripes 2s linear infinite; +} +.progress-bar-success { + background-color: #5cb85c; +} +.progress-striped .progress-bar-success { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} +.progress-bar-info { + background-color: #5bc0de; +} +.progress-striped .progress-bar-info { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} +.progress-bar-warning { + background-color: #f0ad4e; +} +.progress-striped .progress-bar-warning { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} +.progress-bar-danger { + background-color: #d9534f; +} +.progress-striped .progress-bar-danger { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} +.media { + margin-top: 15px; +} +.media:first-child { + margin-top: 0; +} +.media, +.media-body { + zoom: 1; + overflow: hidden; +} +.media-body { + width: 10000px; +} +.media-object { + display: block; +} +.media-object.img-thumbnail { + max-width: none; +} +.media-right, +.media > .pull-right { + padding-left: 10px; +} +.media-left, +.media > .pull-left { + padding-right: 10px; +} +.media-left, +.media-right, +.media-body { + display: table-cell; + vertical-align: top; +} +.media-middle { + vertical-align: middle; +} +.media-bottom { + vertical-align: bottom; +} +.media-heading { + margin-top: 0; + margin-bottom: 5px; +} +.media-list { + padding-left: 0; + list-style: none; +} +.list-group { + margin-bottom: 20px; + padding-left: 0; +} +.list-group-item { + position: relative; + display: block; + padding: 10px 15px; + margin-bottom: -1px; + background-color: #282828; + border: 1px solid transparent; +} +.list-group-item:first-child { + border-top-right-radius: 0; + border-top-left-radius: 0; +} +.list-group-item:last-child { + margin-bottom: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} +a.list-group-item, +button.list-group-item { + color: #ebebeb; +} +a.list-group-item .list-group-item-heading, +button.list-group-item .list-group-item-heading { + color: #ebebeb; +} +a.list-group-item:hover, +button.list-group-item:hover, +a.list-group-item:focus, +button.list-group-item:focus { + text-decoration: none; + color: #ebebeb; + background-color: #333333; +} +button.list-group-item { + width: 100%; + text-align: left; +} +.list-group-item.disabled, +.list-group-item.disabled:hover, +.list-group-item.disabled:focus { + background-color: #ebebeb; + color: #4e5d6c; + cursor: not-allowed; +} +.list-group-item.disabled .list-group-item-heading, +.list-group-item.disabled:hover .list-group-item-heading, +.list-group-item.disabled:focus .list-group-item-heading { + color: inherit; +} +.list-group-item.disabled .list-group-item-text, +.list-group-item.disabled:hover .list-group-item-text, +.list-group-item.disabled:focus .list-group-item-text { + color: #4e5d6c; +} +.list-group-item.active, +.list-group-item.active:hover, +.list-group-item.active:focus { + z-index: 2; + color: #ffffff; + background-color: #df691a; + border-color: #df691a; +} +.list-group-item.active .list-group-item-heading, +.list-group-item.active:hover .list-group-item-heading, +.list-group-item.active:focus .list-group-item-heading, +.list-group-item.active .list-group-item-heading > small, +.list-group-item.active:hover .list-group-item-heading > small, +.list-group-item.active:focus .list-group-item-heading > small, +.list-group-item.active .list-group-item-heading > .small, +.list-group-item.active:hover .list-group-item-heading > .small, +.list-group-item.active:focus .list-group-item-heading > .small { + color: inherit; +} +.list-group-item.active .list-group-item-text, +.list-group-item.active:hover .list-group-item-text, +.list-group-item.active:focus .list-group-item-text { + color: #f9decc; +} +.list-group-item-success { + color: #ebebeb; + background-color: #5cb85c; +} +a.list-group-item-success, +button.list-group-item-success { + color: #ebebeb; +} +a.list-group-item-success .list-group-item-heading, +button.list-group-item-success .list-group-item-heading { + color: inherit; +} +a.list-group-item-success:hover, +button.list-group-item-success:hover, +a.list-group-item-success:focus, +button.list-group-item-success:focus { + color: #ebebeb; + background-color: #4cae4c; +} +a.list-group-item-success.active, +button.list-group-item-success.active, +a.list-group-item-success.active:hover, +button.list-group-item-success.active:hover, +a.list-group-item-success.active:focus, +button.list-group-item-success.active:focus { + color: #fff; + background-color: #ebebeb; + border-color: #ebebeb; +} +.list-group-item-info { + color: #ebebeb; + background-color: #5bc0de; +} +a.list-group-item-info, +button.list-group-item-info { + color: #ebebeb; +} +a.list-group-item-info .list-group-item-heading, +button.list-group-item-info .list-group-item-heading { + color: inherit; +} +a.list-group-item-info:hover, +button.list-group-item-info:hover, +a.list-group-item-info:focus, +button.list-group-item-info:focus { + color: #ebebeb; + background-color: #46b8da; +} +a.list-group-item-info.active, +button.list-group-item-info.active, +a.list-group-item-info.active:hover, +button.list-group-item-info.active:hover, +a.list-group-item-info.active:focus, +button.list-group-item-info.active:focus { + color: #fff; + background-color: #ebebeb; + border-color: #ebebeb; +} +.list-group-item-warning { + color: #ebebeb; + background-color: #f0ad4e; +} +a.list-group-item-warning, +button.list-group-item-warning { + color: #ebebeb; +} +a.list-group-item-warning .list-group-item-heading, +button.list-group-item-warning .list-group-item-heading { + color: inherit; +} +a.list-group-item-warning:hover, +button.list-group-item-warning:hover, +a.list-group-item-warning:focus, +button.list-group-item-warning:focus { + color: #ebebeb; + background-color: #eea236; +} +a.list-group-item-warning.active, +button.list-group-item-warning.active, +a.list-group-item-warning.active:hover, +button.list-group-item-warning.active:hover, +a.list-group-item-warning.active:focus, +button.list-group-item-warning.active:focus { + color: #fff; + background-color: #ebebeb; + border-color: #ebebeb; +} +.list-group-item-danger { + color: #ebebeb; + background-color: #d9534f; +} +a.list-group-item-danger, +button.list-group-item-danger { + color: #ebebeb; +} +a.list-group-item-danger .list-group-item-heading, +button.list-group-item-danger .list-group-item-heading { + color: inherit; +} +a.list-group-item-danger:hover, +button.list-group-item-danger:hover, +a.list-group-item-danger:focus, +button.list-group-item-danger:focus { + color: #ebebeb; + background-color: #d43f3a; +} +a.list-group-item-danger.active, +button.list-group-item-danger.active, +a.list-group-item-danger.active:hover, +button.list-group-item-danger.active:hover, +a.list-group-item-danger.active:focus, +button.list-group-item-danger.active:focus { + color: #fff; + background-color: #ebebeb; + border-color: #ebebeb; +} +.list-group-item-heading { + margin-top: 0; + margin-bottom: 5px; +} +.list-group-item-text { + margin-bottom: 0; + line-height: 1.3; +} +.panel { + margin-bottom: 21px; + background-color: #4e5d6c; + border: 1px solid transparent; + border-radius: 0; + -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05); + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05); +} +.panel-body { + padding: 15px; +} +.panel-heading { + padding: 10px 15px; + border-bottom: 1px solid transparent; + border-top-right-radius: -1; + border-top-left-radius: -1; +} +.panel-heading > .dropdown .dropdown-toggle { + color: inherit; +} +.panel-title { + margin-top: 0; + margin-bottom: 0; + font-size: 17px; + color: inherit; +} +.panel-title > a, +.panel-title > small, +.panel-title > .small, +.panel-title > small > a, +.panel-title > .small > a { + color: inherit; +} +.panel-footer { + padding: 10px 15px; + background-color: #485563; + border-top: 1px solid transparent; + border-bottom-right-radius: -1; + border-bottom-left-radius: -1; +} +.panel > .list-group, +.panel > .panel-collapse > .list-group { + margin-bottom: 0; +} +.panel > .list-group .list-group-item, +.panel > .panel-collapse > .list-group .list-group-item { + border-width: 1px 0; + border-radius: 0; +} +.panel > .list-group:first-child .list-group-item:first-child, +.panel > .panel-collapse > .list-group:first-child .list-group-item:first-child { + border-top: 0; + border-top-right-radius: -1; + border-top-left-radius: -1; +} +.panel > .list-group:last-child .list-group-item:last-child, +.panel > .panel-collapse > .list-group:last-child .list-group-item:last-child { + border-bottom: 0; + border-bottom-right-radius: -1; + border-bottom-left-radius: -1; +} +.panel > .panel-heading + .panel-collapse > .list-group .list-group-item:first-child { + border-top-right-radius: 0; + border-top-left-radius: 0; +} +.panel-heading + .list-group .list-group-item:first-child { + border-top-width: 0; +} +.list-group + .panel-footer { + border-top-width: 0; +} +.panel > .table, +.panel > .table-responsive > .table, +.panel > .panel-collapse > .table { + margin-bottom: 0; +} +.panel > .table caption, +.panel > .table-responsive > .table caption, +.panel > .panel-collapse > .table caption { + padding-left: 15px; + padding-right: 15px; +} +.panel > .table:first-child, +.panel > .table-responsive:first-child > .table:first-child { + border-top-right-radius: -1; + border-top-left-radius: -1; +} +.panel > .table:first-child > thead:first-child > tr:first-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child, +.panel > .table:first-child > tbody:first-child > tr:first-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child { + border-top-left-radius: -1; + border-top-right-radius: -1; +} +.panel > .table:first-child > thead:first-child > tr:first-child td:first-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:first-child, +.panel > .table:first-child > tbody:first-child > tr:first-child td:first-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:first-child, +.panel > .table:first-child > thead:first-child > tr:first-child th:first-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:first-child, +.panel > .table:first-child > tbody:first-child > tr:first-child th:first-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:first-child { + border-top-left-radius: -1; +} +.panel > .table:first-child > thead:first-child > tr:first-child td:last-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:last-child, +.panel > .table:first-child > tbody:first-child > tr:first-child td:last-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:last-child, +.panel > .table:first-child > thead:first-child > tr:first-child th:last-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:last-child, +.panel > .table:first-child > tbody:first-child > tr:first-child th:last-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:last-child { + border-top-right-radius: -1; +} +.panel > .table:last-child, +.panel > .table-responsive:last-child > .table:last-child { + border-bottom-right-radius: -1; + border-bottom-left-radius: -1; +} +.panel > .table:last-child > tbody:last-child > tr:last-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child { + border-bottom-left-radius: -1; + border-bottom-right-radius: -1; +} +.panel > .table:last-child > tbody:last-child > tr:last-child td:first-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:first-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child td:first-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:first-child, +.panel > .table:last-child > tbody:last-child > tr:last-child th:first-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:first-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child th:first-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:first-child { + border-bottom-left-radius: -1; +} +.panel > .table:last-child > tbody:last-child > tr:last-child td:last-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:last-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child td:last-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:last-child, +.panel > .table:last-child > tbody:last-child > tr:last-child th:last-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:last-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child th:last-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:last-child { + border-bottom-right-radius: -1; +} +.panel > .panel-body + .table, +.panel > .panel-body + .table-responsive, +.panel > .table + .panel-body, +.panel > .table-responsive + .panel-body { + border-top: 1px solid #4e5d6c; +} +.panel > .table > tbody:first-child > tr:first-child th, +.panel > .table > tbody:first-child > tr:first-child td { + border-top: 0; +} +.panel > .table-bordered, +.panel > .table-responsive > .table-bordered { + border: 0; +} +.panel > .table-bordered > thead > tr > th:first-child, +.panel > .table-responsive > .table-bordered > thead > tr > th:first-child, +.panel > .table-bordered > tbody > tr > th:first-child, +.panel > .table-responsive > .table-bordered > tbody > tr > th:first-child, +.panel > .table-bordered > tfoot > tr > th:first-child, +.panel > .table-responsive > .table-bordered > tfoot > tr > th:first-child, +.panel > .table-bordered > thead > tr > td:first-child, +.panel > .table-responsive > .table-bordered > thead > tr > td:first-child, +.panel > .table-bordered > tbody > tr > td:first-child, +.panel > .table-responsive > .table-bordered > tbody > tr > td:first-child, +.panel > .table-bordered > tfoot > tr > td:first-child, +.panel > .table-responsive > .table-bordered > tfoot > tr > td:first-child { + border-left: 0; +} +.panel > .table-bordered > thead > tr > th:last-child, +.panel > .table-responsive > .table-bordered > thead > tr > th:last-child, +.panel > .table-bordered > tbody > tr > th:last-child, +.panel > .table-responsive > .table-bordered > tbody > tr > th:last-child, +.panel > .table-bordered > tfoot > tr > th:last-child, +.panel > .table-responsive > .table-bordered > tfoot > tr > th:last-child, +.panel > .table-bordered > thead > tr > td:last-child, +.panel > .table-responsive > .table-bordered > thead > tr > td:last-child, +.panel > .table-bordered > tbody > tr > td:last-child, +.panel > .table-responsive > .table-bordered > tbody > tr > td:last-child, +.panel > .table-bordered > tfoot > tr > td:last-child, +.panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child { + border-right: 0; +} +.panel > .table-bordered > thead > tr:first-child > td, +.panel > .table-responsive > .table-bordered > thead > tr:first-child > td, +.panel > .table-bordered > tbody > tr:first-child > td, +.panel > .table-responsive > .table-bordered > tbody > tr:first-child > td, +.panel > .table-bordered > thead > tr:first-child > th, +.panel > .table-responsive > .table-bordered > thead > tr:first-child > th, +.panel > .table-bordered > tbody > tr:first-child > th, +.panel > .table-responsive > .table-bordered > tbody > tr:first-child > th { + border-bottom: 0; +} +.panel > .table-bordered > tbody > tr:last-child > td, +.panel > .table-responsive > .table-bordered > tbody > tr:last-child > td, +.panel > .table-bordered > tfoot > tr:last-child > td, +.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > td, +.panel > .table-bordered > tbody > tr:last-child > th, +.panel > .table-responsive > .table-bordered > tbody > tr:last-child > th, +.panel > .table-bordered > tfoot > tr:last-child > th, +.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > th { + border-bottom: 0; +} +.panel > .table-responsive { + border: 0; + margin-bottom: 0; +} +.panel-group { + margin-bottom: 21px; +} +.panel-group .panel { + margin-bottom: 0; + border-radius: 0; +} +.panel-group .panel + .panel { + margin-top: 5px; +} +.panel-group .panel-heading { + border-bottom: 0; +} +.panel-group .panel-heading + .panel-collapse > .panel-body, +.panel-group .panel-heading + .panel-collapse > .list-group { + border-top: 1px solid transparent; +} +.panel-group .panel-footer { + border-top: 0; +} +.panel-group .panel-footer + .panel-collapse .panel-body { + border-bottom: 1px solid transparent; +} +.panel-default { + border-color: transparent; +} +.panel-default > .panel-heading { + color: #333333; + background-color: #f5f5f5; + border-color: transparent; +} +.panel-default > .panel-heading + .panel-collapse > .panel-body { + border-top-color: transparent; +} +.panel-default > .panel-heading .badge { + color: #f5f5f5; + background-color: #333333; +} +.panel-default > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: transparent; +} +.panel-primary { + border-color: transparent; +} +.panel-primary > .panel-heading { + color: #ffffff; + background-color: #df691a; + border-color: transparent; +} +.panel-primary > .panel-heading + .panel-collapse > .panel-body { + border-top-color: transparent; +} +.panel-primary > .panel-heading .badge { + color: #df691a; + background-color: #ffffff; +} +.panel-primary > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: transparent; +} +.panel-success { + border-color: transparent; +} +.panel-success > .panel-heading { + color: #ebebeb; + background-color: #5cb85c; + border-color: transparent; +} +.panel-success > .panel-heading + .panel-collapse > .panel-body { + border-top-color: transparent; +} +.panel-success > .panel-heading .badge { + color: #5cb85c; + background-color: #ebebeb; +} +.panel-success > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: transparent; +} +.panel-info { + border-color: transparent; +} +.panel-info > .panel-heading { + color: #ebebeb; + background-color: #5bc0de; + border-color: transparent; +} +.panel-info > .panel-heading + .panel-collapse > .panel-body { + border-top-color: transparent; +} +.panel-info > .panel-heading .badge { + color: #5bc0de; + background-color: #ebebeb; +} +.panel-info > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: transparent; +} +.panel-warning { + border-color: transparent; +} +.panel-warning > .panel-heading { + color: #ebebeb; + background-color: #f0ad4e; + border-color: transparent; +} +.panel-warning > .panel-heading + .panel-collapse > .panel-body { + border-top-color: transparent; +} +.panel-warning > .panel-heading .badge { + color: #f0ad4e; + background-color: #ebebeb; +} +.panel-warning > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: transparent; +} +.panel-danger { + border-color: transparent; +} +.panel-danger > .panel-heading { + color: #ebebeb; + background-color: #d9534f; + border-color: transparent; +} +.panel-danger > .panel-heading + .panel-collapse > .panel-body { + border-top-color: transparent; +} +.panel-danger > .panel-heading .badge { + color: #d9534f; + background-color: #ebebeb; +} +.panel-danger > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: transparent; +} +.embed-responsive { + position: relative; + display: block; + height: 0; + padding: 0; + overflow: hidden; +} +.embed-responsive .embed-responsive-item, +.embed-responsive iframe, +.embed-responsive embed, +.embed-responsive object, +.embed-responsive video { + position: absolute; + top: 0; + left: 0; + bottom: 0; + height: 100%; + width: 100%; + border: 0; +} +.embed-responsive-16by9 { + padding-bottom: 56.25%; +} +.embed-responsive-4by3 { + padding-bottom: 75%; +} +.well { + min-height: 20px; + padding: 19px; + margin-bottom: 20px; + background-color: #4e5d6c; + border: 1px solid transparent; + border-radius: 0; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); +} +.well blockquote { + border-color: #ddd; + border-color: rgba(0, 0, 0, 0.15); +} +.well-lg { + padding: 24px; + border-radius: 0; +} +.well-sm { + padding: 9px; + border-radius: 0; +} +.close { + float: right; + font-size: 22.5px; + font-weight: bold; + line-height: 1; + color: #ebebeb; + text-shadow: none; + opacity: 0.2; + filter: alpha(opacity=20); +} +.close:hover, +.close:focus { + color: #ebebeb; + text-decoration: none; + cursor: pointer; + opacity: 0.5; + filter: alpha(opacity=50); +} +button.close { + padding: 0; + cursor: pointer; + background: transparent; + border: 0; + -webkit-appearance: none; +} +.modal-open { + overflow: hidden; +} +.modal { + display: none; + overflow: hidden; + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1050; + -webkit-overflow-scrolling: touch; + outline: 0; +} +.modal.fade .modal-dialog { + -webkit-transform: translate(0, -25%); + -ms-transform: translate(0, -25%); + -o-transform: translate(0, -25%); + transform: translate(0, -25%); + -webkit-transition: -webkit-transform 0.3s ease-out; + -o-transition: -o-transform 0.3s ease-out; + transition: transform 0.3s ease-out; +} +.modal.in .modal-dialog { + -webkit-transform: translate(0, 0); + -ms-transform: translate(0, 0); + -o-transform: translate(0, 0); + transform: translate(0, 0); +} +.modal-open .modal { + overflow-x: hidden; + overflow-y: auto; +} +.modal-dialog { + position: relative; + width: auto; + margin: 10px; +} +.modal-content { + position: relative; + background-color: #282828; + border: 1px solid transparent; + border-radius: 0; + -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5); + box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5); + -webkit-background-clip: padding-box; + background-clip: padding-box; + outline: 0; +} +.modal-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1040; + background-color: #000000; +} +.modal-backdrop.fade { + opacity: 0; + filter: alpha(opacity=0); +} +.modal-backdrop.in { + opacity: 0.5; + filter: alpha(opacity=50); +} +.modal-header { + padding: 15px; + border-bottom: 1px solid #2b3e50; +} +.modal-header .close { + margin-top: -2px; +} +.modal-title { + margin: 0; + line-height: 1.42857143; +} +.modal-body { + position: relative; + padding: 20px; +} +.modal-footer { + padding: 20px; + text-align: right; + border-top: 1px solid #2b3e50; +} +.modal-footer .btn + .btn { + margin-left: 5px; + margin-bottom: 0; +} +.modal-footer .btn-group .btn + .btn { + margin-left: -1px; +} +.modal-footer .btn-block + .btn-block { + margin-left: 0; +} +.modal-scrollbar-measure { + position: absolute; + top: -9999px; + width: 50px; + height: 50px; + overflow: scroll; +} +@media (min-width: 768px) { + .modal-dialog { + width: 600px; + margin: 30px auto; + } + .modal-content { + -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); + } + .modal-sm { + width: 300px; + } +} +@media (min-width: 992px) { + .modal-lg { + width: 900px; + } +} +.tooltip { + position: absolute; + z-index: 1070; + display: block; + font-family: "Lato", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-style: normal; + font-weight: normal; + letter-spacing: normal; + line-break: auto; + line-height: 1.42857143; + text-align: left; + text-align: start; + text-decoration: none; + text-shadow: none; + text-transform: none; + white-space: normal; + word-break: normal; + word-spacing: normal; + word-wrap: normal; + font-size: 12px; + opacity: 0; + filter: alpha(opacity=0); +} +.tooltip.in { + opacity: 0.9; + filter: alpha(opacity=90); +} +.tooltip.top { + margin-top: -3px; + padding: 5px 0; +} +.tooltip.right { + margin-left: 3px; + padding: 0 5px; +} +.tooltip.bottom { + margin-top: 3px; + padding: 5px 0; +} +.tooltip.left { + margin-left: -3px; + padding: 0 5px; +} +.tooltip-inner { + max-width: 200px; + padding: 3px 8px; + color: #ffffff; + text-align: center; + background-color: #000000; + border-radius: 0; +} +.tooltip-arrow { + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} +.tooltip.top .tooltip-arrow { + bottom: 0; + left: 50%; + margin-left: -5px; + border-width: 5px 5px 0; + border-top-color: #000000; +} +.tooltip.top-left .tooltip-arrow { + bottom: 0; + right: 5px; + margin-bottom: -5px; + border-width: 5px 5px 0; + border-top-color: #000000; +} +.tooltip.top-right .tooltip-arrow { + bottom: 0; + left: 5px; + margin-bottom: -5px; + border-width: 5px 5px 0; + border-top-color: #000000; +} +.tooltip.right .tooltip-arrow { + top: 50%; + left: 0; + margin-top: -5px; + border-width: 5px 5px 5px 0; + border-right-color: #000000; +} +.tooltip.left .tooltip-arrow { + top: 50%; + right: 0; + margin-top: -5px; + border-width: 5px 0 5px 5px; + border-left-color: #000000; +} +.tooltip.bottom .tooltip-arrow { + top: 0; + left: 50%; + margin-left: -5px; + border-width: 0 5px 5px; + border-bottom-color: #000000; +} +.tooltip.bottom-left .tooltip-arrow { + top: 0; + right: 5px; + margin-top: -5px; + border-width: 0 5px 5px; + border-bottom-color: #000000; +} +.tooltip.bottom-right .tooltip-arrow { + top: 0; + left: 5px; + margin-top: -5px; + border-width: 0 5px 5px; + border-bottom-color: #000000; +} +.popover { + position: absolute; + top: 0; + left: 0; + z-index: 1060; + display: none; + max-width: 276px; + padding: 1px; + font-family: "Lato", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-style: normal; + font-weight: normal; + letter-spacing: normal; + line-break: auto; + line-height: 1.42857143; + text-align: left; + text-align: start; + text-decoration: none; + text-shadow: none; + text-transform: none; + white-space: normal; + word-break: normal; + word-spacing: normal; + word-wrap: normal; + font-size: 15px; + background-color: #4e5d6c; + -webkit-background-clip: padding-box; + background-clip: padding-box; + border: 1px solid transparent; + border-radius: 0; + -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); +} +.popover.top { + margin-top: -10px; +} +.popover.right { + margin-left: 10px; +} +.popover.bottom { + margin-top: 10px; +} +.popover.left { + margin-left: -10px; +} +.popover-title { + margin: 0; + padding: 8px 14px; + font-size: 15px; + background-color: #485563; + border-bottom: 1px solid #3d4954; + border-radius: -1 -1 0 0; +} +.popover-content { + padding: 9px 14px; +} +.popover > .arrow, +.popover > .arrow:after { + position: absolute; + display: block; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} +.popover > .arrow { + border-width: 11px; +} +.popover > .arrow:after { + border-width: 10px; + content: ""; +} +.popover.top > .arrow { + left: 50%; + margin-left: -11px; + border-bottom-width: 0; + border-top-color: transparent; + bottom: -11px; +} +.popover.top > .arrow:after { + content: " "; + bottom: 1px; + margin-left: -10px; + border-bottom-width: 0; + border-top-color: #4e5d6c; +} +.popover.right > .arrow { + top: 50%; + left: -11px; + margin-top: -11px; + border-left-width: 0; + border-right-color: transparent; +} +.popover.right > .arrow:after { + content: " "; + left: 1px; + bottom: -10px; + border-left-width: 0; + border-right-color: #4e5d6c; +} +.popover.bottom > .arrow { + left: 50%; + margin-left: -11px; + border-top-width: 0; + border-bottom-color: transparent; + top: -11px; +} +.popover.bottom > .arrow:after { + content: " "; + top: 1px; + margin-left: -10px; + border-top-width: 0; + border-bottom-color: #4e5d6c; +} +.popover.left > .arrow { + top: 50%; + right: -11px; + margin-top: -11px; + border-right-width: 0; + border-left-color: transparent; +} +.popover.left > .arrow:after { + content: " "; + right: 1px; + border-right-width: 0; + border-left-color: #4e5d6c; + bottom: -10px; +} +.carousel { + position: relative; +} +.carousel-inner { + position: relative; + overflow: hidden; + width: 100%; +} +.carousel-inner > .item { + display: none; + position: relative; + -webkit-transition: 0.6s ease-in-out left; + -o-transition: 0.6s ease-in-out left; + transition: 0.6s ease-in-out left; +} +.carousel-inner > .item > img, +.carousel-inner > .item > a > img { + line-height: 1; +} +@media all and (transform-3d), (-webkit-transform-3d) { + .carousel-inner > .item { + -webkit-transition: -webkit-transform 0.6s ease-in-out; + -o-transition: -o-transform 0.6s ease-in-out; + transition: transform 0.6s ease-in-out; + -webkit-backface-visibility: hidden; + backface-visibility: hidden; + -webkit-perspective: 1000px; + perspective: 1000px; + } + .carousel-inner > .item.next, + .carousel-inner > .item.active.right { + -webkit-transform: translate3d(100%, 0, 0); + transform: translate3d(100%, 0, 0); + left: 0; + } + .carousel-inner > .item.prev, + .carousel-inner > .item.active.left { + -webkit-transform: translate3d(-100%, 0, 0); + transform: translate3d(-100%, 0, 0); + left: 0; + } + .carousel-inner > .item.next.left, + .carousel-inner > .item.prev.right, + .carousel-inner > .item.active { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + left: 0; + } +} +.carousel-inner > .active, +.carousel-inner > .next, +.carousel-inner > .prev { + display: block; +} +.carousel-inner > .active { + left: 0; +} +.carousel-inner > .next, +.carousel-inner > .prev { + position: absolute; + top: 0; + width: 100%; +} +.carousel-inner > .next { + left: 100%; +} +.carousel-inner > .prev { + left: -100%; +} +.carousel-inner > .next.left, +.carousel-inner > .prev.right { + left: 0; +} +.carousel-inner > .active.left { + left: -100%; +} +.carousel-inner > .active.right { + left: 100%; +} +.carousel-control { + position: absolute; + top: 0; + left: 0; + bottom: 0; + width: 15%; + opacity: 0.5; + filter: alpha(opacity=50); + font-size: 20px; + color: #ffffff; + text-align: center; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6); + background-color: rgba(0, 0, 0, 0); +} +.carousel-control.left { + background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%); + background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%); + background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, 0.5)), to(rgba(0, 0, 0, 0.0001))); + background-image: linear-gradient(to right, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1); +} +.carousel-control.right { + left: auto; + right: 0; + background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%); + background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%); + background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, 0.0001)), to(rgba(0, 0, 0, 0.5))); + background-image: linear-gradient(to right, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1); +} +.carousel-control:hover, +.carousel-control:focus { + outline: 0; + color: #ffffff; + text-decoration: none; + opacity: 0.9; + filter: alpha(opacity=90); +} +.carousel-control .icon-prev, +.carousel-control .icon-next, +.carousel-control .glyphicon-chevron-left, +.carousel-control .glyphicon-chevron-right { + position: absolute; + top: 50%; + margin-top: -10px; + z-index: 5; + display: inline-block; +} +.carousel-control .icon-prev, +.carousel-control .glyphicon-chevron-left { + left: 50%; + margin-left: -10px; +} +.carousel-control .icon-next, +.carousel-control .glyphicon-chevron-right { + right: 50%; + margin-right: -10px; +} +.carousel-control .icon-prev, +.carousel-control .icon-next { + width: 20px; + height: 20px; + line-height: 1; + font-family: serif; +} +.carousel-control .icon-prev:before { + content: '\2039'; +} +.carousel-control .icon-next:before { + content: '\203a'; +} +.carousel-indicators { + position: absolute; + bottom: 10px; + left: 50%; + z-index: 15; + width: 60%; + margin-left: -30%; + padding-left: 0; + list-style: none; + text-align: center; +} +.carousel-indicators li { + display: inline-block; + width: 10px; + height: 10px; + margin: 1px; + text-indent: -999px; + border: 1px solid #ffffff; + border-radius: 10px; + cursor: pointer; + background-color: #000 \9; + background-color: rgba(0, 0, 0, 0); +} +.carousel-indicators .active { + margin: 0; + width: 12px; + height: 12px; + background-color: #ffffff; +} +.carousel-caption { + position: absolute; + left: 15%; + right: 15%; + bottom: 20px; + z-index: 10; + padding-top: 20px; + padding-bottom: 20px; + color: #ffffff; + text-align: center; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6); +} +.carousel-caption .btn { + text-shadow: none; +} +@media screen and (min-width: 768px) { + .carousel-control .glyphicon-chevron-left, + .carousel-control .glyphicon-chevron-right, + .carousel-control .icon-prev, + .carousel-control .icon-next { + width: 30px; + height: 30px; + margin-top: -10px; + font-size: 30px; + } + .carousel-control .glyphicon-chevron-left, + .carousel-control .icon-prev { + margin-left: -10px; + } + .carousel-control .glyphicon-chevron-right, + .carousel-control .icon-next { + margin-right: -10px; + } + .carousel-caption { + left: 20%; + right: 20%; + padding-bottom: 30px; + } + .carousel-indicators { + bottom: 20px; + } +} +.clearfix:before, +.clearfix:after, +.dl-horizontal dd:before, +.dl-horizontal dd:after, +.container:before, +.container:after, +.container-fluid:before, +.container-fluid:after, +.row:before, +.row:after, +.form-horizontal .form-group:before, +.form-horizontal .form-group:after, +.btn-toolbar:before, +.btn-toolbar:after, +.btn-group-vertical > .btn-group:before, +.btn-group-vertical > .btn-group:after, +.nav:before, +.nav:after, +.navbar:before, +.navbar:after, +.navbar-header:before, +.navbar-header:after, +.navbar-collapse:before, +.navbar-collapse:after, +.pager:before, +.pager:after, +.panel-body:before, +.panel-body:after, +.modal-header:before, +.modal-header:after, +.modal-footer:before, +.modal-footer:after { + content: " "; + display: table; +} +.clearfix:after, +.dl-horizontal dd:after, +.container:after, +.container-fluid:after, +.row:after, +.form-horizontal .form-group:after, +.btn-toolbar:after, +.btn-group-vertical > .btn-group:after, +.nav:after, +.navbar:after, +.navbar-header:after, +.navbar-collapse:after, +.pager:after, +.panel-body:after, +.modal-header:after, +.modal-footer:after { + clear: both; +} +.center-block { + display: block; + margin-left: auto; + margin-right: auto; +} +.pull-right { + float: right !important; +} +.pull-left { + float: left !important; +} +.hide { + display: none !important; +} +.show { + display: block !important; +} +.invisible { + visibility: hidden; +} +.text-hide { + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; +} +.hidden { + display: none !important; +} +.affix { + position: fixed; +} +@-ms-viewport { + width: device-width; +} +.visible-xs, +.visible-sm, +.visible-md, +.visible-lg { + display: none !important; +} +.visible-xs-block, +.visible-xs-inline, +.visible-xs-inline-block, +.visible-sm-block, +.visible-sm-inline, +.visible-sm-inline-block, +.visible-md-block, +.visible-md-inline, +.visible-md-inline-block, +.visible-lg-block, +.visible-lg-inline, +.visible-lg-inline-block { + display: none !important; +} +@media (max-width: 767px) { + .visible-xs { + display: block !important; + } + table.visible-xs { + display: table !important; + } + tr.visible-xs { + display: table-row !important; + } + th.visible-xs, + td.visible-xs { + display: table-cell !important; + } +} +@media (max-width: 767px) { + .visible-xs-block { + display: block !important; + } +} +@media (max-width: 767px) { + .visible-xs-inline { + display: inline !important; + } +} +@media (max-width: 767px) { + .visible-xs-inline-block { + display: inline-block !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .visible-sm { + display: block !important; + } + table.visible-sm { + display: table !important; + } + tr.visible-sm { + display: table-row !important; + } + th.visible-sm, + td.visible-sm { + display: table-cell !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .visible-sm-block { + display: block !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .visible-sm-inline { + display: inline !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .visible-sm-inline-block { + display: inline-block !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .visible-md { + display: block !important; + } + table.visible-md { + display: table !important; + } + tr.visible-md { + display: table-row !important; + } + th.visible-md, + td.visible-md { + display: table-cell !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .visible-md-block { + display: block !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .visible-md-inline { + display: inline !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .visible-md-inline-block { + display: inline-block !important; + } +} +@media (min-width: 1200px) { + .visible-lg { + display: block !important; + } + table.visible-lg { + display: table !important; + } + tr.visible-lg { + display: table-row !important; + } + th.visible-lg, + td.visible-lg { + display: table-cell !important; + } +} +@media (min-width: 1200px) { + .visible-lg-block { + display: block !important; + } +} +@media (min-width: 1200px) { + .visible-lg-inline { + display: inline !important; + } +} +@media (min-width: 1200px) { + .visible-lg-inline-block { + display: inline-block !important; + } +} +@media (max-width: 767px) { + .hidden-xs { + display: none !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .hidden-sm { + display: none !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .hidden-md { + display: none !important; + } +} +@media (min-width: 1200px) { + .hidden-lg { + display: none !important; + } +} +.visible-print { + display: none !important; +} +@media print { + .visible-print { + display: block !important; + } + table.visible-print { + display: table !important; + } + tr.visible-print { + display: table-row !important; + } + th.visible-print, + td.visible-print { + display: table-cell !important; + } +} +.visible-print-block { + display: none !important; +} +@media print { + .visible-print-block { + display: block !important; + } +} +.visible-print-inline { + display: none !important; +} +@media print { + .visible-print-inline { + display: inline !important; + } +} +.visible-print-inline-block { + display: none !important; +} +@media print { + .visible-print-inline-block { + display: inline-block !important; + } +} +@media print { + .hidden-print { + display: none !important; + } +} +.navbar { + -webkit-box-shadow: none; + box-shadow: none; + border: none; + font-size: 12px; +} +.navbar-default .badge { + background-color: #fff; + color: #4e5d6c; +} +.navbar-inverse .badge { + background-color: #fff; + color: #df691a; +} +.btn-default:hover { + background-color: #485563; +} +.btn-sm, +.btn-xs { + font-size: 12px; +} +.text-primary, +.text-primary:hover { + color: #df691a; +} +.text-success, +.text-success:hover { + color: #5cb85c; +} +.text-danger, +.text-danger:hover { + color: #d9534f; +} +.text-warning, +.text-warning:hover { + color: #f0ad4e; +} +.text-info, +.text-info:hover { + color: #5bc0de; +} +.page-header { + border-bottom-color: #4e5d6c; +} +.dropdown-menu { + border: none; + margin: 0; + -webkit-box-shadow: none; + box-shadow: none; +} +.dropdown-menu > li > a { + font-size: 12px; +} +.btn-group.open .dropdown-toggle { + -webkit-box-shadow: none; + box-shadow: none; +} +.dropdown-header { + font-size: 12px; +} +table, +.table { + font-size: 12px; +} +table a:not(.btn), +.table a:not(.btn) { + color: #fff; + text-decoration: underline; +} +table .dropdown-menu a, +.table .dropdown-menu a { + text-decoration: none; +} +table .text-muted, +.table .text-muted { + color: #4e5d6c; +} +table > thead > tr > th, +.table > thead > tr > th, +table > tbody > tr > th, +.table > tbody > tr > th, +table > tfoot > tr > th, +.table > tfoot > tr > th, +table > thead > tr > td, +.table > thead > tr > td, +table > tbody > tr > td, +.table > tbody > tr > td, +table > tfoot > tr > td, +.table > tfoot > tr > td { + border-color: transparent; +} +input, +textarea { + color: #2b3e50; +} +label, +.radio label, +.checkbox label, +.help-block { + font-size: 12px; +} +.input-addon, +.input-group-addon { + color: #df691a; +} +.has-warning .help-block, +.has-warning .control-label, +.has-warning .radio, +.has-warning .checkbox, +.has-warning .radio-inline, +.has-warning .checkbox-inline, +.has-warning.radio label, +.has-warning.checkbox label, +.has-warning.radio-inline label, +.has-warning.checkbox-inline label, +.has-warning .form-control-feedback { + color: #f0ad4e; +} +.has-warning .input-group-addon { + border: none; +} +.has-error .help-block, +.has-error .control-label, +.has-error .radio, +.has-error .checkbox, +.has-error .radio-inline, +.has-error .checkbox-inline, +.has-error.radio label, +.has-error.checkbox label, +.has-error.radio-inline label, +.has-error.checkbox-inline label, +.has-error .form-control-feedback { + color: #d9534f; +} +.has-error .input-group-addon { + border: none; +} +.has-success .help-block, +.has-success .control-label, +.has-success .radio, +.has-success .checkbox, +.has-success .radio-inline, +.has-success .checkbox-inline, +.has-success.radio label, +.has-success.checkbox label, +.has-success.radio-inline label, +.has-success.checkbox-inline label, +.has-success .form-control-feedback { + color: #5cb85c; +} +.has-success .input-group-addon { + border: none; +} +.form-control:focus { + -webkit-box-shadow: none; + box-shadow: none; +} +.has-warning .form-control:focus, +.has-error .form-control:focus, +.has-success .form-control:focus { + -webkit-box-shadow: none; + box-shadow: none; +} +.nav .open > a, +.nav .open > a:hover, +.nav .open > a:focus { + border-color: transparent; +} +.nav-tabs > li > a { + color: #ebebeb; +} +.nav-pills > li > a { + color: #ebebeb; +} +.pager a { + color: #ebebeb; +} +.alert { + color: #fff; +} +.alert a, +.alert .alert-link { + color: #fff; +} +.close { + opacity: 0.4; +} +.close:hover, +.close:focus { + opacity: 1; +} +.well { + -webkit-box-shadow: none; + box-shadow: none; +} +a.list-group-item.active, +a.list-group-item.active:hover, +a.list-group-item.active:focus { + border: none; +} +a.list-group-item-success.active { + background-color: #5cb85c; +} +a.list-group-item-success.active:hover, +a.list-group-item-success.active:focus { + background-color: #4cae4c; +} +a.list-group-item-warning.active { + background-color: #f0ad4e; +} +a.list-group-item-warning.active:hover, +a.list-group-item-warning.active:focus { + background-color: #eea236; +} +a.list-group-item-danger.active { + background-color: #d9534f; +} +a.list-group-item-danger.active:hover, +a.list-group-item-danger.active:focus { + background-color: #d43f3a; +} +.panel { + border: none; +} +.panel-default > .panel-heading { + background-color: #485563; + color: #ebebeb; +} +.thumbnail { + background-color: #4e5d6c; + border: none; +} +.modal { + padding: 0; +} +.modal-header, +.modal-footer { + background-color: #282828; + border: none; + border-radius: 0; +} +.popover-title { + border: none; +} + diff --git a/PlexRequests.UI/Content/custom.min.css b/PlexRequests.UI/Content/custom.min.css index 6f7c7827f..7ba0e8d7b 100644 --- a/PlexRequests.UI/Content/custom.min.css +++ b/PlexRequests.UI/Content/custom.min.css @@ -1 +1 @@ -@media(min-width:768px){.row{position:relative;}.bottom-align-text{position:absolute;bottom:0;right:0;}}@media(max-width:48em){.home{padding-top:1rem;}}@media(min-width:48em){.home{padding-top:4rem;}}.btn{border-radius:.25rem !important;}.multiSelect{background-color:#4e5d6c;}.form-control-custom{background-color:#333333 !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;}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{background:#df691a;}.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:#ffffff !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:#df691a;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:#71DD98;}.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:#A672DE;}.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:13px;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;} +@media(min-width:768px){.row{position:relative;}.bottom-align-text{position:absolute;bottom:0;right:0;}}@media(max-width:48em){.home{padding-top:1rem;}}@media(min-width:48em){.home{padding-top:4rem;}}.btn{border-radius:.25rem !important;}.multiSelect{background-color:#4e5d6c;}.form-control-custom{background-color:#333333 !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;}.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:#df691a;}.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:#009486;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:#df691a;}.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:35B691;}.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:13px;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:#ffa400;text-align:center;font-size:15px;}.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;}.input-group-sm{padding-top:2px;padding-bottom:2px;}.tab-pane .form-horizontal .form-group{margin-right:15px;margin-left:15px;}