User management, migration and newsletter

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

@ -25,39 +25,76 @@
// ************************************************************************/
#endregion
using System;
using System.Data;
using PlexRequests.Core.SettingModels;
using PlexRequests.Store;
using Quartz;
namespace PlexRequests.Core.Migration.Migrations
{
[Migration(1950, "v1.9.5.0")]
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;
NewsletterSettings = news;
Jobs = jobs;
}
public int Version => 1950;
private ISettingsService<PlexRequestSettings> PlexRequestSettings { get; }
private ISettingsService<NewletterSettings> NewsletterSettings { get; }
private ISettingsService<ScheduledJobsSettings> Jobs { get; }
public void Start(IDbConnection con)
{
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();
newsLetter.SendToPlexUsers = true;
UpdateScheduledSettings(jobSettings);
if (plex.SendRecentlyAddedEmail)
{
newsLetter.SendRecentlyAddedEmail = plex.SendRecentlyAddedEmail;
plex.SendRecentlyAddedEmail = false;
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>
</PropertyGroup>
<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">
<HintPath>..\Assemblies\Mono.Data.Sqlite.dll</HintPath>
</Reference>
<Reference Include="Ninject">
<HintPath>..\packages\Ninject.3.2.0.0\lib\net45-full\Ninject.dll</HintPath>
</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.Core" />
<Reference Include="System.Xml.Linq" />
@ -66,6 +78,13 @@
<Name>PlexRequests.Store</Name>
</ProjectReference>
</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" />
<!-- 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.

@ -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
using System.Collections.Generic;
using PlexRequests.Core.Models;
using PlexRequests.Core.Notification;
using Newtonsoft.Json;
using PlexRequests.Helpers;
namespace PlexRequests.Core.SettingModels
{
public class NewletterSettings : Settings
{
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.
// ************************************************************************/
#endregion
using System;
namespace PlexRequests.Core.SettingModels
{
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 SickRageCacher { get; set; }
public int SonarrCacher { get; set; }
@ -49,6 +39,8 @@ namespace PlexRequests.Core.SettingModels
public int StoreCleanup { get; set; }
public int UserRequestLimitResetter { get; set; }
public int PlexEpisodeCacher { get; set; }
[Obsolete("We use the CRON job now")]
public int RecentlyAdded { get; set; }
public string RecentlyAddedCron { get; set; }
}
}

@ -110,10 +110,9 @@ namespace PlexRequests.Core
Salt = salt,
Hash = PasswordHasher.ComputeHash(password, salt),
Claims = ByteConverterHelper.ReturnBytes(claims),
UserProperties = ByteConverterHelper.ReturnBytes(properties ?? new UserProperties())
UserProperties = ByteConverterHelper.ReturnBytes(properties ?? new UserProperties()),
};
Repo.Insert(userModel);
var userRecord = Repo.Get(userModel.UserGuid);
return new Guid(userRecord.UserGuid);

@ -24,6 +24,8 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
@ -64,5 +66,36 @@ namespace PlexRequests.Helpers
}
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 const string Admin = "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 User = "User"; // Can only request
public const string Admin = nameof(Admin); // Can do everything including creating new users and editing settings
public const string PowerUser = nameof(PowerUser); // Can only manage the requests, approve etc.
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
{
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);
}
protected virtual void AddImageInsideTable(ref StringBuilder sb, string url)
protected virtual void AddImageInsideTable(StringBuilder sb, string url)
{
sb.Append("<tr>");
sb.Append("<td align=\"center\">");
@ -49,17 +49,17 @@ namespace PlexRequests.Services.Jobs
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);
}
protected virtual void EndTag(ref StringBuilder sb, string tag)
protected virtual void EndTag(StringBuilder sb, string 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(
"<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 RecentlyAdded(IPlexApi api, ISettingsService<PlexSettings> plexSettings,
ISettingsService<EmailNotificationSettings> email,
ISettingsService<ScheduledJobsSettings> scheduledService, IJobRecord rec,
ISettingsService<EmailNotificationSettings> email, IJobRecord rec,
ISettingsService<NewletterSettings> newsletter,
IPlexReadOnlyDatabase db)
{
@ -60,7 +59,6 @@ namespace PlexRequests.Services.Jobs
Api = api;
PlexSettings = plexSettings;
EmailSettings = email;
ScheduledJobsSettings = scheduledService;
NewsletterSettings = newsletter;
PlexDb = db;
}
@ -73,7 +71,6 @@ namespace PlexRequests.Services.Jobs
private ISettingsService<PlexSettings> PlexSettings { get; }
private ISettingsService<EmailNotificationSettings> EmailSettings { get; }
private ISettingsService<NewletterSettings> NewsletterSettings { get; }
private ISettingsService<ScheduledJobsSettings> ScheduledJobsSettings { get; }
private IJobRecord JobRecord { get; }
private IPlexReadOnlyDatabase PlexDb { get; }
@ -88,19 +85,8 @@ namespace PlexRequests.Services.Jobs
{
return;
}
var jobs = JobRecord.GetJobs();
var thisJob =
jobs.FirstOrDefault(
x => x.Name.Equals(JobNames.RecentlyAddedEmail, StringComparison.CurrentCultureIgnoreCase));
var jobSettings = ScheduledJobsSettings.GetSettings();
if (thisJob?.LastRun > DateTime.Now.AddHours(-jobSettings.RecentlyAdded))
{
return;
}
Start();
Start(settings);
}
catch (Exception e)
{
@ -114,10 +100,11 @@ namespace PlexRequests.Services.Jobs
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 plexSettings = PlexSettings.GetSettings();
@ -129,16 +116,16 @@ namespace PlexRequests.Services.Jobs
var recentlyAddedTv = Api.RecentlyAdded(plexSettings.PlexAuthToken, plexSettings.FullUri, tvSection.Key);
var recentlyAddedMovies = Api.RecentlyAdded(plexSettings.PlexAuthToken, plexSettings.FullUri, movieSection.Key);
GenerateMovieHtml(recentlyAddedMovies, plexSettings, ref sb);
GenerateTvHtml(recentlyAddedTv, plexSettings, ref sb);
GenerateMovieHtml(recentlyAddedMovies, plexSettings, sb);
GenerateTvHtml(recentlyAddedTv, plexSettings, sb);
var template = new RecentlyAddedTemplate();
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(
@ -156,94 +143,41 @@ namespace PlexRequests.Services.Jobs
var imdbId = PlexHelper.GetProviderIdFromPlexGuid(plexGUID);
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(
"<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}/");
Header(ref sb, 3, $"{info.Title} {info.ReleaseDate?.ToString("yyyy") ?? string.Empty}");
EndTag(ref sb, "a");
Href(sb, $"https://www.imdb.com/title/{info.ImdbId}/");
Header(sb, 3, $"{info.Title} {info.ReleaseDate?.ToString("yyyy") ?? string.Empty}");
EndTag(sb, "a");
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);
EndLoopHtml(ref sb);
AddParagraph(sb, info.Overview);
}
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);
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
finally
{
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)}");
}
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);
EndLoopHtml(sb);
}
}
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
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
}
sb.Append("<tr>");
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);
AddImageInsideTable(sb, banner);
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("<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 = !string.IsNullOrEmpty(t.SeriesTitle)
? $"{t.SeriesTitle} - {t.title} {t.originally_available_at:yyyy}"
: $"{t.title}";
sb.Append(
"<td align=\"center\" style=\"font-family: sans-serif; font-size: 14px; vertical-align: top;\" valign=\"top\">");
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);
var title = $"{t.grandparentTitle} - {t.title} {t.originallyAvailableAt?.Substring(0, 4)}";
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())
{
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()));
AddParagraph(sb, $"Genre: {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>");
AddParagraph(sb, string.IsNullOrEmpty(t.summary) ? info.summary : t.summary);
}
catch (Exception 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/>");
}
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();
@ -395,13 +254,24 @@ namespace PlexRequests.Services.Jobs
if (!testEmail)
{
var users = Api.GetUsers(plexSettings.PlexAuthToken);
foreach (var user in users.User)
if (newletterSettings.SendToPlexUsers)
{
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));
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("<hr>");

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

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

@ -22,7 +22,10 @@
// Select a user to populate on the right side
$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
@ -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}',
function (nv) {
$scope.selectedClaims = nv.map(function (claim) {
@ -74,7 +86,7 @@
$scope.updateUser = function () {
userManagementService.updateUser($scope.selectedUser.id, $scope.selectedUser.claimsItem);
}
function getBaseUrl() {

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

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

@ -1,50 +1,51 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: SearchTvShowViewModel.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System;
using System.Collections.Generic;
namespace PlexRequests.UI.Models
{
public class SearchMovieViewModel : SearchViewModel
{
public bool Adult { get; set; }
public string BackdropPath { get; set; }
public List<int> GenreIds { get; set; }
public int Id { get; set; }
public string OriginalLanguage { get; set; }
public string OriginalTitle { get; set; }
public string Overview { get; set; }
public double Popularity { get; set; }
public string PosterPath { get; set; }
public DateTime? ReleaseDate { get; set; }
public string Title { get; set; }
public bool Video { get; set; }
public double VoteAverage { get; set; }
public int VoteCount { get; set; }
}
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: SearchTvShowViewModel.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System;
using System.Collections.Generic;
namespace PlexRequests.UI.Models
{
public class SearchMovieViewModel : SearchViewModel
{
public bool Adult { get; set; }
public string BackdropPath { get; set; }
public List<int> GenreIds { get; set; }
public int Id { get; set; }
public string OriginalLanguage { get; set; }
public string OriginalTitle { get; set; }
public string Overview { get; set; }
public double Popularity { get; set; }
public string PosterPath { get; set; }
public DateTime? ReleaseDate { get; set; }
public string Title { get; set; }
public bool Video { get; set; }
public double VoteAverage { 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 UserManagementPlexInformation PlexInfo { get; set; }
public string[] ClaimsArray { get; set; }
public List<UserManagementUpdateModel.ClaimsModel> ClaimsItem { get; set; }
}
public class UserManagementPlexInformation
@ -57,5 +58,22 @@ namespace PlexRequests.UI.Models
[JsonProperty("email")]
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.UI.Helpers;
using PlexRequests.UI.Models;
using Quartz;
using Action = PlexRequests.Helpers.Analytics.Action;
namespace PlexRequests.UI.Modules
@ -97,7 +97,7 @@ namespace PlexRequests.UI.Modules
private IJobRecord JobRecorder { get; }
private IAnalytics Analytics { get; }
private IRecentlyAdded RecentlyAdded { get; }
private ISettingsService<NotificationSettingsV2> NotifySettings { get; }
private ISettingsService<NotificationSettingsV2> NotifySettings { get; }
private static Logger Log = LogManager.GetCurrentClassLogger();
public AdminModule(ISettingsService<PlexRequestSettings> prService,
@ -210,7 +210,7 @@ namespace PlexRequests.UI.Modules
Post["/autoupdate"] = x => AutoUpdate();
Post["/testslacknotification", true] = async (x,ct) => await TestSlackNotification();
Post["/testslacknotification", true] = async (x, ct) => await TestSlackNotification();
Get["/slacknotification"] = _ => SlackNotifications();
Post["/slacknotification"] = _ => SaveSlackNotifications();
@ -975,7 +975,8 @@ namespace PlexRequests.UI.Modules
SonarrCacher = s.SonarrCacher,
StoreBackup = s.StoreBackup,
StoreCleanup = s.StoreCleanup,
JobRecorder = jobsDict
JobRecorder = jobsDict,
RecentlyAddedCron = s.RecentlyAddedCron
};
return View["SchedulerSettings", model];
}
@ -986,6 +987,21 @@ namespace PlexRequests.UI.Modules
Analytics.TrackEventAsync(Category.Admin, Action.Update, "Update ScheduledJobs", Username, CookieHelper.GetAnalyticClientId(Cookies));
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);
return Response.AsJson(result

@ -35,6 +35,7 @@ namespace PlexRequests.UI.Modules
Get["/local/{id}"] = x => LocalDetails((Guid)x.id);
Get["/plex/{id}", true] = async (x, ct) => await PlexDetails(x.id);
Get["/claims"] = x => GetClaims();
Post["/updateuser"] = x => UpdateUser();
}
private ICustomUserMapper UserMapper { get; }
@ -57,15 +58,35 @@ namespace PlexRequests.UI.Modules
var userProps = ByteConverterHelper.ReturnObject<UserProperties>(user.UserProperties);
model.Add(new UserManagementUsersViewModel
var m = new UserManagementUsersViewModel
{
Id = user.UserGuid,
Claims = claimsString,
Username = user.UserName,
Type = UserType.LocalUser,
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();
@ -121,6 +142,44 @@ namespace PlexRequests.UI.Modules
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)
{
var localUser = UserMapper.GetUser(id);

@ -48,30 +48,41 @@ namespace PlexRequests.UI
{
try
{
Debug.WriteLine("Starting StartupConfiguration");
Debug.WriteLine("Starting StartupConfiguration");
var resolver = new DependancyResolver();
Debug.WriteLine("Created DI Resolver");
Debug.WriteLine("Created DI Resolver");
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);
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>();
Debug.WriteLine("Start the bootstrapper with the Kernel.ı");
app.UseNancy(options => options.Bootstrapper = new Bootstrapper(kernel));
Debug.WriteLine("Finished bootstrapper");
var scheduler = new Scheduler();
scheduler.StartScheduler();
Debug.WriteLine("Start the bootstrapper with the Kernel.");
app.UseNancy(options => options.Bootstrapper = new Bootstrapper(kernel));
Debug.WriteLine("Finished bootstrapper");
Debug.WriteLine("Migrating DB Now");
var runner = kernel.Get<IMigrationRunner>();
runner.MigrateToLatest();
Debug.WriteLine("Settings up Scheduler");
var scheduler = new Scheduler();
scheduler.StartScheduler();
//var c = kernel.Get<IRecentlyAdded>();
//c.Test();
}
catch (Exception exception)
{

@ -16,12 +16,30 @@
<small>Note: This will require you to setup your email notifications</small>
@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
{
<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>
<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">
</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">
<label for="RecentlyAdded" class="control-label">Recently Added Email (hours)</label>
<label for="RecentlyAddedCron" class="control-label">Recently Added Email (CRON)</label>
<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>

@ -188,19 +188,20 @@
<form method="POST" action="@url/search/request/{{type}}" id="form{{id}}">
<input name="{{type}}Id" type="text" value="{{id}}" hidden="hidden" />
{{#if_eq type "movie"}}
{{#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>
<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>
{{else}}
{{#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>
{{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>
{{/if_eq}}
{{/if_eq}}
{{#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>
<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>
{{else}}
{{#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>
{{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>
{{/if_eq}}
{{/if_eq}}
{{/if_eq}}
{{#if_eq type "tv"}}
{{#if_eq tvFullyAvailable true}}
@*//TODO Not used yet*@

@ -116,8 +116,8 @@
<strong>Modify Roles:</strong>
<!--Load all claims-->
<div class="checkbox" ng-repeat="claim in claims">
<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" />
<div class="checkbox" ng-repeat="claim in selectedUser.claimsItem">
<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>
</div>

Loading…
Cancel
Save