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)
{ {
var plex = PlexRequestSettings.GetSettings(); UpdateApplicationSettings();
UpdateDb(con);
UpdateSchema(con, Version);
}
private void UpdateDb(IDbConnection con)
{
}
private void UpdateApplicationSettings()
{
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); NewsletterSettings.SaveSettings(newsLetter);
Jobs.SaveSettings(jobSettings);
} }
UpdateSchema(con, Version); 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 =
jobs.FirstOrDefault(
x => x.Name.Equals(JobNames.RecentlyAddedEmail, StringComparison.CurrentCultureIgnoreCase));
var jobSettings = ScheduledJobsSettings.GetSettings(); Start(settings);
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);
}
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;
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)}");
} }
finally
AddParagraph(ref sb, movie.summary);
EndLoopHtml(ref sb);
}
catch (Exception e)
{ {
Log.Error(e); EndLoopHtml(sb);
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
sb.Append("<td"); AddParagraph(sb, string.IsNullOrEmpty(t.summary) ? info.summary : t.summary);
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();
@ -394,6 +253,8 @@ namespace PlexRequests.Services.Jobs
}; };
if (!testEmail) if (!testEmail)
{
if (newletterSettings.SendToPlexUsers)
{ {
var users = Api.GetUsers(plexSettings.PlexAuthToken); var users = Api.GetUsers(plexSettings.PlexAuthToken);
foreach (var user in users.User) foreach (var user in users.User)
@ -401,7 +262,16 @@ namespace PlexRequests.Services.Jobs
message.Bcc.Add(new MailboxAddress(user.Username, user.Email)); message.Bcc.Add(new MailboxAddress(user.Username, user.Email));
} }
} }
message.Bcc.Add(new MailboxAddress(settings.EmailUsername, settings.EmailSender)); // Include the admin
if (newletterSettings.CustomUsersEmailAddresses.Any())
{
foreach (var user in newletterSettings.CustomUsersEmailAddresses)
{
message.Bcc.Add(new MailboxAddress(user, user));
}
}
}
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)
{ {

@ -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);
@ -155,24 +162,34 @@ namespace PlexRequests.UI.Jobs
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))
// Everything has started on application start, lets wait 5 minutes
.WithSimpleSchedule(x => x.WithIntervalInHours(s.UserRequestLimitResetter).RepeatForever()) .WithSimpleSchedule(x => x.WithIntervalInHours(s.UserRequestLimitResetter).RepeatForever())
.Build(); .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 = var rencentlyAdded =
TriggerBuilder.Create() TriggerBuilder.Create()
.WithIdentity("RecentlyAddedModel", "Email") .WithIdentity("RecentlyAddedModel", "Email")
.StartNow() .StartNow()
.WithCronSchedule(s.RecentlyAddedCron)
.WithSimpleSchedule(x => x.WithIntervalInHours(2).RepeatForever()) .WithSimpleSchedule(x => x.WithIntervalInHours(2).RepeatForever())
.Build(); .Build();
triggers.Add(rencentlyAdded);
}
triggers.Add(plexAvailabilityChecker); triggers.Add(plexAvailabilityChecker);
triggers.Add(srCacher); triggers.Add(srCacher);
@ -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;
} }

@ -46,5 +46,6 @@ namespace PlexRequests.UI.Models
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
@ -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);

@ -63,15 +63,26 @@ namespace PlexRequests.UI
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,13 +16,31 @@
<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>
</div> </div>

@ -79,10 +79,11 @@
</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>

@ -201,6 +201,7 @@
{{/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