User management, migration and newsletter

pull/618/head
Jamie.Rees 8 years ago
parent 11ecbf04f6
commit 42c437905e

@ -25,39 +25,76 @@
// ************************************************************************/ // ************************************************************************/
#endregion #endregion
using System;
using System.Data; using System.Data;
using PlexRequests.Core.SettingModels; using PlexRequests.Core.SettingModels;
using PlexRequests.Store;
using Quartz;
namespace PlexRequests.Core.Migration.Migrations namespace PlexRequests.Core.Migration.Migrations
{ {
[Migration(1950, "v1.9.5.0")] [Migration(1950, "v1.9.5.0")]
public class Version195 : BaseMigration, IMigration public class Version195 : BaseMigration, IMigration
{ {
public Version195(ISettingsService<PlexRequestSettings> plexRequestSettings, ISettingsService<NewletterSettings> news) public Version195(ISettingsService<PlexRequestSettings> plexRequestSettings, ISettingsService<NewletterSettings> news, ISettingsService<ScheduledJobsSettings> jobs)
{ {
PlexRequestSettings = plexRequestSettings; PlexRequestSettings = plexRequestSettings;
NewsletterSettings = news; NewsletterSettings = news;
Jobs = jobs;
} }
public int Version => 1950; public int Version => 1950;
private ISettingsService<PlexRequestSettings> PlexRequestSettings { get; } private ISettingsService<PlexRequestSettings> PlexRequestSettings { get; }
private ISettingsService<NewletterSettings> NewsletterSettings { get; } private ISettingsService<NewletterSettings> NewsletterSettings { get; }
private ISettingsService<ScheduledJobsSettings> Jobs { get; }
public void Start(IDbConnection con) public void Start(IDbConnection con)
{
UpdateApplicationSettings();
UpdateDb(con);
UpdateSchema(con, Version);
}
private void UpdateDb(IDbConnection con)
{
}
private void UpdateApplicationSettings()
{ {
var plex = PlexRequestSettings.GetSettings(); var plex = PlexRequestSettings.GetSettings();
var jobSettings = Jobs.GetSettings();
var newsLetter = NewsletterSettings.GetSettings(); var newsLetter = NewsletterSettings.GetSettings();
newsLetter.SendToPlexUsers = true;
UpdateScheduledSettings(jobSettings);
if (plex.SendRecentlyAddedEmail) if (plex.SendRecentlyAddedEmail)
{ {
newsLetter.SendRecentlyAddedEmail = plex.SendRecentlyAddedEmail; newsLetter.SendRecentlyAddedEmail = plex.SendRecentlyAddedEmail;
plex.SendRecentlyAddedEmail = false; plex.SendRecentlyAddedEmail = false;
PlexRequestSettings.SaveSettings(plex); PlexRequestSettings.SaveSettings(plex);
NewsletterSettings.SaveSettings(newsLetter);
} }
UpdateSchema(con, Version);
NewsletterSettings.SaveSettings(newsLetter);
Jobs.SaveSettings(jobSettings);
}
private void UpdateScheduledSettings(ScheduledJobsSettings settings)
{
settings.PlexAvailabilityChecker = 60;
settings.SickRageCacher = 60;
settings.SonarrCacher = 60;
settings.CouchPotatoCacher = 60;
settings.StoreBackup = 24;
settings.StoreCleanup = 24;
settings.UserRequestLimitResetter = 12;
settings.PlexEpisodeCacher = 12;
var cron = (Quartz.Impl.Triggers.CronTriggerImpl)CronScheduleBuilder.WeeklyOnDayAndHourAndMinute(DayOfWeek.Friday, 7, 0).Build();
settings.RecentlyAddedCron = cron.CronExpressionString; // Weekly CRON at 7 am on Mondays
} }
} }
} }

@ -31,12 +31,24 @@
<WarningLevel>4</WarningLevel> <WarningLevel>4</WarningLevel>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Reference Include="Common.Logging, Version=3.0.0.0, Culture=neutral, PublicKeyToken=af08829b84f0328e, processorArchitecture=MSIL">
<HintPath>..\packages\Common.Logging.3.0.0\lib\net40\Common.Logging.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Common.Logging.Core, Version=3.0.0.0, Culture=neutral, PublicKeyToken=af08829b84f0328e, processorArchitecture=MSIL">
<HintPath>..\packages\Common.Logging.Core.3.0.0\lib\net40\Common.Logging.Core.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Mono.Data.Sqlite"> <Reference Include="Mono.Data.Sqlite">
<HintPath>..\Assemblies\Mono.Data.Sqlite.dll</HintPath> <HintPath>..\Assemblies\Mono.Data.Sqlite.dll</HintPath>
</Reference> </Reference>
<Reference Include="Ninject"> <Reference Include="Ninject">
<HintPath>..\packages\Ninject.3.2.0.0\lib\net45-full\Ninject.dll</HintPath> <HintPath>..\packages\Ninject.3.2.0.0\lib\net45-full\Ninject.dll</HintPath>
</Reference> </Reference>
<Reference Include="Quartz, Version=2.3.3.0, Culture=neutral, PublicKeyToken=f6b8c98a402cc8a4, processorArchitecture=MSIL">
<HintPath>..\packages\Quartz.2.3.3\lib\net40\Quartz.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.Core" /> <Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" /> <Reference Include="System.Xml.Linq" />
@ -66,6 +78,13 @@
<Name>PlexRequests.Store</Name> <Name>PlexRequests.Store</Name>
</ProjectReference> </ProjectReference>
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Include="app.config" />
<None Include="job_scheduling_data_2_0.xsd">
<SubType>Designer</SubType>
</None>
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it. <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets. Other similar extension points exist, see Microsoft.Common.targets.

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-9.0.0.0" newVersion="9.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

@ -0,0 +1,361 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns="http://quartznet.sourceforge.net/JobSchedulingData"
targetNamespace="http://quartznet.sourceforge.net/JobSchedulingData"
elementFormDefault="qualified"
version="2.0">
<xs:element name="job-scheduling-data">
<xs:annotation>
<xs:documentation>Root level node</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:sequence maxOccurs="unbounded">
<xs:element name="pre-processing-commands" type="pre-processing-commandsType" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>Commands to be executed before scheduling the jobs and triggers in this file.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="processing-directives" type="processing-directivesType" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>Directives to be followed while scheduling the jobs and triggers in this file.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="schedule" minOccurs="0" maxOccurs="unbounded">
<xs:complexType>
<xs:sequence maxOccurs="unbounded">
<xs:element name="job" type="job-detailType" minOccurs="0" maxOccurs="unbounded" />
<xs:element name="trigger" type="triggerType" minOccurs="0" maxOccurs="unbounded" />
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:attribute name="version" type="xs:string">
<xs:annotation>
<xs:documentation>Version of the XML Schema instance</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
<xs:complexType name="pre-processing-commandsType">
<xs:sequence maxOccurs="unbounded">
<xs:element name="delete-jobs-in-group" type="xs:string" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>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.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="delete-triggers-in-group" type="xs:string" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>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.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="delete-job" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>Delete the identified job if it exists (will also result in deleting all triggers related to it).</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:sequence>
<xs:element name="name" type="xs:string" />
<xs:element name="group" type="xs:string" minOccurs="0" />
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="delete-trigger" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>Delete the identified trigger if it exists (will also result in deletion of related jobs that are non-durable).</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:sequence>
<xs:element name="name" type="xs:string" />
<xs:element name="group" type="xs:string" minOccurs="0" />
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
<xs:complexType name="processing-directivesType">
<xs:sequence>
<xs:element name="overwrite-existing-data" type="xs:boolean" minOccurs="0" default="true">
<xs:annotation>
<xs:documentation>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.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="ignore-duplicates" type="xs:boolean" minOccurs="0" default="false">
<xs:annotation>
<xs:documentation>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.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="schedule-trigger-relative-to-replaced-trigger" type="xs:boolean" minOccurs="0" default="false">
<xs:annotation>
<xs:documentation>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.</xs:documentation>
</xs:annotation>
</xs:element>
</xs:sequence>
</xs:complexType>
<xs:complexType name="job-detailType">
<xs:annotation>
<xs:documentation>Define a JobDetail</xs:documentation>
</xs:annotation>
<xs:sequence>
<xs:element name="name" type="xs:string" />
<xs:element name="group" type="xs:string" minOccurs="0" />
<xs:element name="description" type="xs:string" minOccurs="0" />
<xs:element name="job-type" type="xs:string" />
<xs:sequence minOccurs="0">
<xs:element name="durable" type="xs:boolean" />
<xs:element name="recover" type="xs:boolean" />
</xs:sequence>
<xs:element name="job-data-map" type="job-data-mapType" minOccurs="0" />
</xs:sequence>
</xs:complexType>
<xs:complexType name="job-data-mapType">
<xs:annotation>
<xs:documentation>Define a JobDataMap</xs:documentation>
</xs:annotation>
<xs:sequence minOccurs="0" maxOccurs="unbounded">
<xs:element name="entry" type="entryType" />
</xs:sequence>
</xs:complexType>
<xs:complexType name="entryType">
<xs:annotation>
<xs:documentation>Define a JobDataMap entry</xs:documentation>
</xs:annotation>
<xs:sequence>
<xs:element name="key" type="xs:string" />
<xs:element name="value" type="xs:string" />
</xs:sequence>
</xs:complexType>
<xs:complexType name="triggerType">
<xs:annotation>
<xs:documentation>Define a Trigger</xs:documentation>
</xs:annotation>
<xs:choice>
<xs:element name="simple" type="simpleTriggerType" />
<xs:element name="cron" type="cronTriggerType" />
<xs:element name="calendar-interval" type="calendarIntervalTriggerType" />
</xs:choice>
</xs:complexType>
<xs:complexType name="abstractTriggerType" abstract="true">
<xs:annotation>
<xs:documentation>Common Trigger definitions</xs:documentation>
</xs:annotation>
<xs:sequence>
<xs:element name="name" type="xs:string" />
<xs:element name="group" type="xs:string" minOccurs="0" />
<xs:element name="description" type="xs:string" minOccurs="0" />
<xs:element name="job-name" type="xs:string" />
<xs:element name="job-group" type="xs:string" minOccurs="0" />
<xs:element name="priority" type="xs:nonNegativeInteger" minOccurs="0" />
<xs:element name="calendar-name" type="xs:string" minOccurs="0" />
<xs:element name="job-data-map" type="job-data-mapType" minOccurs="0" />
<xs:sequence minOccurs="0">
<xs:choice>
<xs:element name="start-time" type="xs:dateTime" />
<xs:element name="start-time-seconds-in-future" type="xs:nonNegativeInteger" />
</xs:choice>
<xs:element name="end-time" type="xs:dateTime" minOccurs="0" />
</xs:sequence>
</xs:sequence>
</xs:complexType>
<xs:complexType name="simpleTriggerType">
<xs:annotation>
<xs:documentation>Define a SimpleTrigger</xs:documentation>
</xs:annotation>
<xs:complexContent>
<xs:extension base="abstractTriggerType">
<xs:sequence>
<xs:element name="misfire-instruction" type="simple-trigger-misfire-instructionType" minOccurs="0" />
<xs:sequence minOccurs="0">
<xs:element name="repeat-count" type="repeat-countType" />
<xs:element name="repeat-interval" type="xs:nonNegativeInteger" />
</xs:sequence>
</xs:sequence>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:complexType name="cronTriggerType">
<xs:annotation>
<xs:documentation>Define a CronTrigger</xs:documentation>
</xs:annotation>
<xs:complexContent>
<xs:extension base="abstractTriggerType">
<xs:sequence>
<xs:element name="misfire-instruction" type="cron-trigger-misfire-instructionType" minOccurs="0" />
<xs:element name="cron-expression" type="cron-expressionType" />
<xs:element name="time-zone" type="xs:string" minOccurs="0" />
</xs:sequence>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:complexType name="calendarIntervalTriggerType">
<xs:annotation>
<xs:documentation>Define a DateIntervalTrigger</xs:documentation>
</xs:annotation>
<xs:complexContent>
<xs:extension base="abstractTriggerType">
<xs:sequence>
<xs:element name="misfire-instruction" type="date-interval-trigger-misfire-instructionType" minOccurs="0" />
<xs:element name="repeat-interval" type="xs:nonNegativeInteger" />
<xs:element name="repeat-interval-unit" type="interval-unitType" />
</xs:sequence>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:simpleType name="cron-expressionType">
<xs:annotation>
<xs:documentation>
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])))?)?
)
</xs:documentation>
</xs:annotation>
<xs:restriction base="xs:string">
<xs:pattern
value="(((([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](((([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](((([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](((([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](((([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]((([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])?)|([\?])|([\*]))([\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])))?)?)" />
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="repeat-countType">
<xs:annotation>
<xs:documentation>Number of times to repeat the Trigger (-1 for indefinite)</xs:documentation>
</xs:annotation>
<xs:restriction base="xs:integer">
<xs:minInclusive value="-1" />
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="simple-trigger-misfire-instructionType">
<xs:annotation>
<xs:documentation>Simple Trigger Misfire Instructions</xs:documentation>
</xs:annotation>
<xs:restriction base="xs:string">
<xs:pattern value="SmartPolicy" />
<xs:pattern value="RescheduleNextWithExistingCount" />
<xs:pattern value="RescheduleNextWithRemainingCount" />
<xs:pattern value="RescheduleNowWithExistingRepeatCount" />
<xs:pattern value="RescheduleNowWithRemainingRepeatCount" />
<xs:pattern value="FireNow" />
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="cron-trigger-misfire-instructionType">
<xs:annotation>
<xs:documentation>Cron Trigger Misfire Instructions</xs:documentation>
</xs:annotation>
<xs:restriction base="xs:string">
<xs:pattern value="SmartPolicy" />
<xs:pattern value="DoNothing" />
<xs:pattern value="FireOnceNow" />
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="date-interval-trigger-misfire-instructionType">
<xs:annotation>
<xs:documentation>Date Interval Trigger Misfire Instructions</xs:documentation>
</xs:annotation>
<xs:restriction base="xs:string">
<xs:pattern value="SmartPolicy" />
<xs:pattern value="DoNothing" />
<xs:pattern value="FireOnceNow" />
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="interval-unitType">
<xs:annotation>
<xs:documentation>Interval Units</xs:documentation>
</xs:annotation>
<xs:restriction base="xs:string">
<xs:pattern value="Day" />
<xs:pattern value="Hour" />
<xs:pattern value="Minute" />
<xs:pattern value="Month" />
<xs:pattern value="Second" />
<xs:pattern value="Week" />
<xs:pattern value="Year" />
</xs:restriction>
</xs:simpleType>
</xs:schema>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Common.Logging" version="3.0.0" targetFramework="net45" />
<package id="Common.Logging.Core" version="3.0.0" targetFramework="net45" />
<package id="Quartz" version="2.3.3" targetFramework="net45" />
</packages>

@ -26,13 +26,20 @@
#endregion #endregion
using System.Collections.Generic; using System.Collections.Generic;
using PlexRequests.Core.Models; using Newtonsoft.Json;
using PlexRequests.Core.Notification; using PlexRequests.Helpers;
namespace PlexRequests.Core.SettingModels namespace PlexRequests.Core.SettingModels
{ {
public class NewletterSettings : Settings public class NewletterSettings : Settings
{ {
public bool SendRecentlyAddedEmail { get; set; } public bool SendRecentlyAddedEmail { get; set; }
public bool SendToPlexUsers { get; set; }
public string CustomUsers { get; set; }
[JsonIgnore]
public IEnumerable<string> CustomUsersEmailAddresses => CustomUsers.SplitEmailsByDelimiter(';');
} }
} }

@ -24,23 +24,13 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/ // ************************************************************************/
#endregion #endregion
using System;
namespace PlexRequests.Core.SettingModels namespace PlexRequests.Core.SettingModels
{ {
public class ScheduledJobsSettings : Settings public class ScheduledJobsSettings : Settings
{ {
public ScheduledJobsSettings()
{
PlexAvailabilityChecker = 60;
SickRageCacher = 60;
SonarrCacher = 60;
CouchPotatoCacher = 60;
StoreBackup = 24;
StoreCleanup = 24;
UserRequestLimitResetter = 12;
PlexEpisodeCacher = 12;
RecentlyAdded = 168;
}
public int PlexAvailabilityChecker { get; set; } public int PlexAvailabilityChecker { get; set; }
public int SickRageCacher { get; set; } public int SickRageCacher { get; set; }
public int SonarrCacher { get; set; } public int SonarrCacher { get; set; }
@ -49,6 +39,8 @@ namespace PlexRequests.Core.SettingModels
public int StoreCleanup { get; set; } public int StoreCleanup { get; set; }
public int UserRequestLimitResetter { get; set; } public int UserRequestLimitResetter { get; set; }
public int PlexEpisodeCacher { get; set; } public int PlexEpisodeCacher { get; set; }
[Obsolete("We use the CRON job now")]
public int RecentlyAdded { get; set; } public int RecentlyAdded { get; set; }
public string RecentlyAddedCron { get; set; }
} }
} }

@ -110,10 +110,9 @@ namespace PlexRequests.Core
Salt = salt, Salt = salt,
Hash = PasswordHasher.ComputeHash(password, salt), Hash = PasswordHasher.ComputeHash(password, salt),
Claims = ByteConverterHelper.ReturnBytes(claims), Claims = ByteConverterHelper.ReturnBytes(claims),
UserProperties = ByteConverterHelper.ReturnBytes(properties ?? new UserProperties()) UserProperties = ByteConverterHelper.ReturnBytes(properties ?? 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); return new Guid(userRecord.UserGuid);

@ -24,6 +24,8 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/ // ************************************************************************/
#endregion #endregion
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@ -64,5 +66,36 @@ namespace PlexRequests.Helpers
} }
return sb.ToString(); return sb.ToString();
} }
public static IEnumerable<string> SplitEmailsByDelimiter(this string input, char delimiter)
{
if (string.IsNullOrEmpty(input))
{
yield return string.Empty;
}
var startIndex = 0;
var delimiterIndex = 0;
while (delimiterIndex >= 0)
{
delimiterIndex = input.IndexOf(delimiter, startIndex);
string substring = input;
if (delimiterIndex > 0)
{
substring = input.Substring(0, delimiterIndex).Trim();
}
if (!substring.Contains("\"") || substring.IndexOf("\"") != substring.LastIndexOf("\""))
{
yield return substring;
input = input.Substring(delimiterIndex + 1).Trim();
startIndex = 0;
}
else
{
startIndex = delimiterIndex + 1;
}
}
}
} }
} }

@ -4,9 +4,10 @@ namespace PlexRequests.Helpers
{ {
public class UserClaims public class UserClaims
{ {
public const string Admin = "Admin"; // Can do everything including creating new users and editing settings public const string Admin = nameof(Admin); // Can do everything including creating new users and editing settings
public const string PowerUser = "PowerUser"; // Can only manage the requests, approve etc. public const string PowerUser = nameof(PowerUser); // Can only manage the requests, approve etc.
public const string User = "User"; // Can only request public const string User = nameof(User); // Can only request
public const string Newsletter = nameof(Newsletter); // Has newsletter feature enabled
} }
} }

@ -33,12 +33,12 @@ namespace PlexRequests.Services.Jobs
{ {
public abstract class HtmlTemplateGenerator public abstract class HtmlTemplateGenerator
{ {
protected virtual void AddParagraph(ref StringBuilder stringBuilder, string text, int fontSize = 14, string fontWeight = "normal") protected virtual void AddParagraph(StringBuilder stringBuilder, string text, int fontSize = 14, string fontWeight = "normal")
{ {
stringBuilder.AppendFormat("<p style=\"font-family: sans-serif; font-size: {1}px; font-weight: {2}; margin: 0; Margin-bottom: 15px;\">{0}</p>", text, fontSize, fontWeight); stringBuilder.AppendFormat("<p style=\"font-family: sans-serif; font-size: {1}px; font-weight: {2}; margin: 0; Margin-bottom: 15px;\">{0}</p>", text, fontSize, fontWeight);
} }
protected virtual void AddImageInsideTable(ref StringBuilder sb, string url) protected virtual void AddImageInsideTable(StringBuilder sb, string url)
{ {
sb.Append("<tr>"); sb.Append("<tr>");
sb.Append("<td align=\"center\">"); sb.Append("<td align=\"center\">");
@ -49,17 +49,17 @@ namespace PlexRequests.Services.Jobs
sb.Append("</tr>"); sb.Append("</tr>");
} }
protected virtual void Href(ref StringBuilder sb, string url) protected virtual void Href(StringBuilder sb, string url)
{ {
sb.AppendFormat("<a href=\"{0}\">", url); sb.AppendFormat("<a href=\"{0}\">", url);
} }
protected virtual void EndTag(ref StringBuilder sb, string tag) protected virtual void EndTag(StringBuilder sb, string tag)
{ {
sb.AppendFormat("</{0}>", tag); sb.AppendFormat("</{0}>", tag);
} }
protected virtual void Header(ref StringBuilder sb, int size, string text, string fontWeight = "normal") protected virtual void Header(StringBuilder sb, int size, string text, string fontWeight = "normal")
{ {
sb.AppendFormat( sb.AppendFormat(
"<h{0} style=\"font-family: sans-serif; font-weight: {2}; margin: 0; Margin-bottom: 15px;\">{1}</h{0}>", "<h{0} style=\"font-family: sans-serif; font-weight: {2}; margin: 0; Margin-bottom: 15px;\">{1}</h{0}>",

@ -51,8 +51,7 @@ namespace PlexRequests.Services.Jobs
public class RecentlyAdded : HtmlTemplateGenerator, IJob, IRecentlyAdded public class RecentlyAdded : HtmlTemplateGenerator, IJob, IRecentlyAdded
{ {
public RecentlyAdded(IPlexApi api, ISettingsService<PlexSettings> plexSettings, public RecentlyAdded(IPlexApi api, ISettingsService<PlexSettings> plexSettings,
ISettingsService<EmailNotificationSettings> email, ISettingsService<EmailNotificationSettings> email, IJobRecord rec,
ISettingsService<ScheduledJobsSettings> scheduledService, IJobRecord rec,
ISettingsService<NewletterSettings> newsletter, ISettingsService<NewletterSettings> newsletter,
IPlexReadOnlyDatabase db) IPlexReadOnlyDatabase db)
{ {
@ -60,7 +59,6 @@ namespace PlexRequests.Services.Jobs
Api = api; Api = api;
PlexSettings = plexSettings; PlexSettings = plexSettings;
EmailSettings = email; EmailSettings = email;
ScheduledJobsSettings = scheduledService;
NewsletterSettings = newsletter; NewsletterSettings = newsletter;
PlexDb = db; PlexDb = db;
} }
@ -73,7 +71,6 @@ namespace PlexRequests.Services.Jobs
private ISettingsService<PlexSettings> PlexSettings { get; } private ISettingsService<PlexSettings> PlexSettings { get; }
private ISettingsService<EmailNotificationSettings> EmailSettings { get; } private ISettingsService<EmailNotificationSettings> EmailSettings { get; }
private ISettingsService<NewletterSettings> NewsletterSettings { get; } private ISettingsService<NewletterSettings> NewsletterSettings { get; }
private ISettingsService<ScheduledJobsSettings> ScheduledJobsSettings { get; }
private IJobRecord JobRecord { get; } private IJobRecord JobRecord { get; }
private IPlexReadOnlyDatabase PlexDb { get; } private IPlexReadOnlyDatabase PlexDb { get; }
@ -88,19 +85,8 @@ namespace PlexRequests.Services.Jobs
{ {
return; return;
} }
var jobs = JobRecord.GetJobs();
var thisJob = Start(settings);
jobs.FirstOrDefault(
x => x.Name.Equals(JobNames.RecentlyAddedEmail, StringComparison.CurrentCultureIgnoreCase));
var jobSettings = ScheduledJobsSettings.GetSettings();
if (thisJob?.LastRun > DateTime.Now.AddHours(-jobSettings.RecentlyAdded))
{
return;
}
Start();
} }
catch (Exception e) catch (Exception e)
{ {
@ -114,10 +100,11 @@ namespace PlexRequests.Services.Jobs
public void Test() public void Test()
{ {
Start(true); var settings = NewsletterSettings.GetSettings();
Start(settings, true);
} }
private void Start(bool testEmail = false) private void Start(NewletterSettings newletterSettings, bool testEmail = false)
{ {
var sb = new StringBuilder(); var sb = new StringBuilder();
var plexSettings = PlexSettings.GetSettings(); var plexSettings = PlexSettings.GetSettings();
@ -129,16 +116,16 @@ namespace PlexRequests.Services.Jobs
var recentlyAddedTv = Api.RecentlyAdded(plexSettings.PlexAuthToken, plexSettings.FullUri, tvSection.Key); var recentlyAddedTv = Api.RecentlyAdded(plexSettings.PlexAuthToken, plexSettings.FullUri, tvSection.Key);
var recentlyAddedMovies = Api.RecentlyAdded(plexSettings.PlexAuthToken, plexSettings.FullUri, movieSection.Key); var recentlyAddedMovies = Api.RecentlyAdded(plexSettings.PlexAuthToken, plexSettings.FullUri, movieSection.Key);
GenerateMovieHtml(recentlyAddedMovies, plexSettings, ref sb); GenerateMovieHtml(recentlyAddedMovies, plexSettings, sb);
GenerateTvHtml(recentlyAddedTv, plexSettings, ref sb); GenerateTvHtml(recentlyAddedTv, plexSettings, sb);
var template = new RecentlyAddedTemplate(); var template = new RecentlyAddedTemplate();
var html = template.LoadTemplate(sb.ToString()); var html = template.LoadTemplate(sb.ToString());
Send(html, plexSettings, testEmail); Send(newletterSettings, html, plexSettings, testEmail);
} }
private void GenerateMovieHtml(RecentlyAddedModel movies, PlexSettings plexSettings, ref StringBuilder sb) private void GenerateMovieHtml(RecentlyAddedModel movies, PlexSettings plexSettings, StringBuilder sb)
{ {
sb.Append("<h1>New Movies:</h1><br/><br/>"); sb.Append("<h1>New Movies:</h1><br/><br/>");
sb.Append( sb.Append(
@ -156,94 +143,41 @@ namespace PlexRequests.Services.Jobs
var imdbId = PlexHelper.GetProviderIdFromPlexGuid(plexGUID); var imdbId = PlexHelper.GetProviderIdFromPlexGuid(plexGUID);
var info = _movieApi.GetMovieInformation(imdbId).Result; var info = _movieApi.GetMovieInformation(imdbId).Result;
AddImageInsideTable(ref sb, $"https://image.tmdb.org/t/p/w500{info.BackdropPath}"); AddImageInsideTable(sb, $"https://image.tmdb.org/t/p/w500{info.BackdropPath}");
sb.Append("<tr>"); sb.Append("<tr>");
sb.Append( sb.Append(
"<td align=\"center\" style=\"font-family: sans-serif; font-size: 14px; vertical-align: top;\" valign=\"top\">"); "<td align=\"center\" style=\"font-family: sans-serif; font-size: 14px; vertical-align: top;\" valign=\"top\">");
Href(ref sb, $"https://www.imdb.com/title/{info.ImdbId}/"); Href(sb, $"https://www.imdb.com/title/{info.ImdbId}/");
Header(ref sb, 3, $"{info.Title} {info.ReleaseDate?.ToString("yyyy") ?? string.Empty}"); Header(sb, 3, $"{info.Title} {info.ReleaseDate?.ToString("yyyy") ?? string.Empty}");
EndTag(ref sb, "a"); EndTag(sb, "a");
if (info.Genres.Any()) if (info.Genres.Any())
{ {
AddParagraph(ref sb, $"Genre: {string.Join(", ", info.Genres.Select(x => x.Name.ToString()).ToArray())}"); AddParagraph(sb,
$"Genre: {string.Join(", ", info.Genres.Select(x => x.Name.ToString()).ToArray())}");
} }
AddParagraph(ref sb, info.Overview); AddParagraph(sb, info.Overview);
EndLoopHtml(ref sb);
} }
catch (Exception e) catch (Exception e)
{ {
Log.Error(e); Log.Error(e);
Log.Error("Exception when trying to process a Movie, either in getting the metadata from Plex OR getting the information from TheMovieDB, Plex GUID = {0}", plexGUID); Log.Error(
"Exception when trying to process a Movie, either in getting the metadata from Plex OR getting the information from TheMovieDB, Plex GUID = {0}",
plexGUID);
} }
finally
}
sb.Append("</table><br/><br/>");
}
private void GenerateMovieHtml(IEnumerable<MetadataItems> movies, ref StringBuilder sb)
{
var items = movies as MetadataItems[] ?? movies.ToArray();
if (!items.Any())
{
return;
}
sb.Append("<h1>New Movies:</h1><br/><br/>");
sb.Append(
"<table border=\"0\" cellpadding=\"0\" align=\"center\" cellspacing=\"0\" style=\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;\" width=\"100%\">");
foreach (var movie in items.OrderByDescending(x => x.added_at))
{
var plexGUID = string.Empty;
try
{ {
plexGUID = movie.guid; EndLoopHtml(sb);
var imdbId = PlexHelper.GetProviderIdFromPlexGuid(plexGUID);
var info = _movieApi.GetMovieInformation(imdbId).Result; // TODO remove this and get the image info from Plex https://github.com/jakewaldron/PlexEmail/blob/master/scripts/plexEmail.py#L391
AddImageInsideTable(ref sb, $"https://image.tmdb.org/t/p/w500{info.BackdropPath}");
sb.Append("<tr>");
sb.Append("<td align=\"center\" style=\"font-family: sans-serif; font-size: 14px; vertical-align: top;\" valign=\"top\">");
Href(ref sb, $"https://www.imdb.com/title/{info.ImdbId}/");
var title = string.IsNullOrEmpty(movie.original_title)
? $"{movie.title} {movie.originally_available_at:yyyy}"
: $"{movie.original_title} AKA {movie.title} {movie.originally_available_at:yyyy}";
Header(ref sb, 3, title);
EndTag(ref sb, "a");
if (!string.IsNullOrEmpty(movie.tagline))
{
AddParagraph(ref sb, movie.tagline);
}
if (!string.IsNullOrEmpty(movie.tags_genre))
{
AddParagraph(ref sb, $"Genre: {PlexHelper.FormatGenres(movie.tags_genre)}");
}
AddParagraph(ref sb, movie.summary);
EndLoopHtml(ref sb);
}
catch (Exception e)
{
Log.Error(e);
Log.Error("Exception when trying to process a Movie, either in getting the metadata from Plex OR getting the information from TheMovieDB, Plex GUID = {0}", plexGUID);
} }
} }
sb.Append("</table><br/><br/>"); sb.Append("</table><br/><br/>");
} }
private void GenerateTvHtml(RecentlyAddedModel tv, PlexSettings plexSettings, ref StringBuilder sb) private void GenerateTvHtml(RecentlyAddedModel tv, PlexSettings plexSettings, StringBuilder sb)
{ {
// TV // TV
sb.Append("<h1>New Episodes:</h1><br/><br/>"); sb.Append("<h1>New Episodes:</h1><br/><br/>");
@ -267,117 +201,42 @@ namespace PlexRequests.Services.Jobs
{ {
banner = banner.Replace("http", "https"); // Always use the Https banners banner = banner.Replace("http", "https"); // Always use the Https banners
} }
sb.Append("<tr>"); AddImageInsideTable(sb, banner);
sb.Append("<td align=\"center\">");
sb.AppendFormat("<img src=\"{0}\" width=\"400px\" text-align=\"center\" />", banner);
sb.Append("</td>");
sb.Append("</tr>");
sb.Append("<tr>");
sb.Append("<td align=\"center\" style=\"font-family: sans-serif; font-size: 14px; vertical-align: top;\" valign=\"top\">");
var title = $"{t.grandparentTitle} - {t.title} {t.originallyAvailableAt?.Substring(0, 4)}";
sb.AppendFormat("<a href=\"https://www.imdb.com/title/{0}/\"><h3 style=\"font-family: sans-serif; font-weight: normal; margin: 0; Margin-bottom: 15px;\">{1}</p></a>",
info.externals.imdb, title); // Only the year
sb.AppendFormat("<p style=\"font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;\">Season: {0}, Episode: {1}</p>", t.parentIndex, t.index);
sb.AppendFormat("<p style=\"font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;\">Genre: {0}</p>", string.Join(", ", info.genres.Select(x => x.ToString()).ToArray()));
sb.AppendFormat("<p style=\"font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;\">{0}</p>",
string.IsNullOrEmpty(t.summary) ? info.summary : t.summary); // Episode Summary
sb.Append("<td");
sb.Append("<hr>");
sb.Append("<br>");
sb.Append("<br>");
sb.Append("</tr>");
}
catch (Exception e)
{
sb.Append("<td");
sb.Append("<hr>");
sb.Append("<br>");
sb.Append("<br>");
sb.Append("</tr>");
Log.Error(e);
Log.Error("Exception when trying to process a TV Show, either in getting the metadata from Plex OR getting the information from TVMaze, Plex GUID = {0}", plexGUID);
}
}
sb.Append("</table><br/><br/>");
}
private void GenerateTvHtml(IEnumerable<MetadataItems> tv, ref StringBuilder sb)
{
var items = tv as MetadataItems[] ?? tv.ToArray();
if (!items.Any())
{
return;
}
// TV
sb.Append("<h1>New Episodes:</h1><br/><br/>");
sb.Append(
"<table border=\"0\" cellpadding=\"0\" align=\"center\" cellspacing=\"0\" style=\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;\" width=\"100%\">");
foreach (var t in items.OrderByDescending(x => x.added_at))
{
var plexGUID = string.Empty;
try
{
plexGUID = t.guid;
var seasonInfo = PlexHelper.GetSeasonsAndEpisodesFromPlexGuid(plexGUID);
var info = TvApi.ShowLookupByTheTvDbId(int.Parse(PlexHelper.GetProviderIdFromPlexGuid(plexGUID)));
var banner = info.image?.original;
if (!string.IsNullOrEmpty(banner))
{
banner = banner.Replace("http", "https"); // Always use the Https banners
}
sb.Append("<tr>"); sb.Append("<tr>");
sb.Append("<td align=\"center\">"); sb.Append(
sb.AppendFormat("<img src=\"{0}\" width=\"400px\" text-align=\"center\" />", banner); "<td align=\"center\" style=\"font-family: sans-serif; font-size: 14px; vertical-align: top;\" valign=\"top\">");
sb.Append("</td>");
sb.Append("</tr>");
sb.Append("<tr>");
sb.Append("<td align=\"center\" style=\"font-family: sans-serif; font-size: 14px; vertical-align: top;\" valign=\"top\">");
var title = !string.IsNullOrEmpty(t.SeriesTitle)
? $"{t.SeriesTitle} - {t.title} {t.originally_available_at:yyyy}"
: $"{t.title}";
sb.AppendFormat("<a href=\"https://www.imdb.com/title/{0}/\"><h3 style=\"font-family: sans-serif; font-weight: normal; margin: 0; Margin-bottom: 15px;\">{1}</p></a>", var title = $"{t.grandparentTitle} - {t.title} {t.originallyAvailableAt?.Substring(0, 4)}";
info.externals.imdb, title);
sb.AppendFormat("<p style=\"font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;\">Season: {0}, Episode: {1}</p>", seasonInfo.SeasonNumber, seasonInfo.EpisodeNumber); Href(sb, $"https://www.imdb.com/title/{info.externals.imdb}/");
Header(sb, 3, title);
EndTag(sb, "a");
AddParagraph(sb, $"Season: {t.parentIndex}, Episode: {t.index}");
if (info.genres.Any()) if (info.genres.Any())
{ {
sb.AppendFormat( AddParagraph(sb, $"Genre: {string.Join(", ", info.genres.Select(x => x.ToString()).ToArray())}");
"<p style=\"font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;\">Genre: {0}</p>",
string.Join(", ", info.genres.Select(x => x.ToString()).ToArray()));
} }
sb.AppendFormat("<p style=\"font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;\">{0}</p>",
string.IsNullOrEmpty(t.summary) ? info.summary : t.summary); // Episode Summary AddParagraph(sb, string.IsNullOrEmpty(t.summary) ? info.summary : t.summary);
sb.Append("<td");
sb.Append("<hr>");
sb.Append("<br>");
sb.Append("<br>");
sb.Append("</tr>");
} }
catch (Exception e) catch (Exception e)
{ {
Log.Error(e); Log.Error(e);
Log.Error("Exception when trying to process a TV Show, either in getting the metadata from Plex OR getting the information from TVMaze, Plex GUID = {0}", plexGUID); Log.Error(
"Exception when trying to process a TV Show, either in getting the metadata from Plex OR getting the information from TVMaze, Plex GUID = {0}",
plexGUID);
}
finally
{
EndLoopHtml(sb);
} }
} }
sb.Append("</table><br/><br/>"); sb.Append("</table><br/><br/>");
} }
private void Send(string html, PlexSettings plexSettings, bool testEmail = false) private void Send(NewletterSettings newletterSettings, string html, PlexSettings plexSettings, bool testEmail = false)
{ {
var settings = EmailSettings.GetSettings(); var settings = EmailSettings.GetSettings();
@ -395,13 +254,24 @@ namespace PlexRequests.Services.Jobs
if (!testEmail) if (!testEmail)
{ {
var users = Api.GetUsers(plexSettings.PlexAuthToken); if (newletterSettings.SendToPlexUsers)
foreach (var user in users.User) {
var users = Api.GetUsers(plexSettings.PlexAuthToken);
foreach (var user in users.User)
{
message.Bcc.Add(new MailboxAddress(user.Username, user.Email));
}
}
if (newletterSettings.CustomUsersEmailAddresses.Any())
{ {
message.Bcc.Add(new MailboxAddress(user.Username, user.Email)); foreach (var user in newletterSettings.CustomUsersEmailAddresses)
{
message.Bcc.Add(new MailboxAddress(user, user));
}
} }
} }
message.Bcc.Add(new MailboxAddress(settings.EmailUsername, settings.EmailSender)); // Include the admin message.Bcc.Add(new MailboxAddress(settings.EmailUsername, settings.RecipientEmail)); // Include the admin
message.From.Add(new MailboxAddress(settings.EmailUsername, settings.EmailSender)); message.From.Add(new MailboxAddress(settings.EmailUsername, settings.EmailSender));
try try
@ -429,7 +299,7 @@ namespace PlexRequests.Services.Jobs
} }
} }
private void EndLoopHtml(ref StringBuilder sb) private void EndLoopHtml(StringBuilder sb)
{ {
sb.Append("<td"); sb.Append("<td");
sb.Append("<hr>"); sb.Append("<hr>");

@ -39,14 +39,14 @@ namespace PlexRequests.Store
/// Creates the tables located in the SqlTables.sql file. /// Creates the tables located in the SqlTables.sql file.
/// </summary> /// </summary>
/// <param name="connection">The connection.</param> /// <param name="connection">The connection.</param>
public static void CreateTables(IDbConnection connection) public static void CreateTables(this IDbConnection connection)
{ {
connection.Open(); connection.Open();
connection.Execute(Sql.SqlTables); connection.Execute(Sql.SqlTables);
connection.Close(); connection.Close();
} }
public static void DropTable(IDbConnection con, string tableName) public static void DropTable(this IDbConnection con, string tableName)
{ {
using (con) using (con)
{ {
@ -57,7 +57,7 @@ namespace PlexRequests.Store
} }
} }
public static void AddColumn(IDbConnection connection, string tableName, string alterType, string newColumn, bool isNullable, string dataType) public static void AddColumn(this IDbConnection connection, string tableName, string alterType, string newColumn, bool isNullable, string dataType)
{ {
connection.Open(); connection.Open();
var result = connection.Query<TableInfo>($"PRAGMA table_info({tableName});"); var result = connection.Query<TableInfo>($"PRAGMA table_info({tableName});");
@ -77,7 +77,7 @@ namespace PlexRequests.Store
connection.Close(); connection.Close();
} }
public static void Vacuum(IDbConnection con) public static void Vacuum(this IDbConnection con)
{ {
using (con) using (con)
{ {

@ -1,39 +1,39 @@
#region Copyright #region Copyright
// /************************************************************************ // /************************************************************************
// Copyright (c) 2016 Jamie Rees // Copyright (c) 2016 Jamie Rees
// File: UserModel.cs // File: UserModel.cs
// Created By: Jamie Rees // Created By: Jamie Rees
// //
// Permission is hereby granted, free of charge, to any person obtaining // Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the // a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including // "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish, // without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to // distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to // permit persons to whom the Software is furnished to do so, subject to
// the following conditions: // the following conditions:
// //
// The above copyright notice and this permission notice shall be // The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software. // included in all copies or substantial portions of the Software.
// //
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/ // ************************************************************************/
#endregion #endregion
using Dapper.Contrib.Extensions; using Dapper.Contrib.Extensions;
namespace PlexRequests.Store namespace PlexRequests.Store
{ {
[Table("Users")] [Table("Users")]
public class UsersModel : UserEntity public class UsersModel : UserEntity
{ {
public byte[] Hash { get; set; } public byte[] Hash { get; set; }
public byte[] Salt { get; set; } public byte[] Salt { get; set; }
public byte[] Claims { get; set; } public byte[] Claims { get; set; }
public byte[] UserProperties { get; set; } public byte[] UserProperties { get; set; }
} }
} }

@ -22,7 +22,10 @@
// Select a user to populate on the right side // Select a user to populate on the right side
$scope.selectUser = function (id) { $scope.selectUser = function (id) {
$scope.selectedUser = $scope.users.find(x => x.id === id); var user = $scope.users.filter(function (item) {
return item.id === id;
});
$scope.selectedUser = user[0];
} }
// Get all users in the system // Get all users in the system
@ -64,6 +67,15 @@
}); });
}; };
$scope.hasClaim = function(claim) {
var claims = $scope.selectedUser.claimsArray;
var result = claims.some(function (item) {
return item === claim.name;
});
return result;
};
$scope.$watch('claims|filter:{selected:true}', $scope.$watch('claims|filter:{selected:true}',
function (nv) { function (nv) {
$scope.selectedClaims = nv.map(function (claim) { $scope.selectedClaims = nv.map(function (claim) {
@ -74,7 +86,7 @@
$scope.updateUser = function () { $scope.updateUser = function () {
userManagementService.updateUser($scope.selectedUser.id, $scope.selectedUser.claimsItem);
} }
function getBaseUrl() { function getBaseUrl() {

@ -24,10 +24,20 @@
return $http.get('/usermanagement/claims'); return $http.get('/usermanagement/claims');
} }
var updateUser = function (id, claims) {
return $http({
url: '/usermanagement/updateUser',
method: "POST",
data: { id: id, claims: claims }
});
}
return { return {
getUsers: getUsers, getUsers: getUsers,
addUser: addUser, addUser: addUser,
getClaims: getClaims getClaims: getClaims,
updateUser: updateUser,
}; };
} }

@ -54,6 +54,9 @@ namespace PlexRequests.UI.Jobs
private IEnumerable<IJobDetail> CreateJobs() private IEnumerable<IJobDetail> CreateJobs()
{ {
var settingsService = Service.Resolve<ISettingsService<ScheduledJobsSettings>>();
var s = settingsService.GetSettings();
var jobs = new List<IJobDetail>(); var jobs = new List<IJobDetail>();
var jobList = new List<IJobDetail> var jobList = new List<IJobDetail>
@ -66,9 +69,13 @@ namespace PlexRequests.UI.Jobs
JobBuilder.Create<StoreBackup>().WithIdentity("StoreBackup", "Database").Build(), JobBuilder.Create<StoreBackup>().WithIdentity("StoreBackup", "Database").Build(),
JobBuilder.Create<StoreCleanup>().WithIdentity("StoreCleanup", "Database").Build(), JobBuilder.Create<StoreCleanup>().WithIdentity("StoreCleanup", "Database").Build(),
JobBuilder.Create<UserRequestLimitResetter>().WithIdentity("UserRequestLimiter", "Request").Build(), JobBuilder.Create<UserRequestLimitResetter>().WithIdentity("UserRequestLimiter", "Request").Build(),
JobBuilder.Create<RecentlyAdded>().WithIdentity("RecentlyAddedModel", "Email").Build()
}; };
if (!string.IsNullOrEmpty(s.RecentlyAddedCron))
{
jobList.Add(JobBuilder.Create<RecentlyAdded>().WithIdentity("RecentlyAddedModel", "Email").Build());
}
jobs.AddRange(jobList); jobs.AddRange(jobList);
@ -112,66 +119,76 @@ namespace PlexRequests.UI.Jobs
var plexAvailabilityChecker = var plexAvailabilityChecker =
TriggerBuilder.Create() TriggerBuilder.Create()
.WithIdentity("PlexAvailabilityChecker", "Plex") .WithIdentity("PlexAvailabilityChecker", "Plex")
.StartNow() .StartNow()
.WithSimpleSchedule(x => x.WithIntervalInMinutes(s.PlexAvailabilityChecker).RepeatForever()) .WithSimpleSchedule(x => x.WithIntervalInMinutes(s.PlexAvailabilityChecker).RepeatForever())
.Build(); .Build();
var srCacher = var srCacher =
TriggerBuilder.Create() TriggerBuilder.Create()
.WithIdentity("SickRageCacher", "Cache") .WithIdentity("SickRageCacher", "Cache")
.StartNow() .StartNow()
.WithSimpleSchedule(x => x.WithIntervalInMinutes(s.SickRageCacher).RepeatForever()) .WithSimpleSchedule(x => x.WithIntervalInMinutes(s.SickRageCacher).RepeatForever())
.Build(); .Build();
var sonarrCacher = var sonarrCacher =
TriggerBuilder.Create() TriggerBuilder.Create()
.WithIdentity("SonarrCacher", "Cache") .WithIdentity("SonarrCacher", "Cache")
.StartNow() .StartNow()
.WithSimpleSchedule(x => x.WithIntervalInMinutes(s.SonarrCacher).RepeatForever()) .WithSimpleSchedule(x => x.WithIntervalInMinutes(s.SonarrCacher).RepeatForever())
.Build(); .Build();
var cpCacher = var cpCacher =
TriggerBuilder.Create() TriggerBuilder.Create()
.WithIdentity("CouchPotatoCacher", "Cache") .WithIdentity("CouchPotatoCacher", "Cache")
.StartNow() .StartNow()
.WithSimpleSchedule(x => x.WithIntervalInMinutes(s.CouchPotatoCacher).RepeatForever()) .WithSimpleSchedule(x => x.WithIntervalInMinutes(s.CouchPotatoCacher).RepeatForever())
.Build(); .Build();
var storeBackup = var storeBackup =
TriggerBuilder.Create() TriggerBuilder.Create()
.WithIdentity("StoreBackup", "Database") .WithIdentity("StoreBackup", "Database")
.StartNow() .StartNow()
.WithSimpleSchedule(x => x.WithIntervalInHours(s.StoreBackup).RepeatForever()) .WithSimpleSchedule(x => x.WithIntervalInHours(s.StoreBackup).RepeatForever())
.Build(); .Build();
var storeCleanup = var storeCleanup =
TriggerBuilder.Create() TriggerBuilder.Create()
.WithIdentity("StoreCleanup", "Database") .WithIdentity("StoreCleanup", "Database")
.StartNow() .StartNow()
.WithSimpleSchedule(x => x.WithIntervalInHours(s.StoreCleanup).RepeatForever()) .WithSimpleSchedule(x => x.WithIntervalInHours(s.StoreCleanup).RepeatForever())
.Build(); .Build();
var userRequestLimiter = var userRequestLimiter =
TriggerBuilder.Create() TriggerBuilder.Create()
.WithIdentity("UserRequestLimiter", "Request") .WithIdentity("UserRequestLimiter", "Request")
.StartAt(DateTimeOffset.Now.AddMinutes(5)) // Everything has started on application start, lets wait 5 minutes .StartAt(DateBuilder.FutureDate(5, IntervalUnit.Minute))
.WithSimpleSchedule(x => x.WithIntervalInHours(s.UserRequestLimitResetter).RepeatForever()) // Everything has started on application start, lets wait 5 minutes
.Build(); .WithSimpleSchedule(x => x.WithIntervalInHours(s.UserRequestLimitResetter).RepeatForever())
.Build();
var plexEpCacher = var plexEpCacher =
TriggerBuilder.Create() TriggerBuilder.Create()
.WithIdentity("PlexEpisodeCacher", "Cache") .WithIdentity("PlexEpisodeCacher", "Cache")
.StartAt(DateTimeOffset.Now.AddMinutes(5)) .StartAt(DateBuilder.FutureDate(5, IntervalUnit.Minute))
.WithSimpleSchedule(x => x.WithIntervalInHours(s.PlexEpisodeCacher).RepeatForever()) .WithSimpleSchedule(x => x.WithIntervalInHours(s.PlexEpisodeCacher).RepeatForever())
.Build(); .Build();
var cronJob = string.IsNullOrEmpty(s.RecentlyAddedCron);
if (!cronJob)
{
var rencentlyAdded =
TriggerBuilder.Create()
.WithIdentity("RecentlyAddedModel", "Email")
.StartNow()
.WithCronSchedule(s.RecentlyAddedCron)
.WithSimpleSchedule(x => x.WithIntervalInHours(2).RepeatForever())
.Build();
triggers.Add(rencentlyAdded);
}
var rencentlyAdded =
TriggerBuilder.Create()
.WithIdentity("RecentlyAddedModel", "Email")
.StartNow()
.WithSimpleSchedule(x => x.WithIntervalInHours(2).RepeatForever())
.Build();
triggers.Add(plexAvailabilityChecker); triggers.Add(plexAvailabilityChecker);
@ -182,7 +199,6 @@ namespace PlexRequests.UI.Jobs
triggers.Add(storeCleanup); triggers.Add(storeCleanup);
triggers.Add(userRequestLimiter); triggers.Add(userRequestLimiter);
triggers.Add(plexEpCacher); triggers.Add(plexEpCacher);
triggers.Add(rencentlyAdded);
return triggers; return triggers;
} }

@ -1,50 +1,51 @@
#region Copyright #region Copyright
// /************************************************************************ // /************************************************************************
// Copyright (c) 2016 Jamie Rees // Copyright (c) 2016 Jamie Rees
// File: SearchTvShowViewModel.cs // File: SearchTvShowViewModel.cs
// Created By: Jamie Rees // Created By: Jamie Rees
// //
// Permission is hereby granted, free of charge, to any person obtaining // Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the // a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including // "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish, // without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to // distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to // permit persons to whom the Software is furnished to do so, subject to
// the following conditions: // the following conditions:
// //
// The above copyright notice and this permission notice shall be // The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software. // included in all copies or substantial portions of the Software.
// //
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/ // ************************************************************************/
#endregion #endregion
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace PlexRequests.UI.Models namespace PlexRequests.UI.Models
{ {
public class SearchMovieViewModel : SearchViewModel public class SearchMovieViewModel : SearchViewModel
{ {
public bool Adult { get; set; } public bool Adult { get; set; }
public string BackdropPath { get; set; } public string BackdropPath { get; set; }
public List<int> GenreIds { get; set; } public List<int> GenreIds { get; set; }
public int Id { get; set; } public int Id { get; set; }
public string OriginalLanguage { get; set; } public string OriginalLanguage { get; set; }
public string OriginalTitle { get; set; } public string OriginalTitle { get; set; }
public string Overview { get; set; } public string Overview { get; set; }
public double Popularity { get; set; } public double Popularity { get; set; }
public string PosterPath { get; set; } public string PosterPath { get; set; }
public DateTime? ReleaseDate { get; set; } public DateTime? ReleaseDate { get; set; }
public string Title { get; set; } public string Title { get; set; }
public bool Video { get; set; } public bool Video { get; set; }
public double VoteAverage { get; set; } public double VoteAverage { get; set; }
public int VoteCount { get; set; } public int VoteCount { get; set; }
} public bool AlreadyInCp { get; set; }
}
} }

@ -17,6 +17,7 @@ namespace PlexRequests.UI.Models
public string EmailAddress { get; set; } public string EmailAddress { get; set; }
public UserManagementPlexInformation PlexInfo { get; set; } public UserManagementPlexInformation PlexInfo { get; set; }
public string[] ClaimsArray { get; set; } public string[] ClaimsArray { get; set; }
public List<UserManagementUpdateModel.ClaimsModel> ClaimsItem { get; set; }
} }
public class UserManagementPlexInformation public class UserManagementPlexInformation
@ -57,5 +58,22 @@ namespace PlexRequests.UI.Models
[JsonProperty("email")] [JsonProperty("email")]
public string EmailAddress { get; set; } public string EmailAddress { get; set; }
} }
public class UserManagementUpdateModel
{
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("claims")]
public List<ClaimsModel> Claims { get; set; }
public class ClaimsModel
{
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("selected")]
public bool Selected { get; set; }
}
}
} }

@ -63,7 +63,7 @@ using PlexRequests.Store.Models;
using PlexRequests.Store.Repository; using PlexRequests.Store.Repository;
using PlexRequests.UI.Helpers; using PlexRequests.UI.Helpers;
using PlexRequests.UI.Models; using PlexRequests.UI.Models;
using Quartz;
using Action = PlexRequests.Helpers.Analytics.Action; using Action = PlexRequests.Helpers.Analytics.Action;
namespace PlexRequests.UI.Modules namespace PlexRequests.UI.Modules
@ -97,7 +97,7 @@ namespace PlexRequests.UI.Modules
private IJobRecord JobRecorder { get; } private IJobRecord JobRecorder { get; }
private IAnalytics Analytics { get; } private IAnalytics Analytics { get; }
private IRecentlyAdded RecentlyAdded { get; } private IRecentlyAdded RecentlyAdded { get; }
private ISettingsService<NotificationSettingsV2> NotifySettings { get; } private ISettingsService<NotificationSettingsV2> NotifySettings { get; }
private static Logger Log = LogManager.GetCurrentClassLogger(); private static Logger Log = LogManager.GetCurrentClassLogger();
public AdminModule(ISettingsService<PlexRequestSettings> prService, public AdminModule(ISettingsService<PlexRequestSettings> prService,
@ -210,7 +210,7 @@ namespace PlexRequests.UI.Modules
Post["/autoupdate"] = x => AutoUpdate(); Post["/autoupdate"] = x => AutoUpdate();
Post["/testslacknotification", true] = async (x,ct) => await TestSlackNotification(); Post["/testslacknotification", true] = async (x, ct) => await TestSlackNotification();
Get["/slacknotification"] = _ => SlackNotifications(); Get["/slacknotification"] = _ => SlackNotifications();
Post["/slacknotification"] = _ => SaveSlackNotifications(); Post["/slacknotification"] = _ => SaveSlackNotifications();
@ -975,7 +975,8 @@ namespace PlexRequests.UI.Modules
SonarrCacher = s.SonarrCacher, SonarrCacher = s.SonarrCacher,
StoreBackup = s.StoreBackup, StoreBackup = s.StoreBackup,
StoreCleanup = s.StoreCleanup, StoreCleanup = s.StoreCleanup,
JobRecorder = jobsDict JobRecorder = jobsDict,
RecentlyAddedCron = s.RecentlyAddedCron
}; };
return View["SchedulerSettings", model]; return View["SchedulerSettings", model];
} }
@ -986,6 +987,21 @@ namespace PlexRequests.UI.Modules
Analytics.TrackEventAsync(Category.Admin, Action.Update, "Update ScheduledJobs", Username, CookieHelper.GetAnalyticClientId(Cookies)); Analytics.TrackEventAsync(Category.Admin, Action.Update, "Update ScheduledJobs", Username, CookieHelper.GetAnalyticClientId(Cookies));
var settings = this.Bind<ScheduledJobsSettings>(); var settings = this.Bind<ScheduledJobsSettings>();
if (!string.IsNullOrEmpty(settings.RecentlyAddedCron))
{
// Validate CRON
var isValid = CronExpression.IsValidExpression(settings.RecentlyAddedCron);
if (!isValid)
{
return Response.AsJson(new JsonResponseModel
{
Result = false,
Message =
$"CRON {settings.RecentlyAddedCron} is not valid. Please ensure you are using a valid CRON."
});
}
}
var result = await ScheduledJobSettings.SaveSettingsAsync(settings); var result = await ScheduledJobSettings.SaveSettingsAsync(settings);
return Response.AsJson(result return Response.AsJson(result

@ -35,6 +35,7 @@ namespace PlexRequests.UI.Modules
Get["/local/{id}"] = x => LocalDetails((Guid)x.id); Get["/local/{id}"] = x => LocalDetails((Guid)x.id);
Get["/plex/{id}", true] = async (x, ct) => await PlexDetails(x.id); Get["/plex/{id}", true] = async (x, ct) => await PlexDetails(x.id);
Get["/claims"] = x => GetClaims(); Get["/claims"] = x => GetClaims();
Post["/updateuser"] = x => UpdateUser();
} }
private ICustomUserMapper UserMapper { get; } private ICustomUserMapper UserMapper { get; }
@ -57,15 +58,35 @@ namespace PlexRequests.UI.Modules
var userProps = ByteConverterHelper.ReturnObject<UserProperties>(user.UserProperties); var userProps = ByteConverterHelper.ReturnObject<UserProperties>(user.UserProperties);
model.Add(new UserManagementUsersViewModel var m = new UserManagementUsersViewModel
{ {
Id = user.UserGuid, Id = user.UserGuid,
Claims = claimsString, Claims = claimsString,
Username = user.UserName, Username = user.UserName,
Type = UserType.LocalUser, Type = UserType.LocalUser,
EmailAddress = userProps.EmailAddress, EmailAddress = userProps.EmailAddress,
ClaimsArray = claims ClaimsArray = claims,
}); ClaimsItem = new List<UserManagementUpdateModel.ClaimsModel>()
};
// Add all of the current claims
foreach (var c in claims)
{
m.ClaimsItem.Add(new UserManagementUpdateModel.ClaimsModel { Name = c, Selected = true });
}
var allClaims = UserMapper.GetAllClaims();
// Get me the current claims that the user does not have
var missingClaims = allClaims.Except(claims);
// Add them into the view
foreach (var missingClaim in missingClaims)
{
m.ClaimsItem.Add(new UserManagementUpdateModel.ClaimsModel { Name = missingClaim, Selected = false });
}
model.Add(m);
} }
var plexSettings = await PlexSettings.GetSettingsAsync(); var plexSettings = await PlexSettings.GetSettingsAsync();
@ -121,6 +142,44 @@ namespace PlexRequests.UI.Modules
return Response.AsJson(new JsonResponseModel { Result = false, Message = "Could not save user" }); return Response.AsJson(new JsonResponseModel { Result = false, Message = "Could not save user" });
} }
private Response UpdateUser()
{
var body = Request.Body.AsString();
if (string.IsNullOrEmpty(body))
{
return Response.AsJson(new JsonResponseModel { Result = false, Message = "Could not save user, invalid JSON body" });
}
var model = JsonConvert.DeserializeObject<UserManagementUpdateModel>(body);
if (string.IsNullOrWhiteSpace(model.Id))
{
return Response.AsJson(new JsonResponseModel
{
Result = true,
Message = "Couldn't find the user"
});
}
var claims = new List<string>();
foreach (var c in model.Claims)
{
if (c.Selected)
{
claims.Add(c.Name);
}
}
var userFound = UserMapper.GetUser(new Guid(model.Id));
userFound.Claims = ByteConverterHelper.ReturnBytes(claims.ToArray());
var user = UserMapper.EditUser(userFound);
return Response.AsJson(user);
}
private Response LocalDetails(Guid id) private Response LocalDetails(Guid id)
{ {
var localUser = UserMapper.GetUser(id); var localUser = UserMapper.GetUser(id);

@ -48,30 +48,41 @@ namespace PlexRequests.UI
{ {
try try
{ {
Debug.WriteLine("Starting StartupConfiguration"); Debug.WriteLine("Starting StartupConfiguration");
var resolver = new DependancyResolver(); var resolver = new DependancyResolver();
Debug.WriteLine("Created DI Resolver"); Debug.WriteLine("Created DI Resolver");
var modules = resolver.GetModules(); var modules = resolver.GetModules();
Debug.WriteLine("Getting all the modules"); Debug.WriteLine("Getting all the modules");
Debug.WriteLine("Modules found finished."); Debug.WriteLine("Modules found finished.");
var kernel = new StandardKernel(modules); var kernel = new StandardKernel(modules);
Debug.WriteLine("Created Kernel and Injected Modules"); Debug.WriteLine("Created Kernel and Injected Modules");
Debug.WriteLine("Added Contravariant Binder"); Debug.WriteLine("Added Contravariant Binder");
kernel.Components.Add<IBindingResolver, ContravariantBindingResolver>(); kernel.Components.Add<IBindingResolver, ContravariantBindingResolver>();
Debug.WriteLine("Start the bootstrapper with the Kernel.ı"); Debug.WriteLine("Start the bootstrapper with the Kernel.");
app.UseNancy(options => options.Bootstrapper = new Bootstrapper(kernel)); app.UseNancy(options => options.Bootstrapper = new Bootstrapper(kernel));
Debug.WriteLine("Finished bootstrapper"); Debug.WriteLine("Finished bootstrapper");
var scheduler = new Scheduler();
scheduler.StartScheduler();
Debug.WriteLine("Migrating DB Now");
var runner = kernel.Get<IMigrationRunner>(); var runner = kernel.Get<IMigrationRunner>();
runner.MigrateToLatest(); runner.MigrateToLatest();
Debug.WriteLine("Settings up Scheduler");
var scheduler = new Scheduler();
scheduler.StartScheduler();
//var c = kernel.Get<IRecentlyAdded>();
//c.Test();
} }
catch (Exception exception) catch (Exception exception)
{ {

@ -16,12 +16,30 @@
<small>Note: This will require you to setup your email notifications</small> <small>Note: This will require you to setup your email notifications</small>
@if (Model.SendRecentlyAddedEmail) @if (Model.SendRecentlyAddedEmail)
{ {
<input type="checkbox" id="SendRecentlyAddedEmail" name="SendRecentlyAddedEmail" checked="checked"><label for="SendRecentlyAddedEmail">Send out a weekly email of recently added content to all your Plex 'Friends'</label> <input type="checkbox" id="SendRecentlyAddedEmail" name="SendRecentlyAddedEmail" checked="checked"><label for="SendRecentlyAddedEmail">Enable the newsletter of recently added content</label>
} }
else else
{ {
<input type="checkbox" id="SendRecentlyAddedEmail" name="SendRecentlyAddedEmail"><label for="SendRecentlyAddedEmail">Send out a weekly email of recently added content to all your Plex 'Friends'</label> <input type="checkbox" id="SendRecentlyAddedEmail" name="SendRecentlyAddedEmail"><label for="SendRecentlyAddedEmail">Enable the newsletter of recently added content</label>
} }
@if (Model.SendToPlexUsers)
{
<input type="checkbox" id="SendToPlexUsers" name="SendToPlexUsers" checked="checked"><label for="SendToPlexUsers">Send to all of your Plex 'Friends'</label>
}
else
{
<input type="checkbox" id="SendToPlexUsers" name="SendToPlexUsers"><label for="SendToPlexUsers">Send to all of your Plex 'Friends'</label>
}
<div class="form-group">
<label for="StoreCleanup" class="control-label">A comma separated list of email addresses you want the newsletter to go to (For users that are not in your Plex Friends)</label>
<div>
<input type="text" class="form-control form-control-custom " placeholder="email@address.com;second@address.com" id="StoreCleanup" name="StoreCleanup" value="@Model.CustomUsers">
</div>
</div>
</div> </div>
<button id="recentlyAddedBtn" class="btn btn-primary-outline">Send test email to Admin</button> <button id="recentlyAddedBtn" class="btn btn-primary-outline">Send test email to Admin</button>

@ -78,11 +78,12 @@
<input type="text" class="form-control form-control-custom " id="UserRequestLimitResetter" name="UserRequestLimitResetter" value="@Model.UserRequestLimitResetter"> <input type="text" class="form-control form-control-custom " id="UserRequestLimitResetter" name="UserRequestLimitResetter" value="@Model.UserRequestLimitResetter">
</div> </div>
</div> </div>
<small>Please note, this uses a Quartz CRON job, you can build a CRON <a href="http://www.cronmaker.com/">Here</a></small>
<div class="form-group"> <div class="form-group">
<label for="RecentlyAdded" class="control-label">Recently Added Email (hours)</label> <label for="RecentlyAddedCron" class="control-label">Recently Added Email (CRON)</label>
<div> <div>
<input type="text" class="form-control form-control-custom " id="RecentlyAdded" name="RecentlyAdded" value="@Model.RecentlyAdded"> <input type="text" class="form-control form-control-custom " id="RecentlyAddedCron" name="RecentlyAddedCron" value="@Model.RecentlyAddedCron">
</div> </div>
</div> </div>

@ -188,19 +188,20 @@
<form method="POST" action="@url/search/request/{{type}}" id="form{{id}}"> <form method="POST" action="@url/search/request/{{type}}" id="form{{id}}">
<input name="{{type}}Id" type="text" value="{{id}}" hidden="hidden" /> <input name="{{type}}Id" type="text" value="{{id}}" hidden="hidden" />
{{#if_eq type "movie"}} {{#if_eq type "movie"}}
{{#if_eq available true}} {{#if_eq available true}}
<button style="text-align: right" class="btn btn-success-outline disabled" disabled><i class="fa fa-check"></i> @UI.Search_Available</button> <button style="text-align: right" class="btn btn-success-outline disabled" disabled><i class="fa fa-check"></i> @UI.Search_Available</button>
<br /> <br />
<br /> <br />
<a style="text-align: right" class="btn btn-sm btn-primary-outline" href="{{url}}" target="_blank"><i class="fa fa-eye"></i> @UI.Search_ViewInPlex</a> <a style="text-align: right" class="btn btn-sm btn-primary-outline" href="{{url}}" target="_blank"><i class="fa fa-eye"></i> @UI.Search_ViewInPlex</a>
{{else}} {{else}}
{{#if_eq requested true}} {{#if_eq requested true}}
<button style="text-align: right" class="btn btn-primary-outline disabled" disabled><i class="fa fa-check"></i> @UI.Search_Requested</button> <button style="text-align: right" class="btn btn-primary-outline disabled" disabled><i class="fa fa-check"></i> @UI.Search_Requested</button>
{{else}} {{else}}
<button id="{{id}}" style="text-align: right" class="btn btn-primary-outline requestMovie" type="submit"><i class="fa fa-plus"></i> @UI.Search_Request</button> <button id="{{id}}" style="text-align: right" class="btn btn-primary-outline requestMovie" type="submit"><i class="fa fa-plus"></i> @UI.Search_Request</button>
{{/if_eq}} {{/if_eq}}
{{/if_eq}} {{/if_eq}}
{{/if_eq}} {{/if_eq}}
{{#if_eq type "tv"}} {{#if_eq type "tv"}}
{{#if_eq tvFullyAvailable true}} {{#if_eq tvFullyAvailable true}}
@*//TODO Not used yet*@ @*//TODO Not used yet*@

@ -116,8 +116,8 @@
<strong>Modify Roles:</strong> <strong>Modify Roles:</strong>
<!--Load all claims--> <!--Load all claims-->
<div class="checkbox" ng-repeat="claim in claims"> <div class="checkbox" ng-repeat="claim in selectedUser.claimsItem">
<input id="claimCheckboxEdit_{{$id}}" class="checkbox-custom" name="selectedClaims[]" ng-checked="@*//TODO: Need to figure our how to preselect them*@" ng-model="claim.selected" type="checkbox" value="claim" /> <input id="claimCheckboxEdit_{{$id}}" class="checkbox-custom" name="selectedClaims[]" ng-checked="claim.selected" ng-model="claim.selected" type="checkbox" value="claim" />
<label for="claimCheckboxEdit_{{$id}}">{{claim.name}}</label> <label for="claimCheckboxEdit_{{$id}}">{{claim.name}}</label>
</div> </div>

Loading…
Cancel
Save