feat: added the watchlist notification

This commit is contained in:
Jamie Rees 2025-05-14 21:34:05 +01:00 committed by GitHub
commit ea0b690c18
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 1210 additions and 2545 deletions

File diff suppressed because it is too large Load diff

440
src/.idea/.idea.Ombi/.idea/dbnavigator.xml generated Normal file
View file

@ -0,0 +1,440 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DBNavigator.Project.DDLFileAttachmentManager">
<mappings />
<preferences />
</component>
<component name="DBNavigator.Project.DatabaseAssistantManager">
<assistants />
</component>
<component name="DBNavigator.Project.DatabaseBrowserManager">
<autoscroll-to-editor value="false" />
<autoscroll-from-editor value="true" />
<show-object-properties value="true" />
<loaded-nodes />
</component>
<component name="DBNavigator.Project.DatabaseFileManager">
<open-files />
</component>
<component name="DBNavigator.Project.ExecutionManager">
<retain-sticky-names value="false" />
</component>
<component name="DBNavigator.Project.ParserDiagnosticsManager">
<diagnostics-history />
</component>
<component name="DBNavigator.Project.Settings">
<connections />
<browser-settings>
<general>
<display-mode value="TABBED" />
<navigation-history-size value="100" />
<show-object-details value="false" />
<enable-sticky-paths value="true" />
</general>
<filters>
<object-type-filter>
<object-type name="SCHEMA" enabled="true" />
<object-type name="USER" enabled="true" />
<object-type name="ROLE" enabled="true" />
<object-type name="PRIVILEGE" enabled="true" />
<object-type name="CHARSET" enabled="true" />
<object-type name="TABLE" enabled="true" />
<object-type name="VIEW" enabled="true" />
<object-type name="MATERIALIZED_VIEW" enabled="true" />
<object-type name="NESTED_TABLE" enabled="true" />
<object-type name="COLUMN" enabled="true" />
<object-type name="INDEX" enabled="true" />
<object-type name="CONSTRAINT" enabled="true" />
<object-type name="DATASET_TRIGGER" enabled="true" />
<object-type name="DATABASE_TRIGGER" enabled="true" />
<object-type name="SYNONYM" enabled="true" />
<object-type name="SEQUENCE" enabled="true" />
<object-type name="PROCEDURE" enabled="true" />
<object-type name="FUNCTION" enabled="true" />
<object-type name="PACKAGE" enabled="true" />
<object-type name="TYPE" enabled="true" />
<object-type name="TYPE_ATTRIBUTE" enabled="true" />
<object-type name="ARGUMENT" enabled="true" />
<object-type name="JAVA_CLASS" enabled="true" />
<object-type name="JAVA_INNER_CLASS" enabled="true" />
<object-type name="JAVA_FIELD" enabled="true" />
<object-type name="JAVA_METHOD" enabled="true" />
<object-type name="DIMENSION" enabled="true" />
<object-type name="CLUSTER" enabled="true" />
<object-type name="DBLINK" enabled="true" />
<object-type name="CREDENTIAL" enabled="true" />
<object-type name="AI_PROFILE" enabled="true" />
</object-type-filter>
</filters>
<sorting>
<object-type name="COLUMN" sorting-type="NAME" />
<object-type name="FUNCTION" sorting-type="NAME" />
<object-type name="PROCEDURE" sorting-type="NAME" />
<object-type name="ARGUMENT" sorting-type="POSITION" />
<object-type name="TYPE ATTRIBUTE" sorting-type="POSITION" />
</sorting>
<default-editors>
<object-type name="VIEW" editor-type="SELECTION" />
<object-type name="PACKAGE" editor-type="SELECTION" />
<object-type name="TYPE" editor-type="SELECTION" />
</default-editors>
</browser-settings>
<navigation-settings>
<lookup-filters>
<lookup-objects>
<object-type name="SCHEMA" enabled="true" />
<object-type name="USER" enabled="false" />
<object-type name="ROLE" enabled="false" />
<object-type name="PRIVILEGE" enabled="false" />
<object-type name="CHARSET" enabled="false" />
<object-type name="TABLE" enabled="true" />
<object-type name="VIEW" enabled="true" />
<object-type name="MATERIALIZED VIEW" enabled="true" />
<object-type name="INDEX" enabled="true" />
<object-type name="CONSTRAINT" enabled="true" />
<object-type name="DATASET TRIGGER" enabled="true" />
<object-type name="DATABASE TRIGGER" enabled="true" />
<object-type name="SYNONYM" enabled="false" />
<object-type name="SEQUENCE" enabled="true" />
<object-type name="PROCEDURE" enabled="true" />
<object-type name="FUNCTION" enabled="true" />
<object-type name="PACKAGE" enabled="true" />
<object-type name="TYPE" enabled="true" />
<object-type name="JAVA CLASS" enabled="true" />
<object-type name="INNER CLASS" enabled="true" />
<object-type name="JAVA FIELD" enabled="true" />
<object-type name="JAVA METHOD" enabled="true" />
<object-type name="JAVA PARAMETER" enabled="true" />
<object-type name="DIMENSION" enabled="false" />
<object-type name="CLUSTER" enabled="false" />
<object-type name="DBLINK" enabled="false" />
<object-type name="CREDENTIAL" enabled="false" />
</lookup-objects>
<force-database-load value="false" />
<prompt-connection-selection value="true" />
<prompt-schema-selection value="true" />
</lookup-filters>
</navigation-settings>
<dataset-grid-settings>
<general>
<enable-zooming value="true" />
<enable-column-tooltip value="true" />
</general>
<sorting>
<nulls-first value="true" />
<max-sorting-columns value="4" />
</sorting>
<audit-columns>
<column-names value="" />
<visible value="true" />
<editable value="false" />
</audit-columns>
</dataset-grid-settings>
<dataset-editor-settings>
<text-editor-popup>
<active value="false" />
<active-if-empty value="false" />
<data-length-threshold value="100" />
<popup-delay value="1000" />
</text-editor-popup>
<values-actions-popup>
<show-popup-button value="true" />
<element-count-threshold value="1000" />
<data-length-threshold value="250" />
</values-actions-popup>
<general>
<fetch-block-size value="100" />
<fetch-timeout value="30" />
<trim-whitespaces value="true" />
<convert-empty-strings-to-null value="true" />
<select-content-on-cell-edit value="true" />
<large-value-preview-active value="true" />
</general>
<filters>
<prompt-filter-dialog value="true" />
<default-filter-type value="BASIC" />
</filters>
<qualified-text-editor text-length-threshold="300">
<content-types>
<content-type name="Text" enabled="true" />
<content-type name="Properties" enabled="true" />
<content-type name="XML" enabled="true" />
<content-type name="DTD" enabled="true" />
<content-type name="HTML" enabled="true" />
<content-type name="XHTML" enabled="true" />
<content-type name="CSS" enabled="true" />
<content-type name="SQL" enabled="true" />
<content-type name="PL/SQL" enabled="true" />
<content-type name="JavaScript" enabled="true" />
<content-type name="JSON" enabled="true" />
<content-type name="JSON5" enabled="true" />
<content-type name="YAML" enabled="true" />
<content-type name="C#" enabled="true" />
</content-types>
</qualified-text-editor>
<record-navigation>
<navigation-target value="VIEWER" />
</record-navigation>
</dataset-editor-settings>
<code-editor-settings>
<general>
<show-object-navigation-gutter value="false" />
<show-spec-declaration-navigation-gutter value="true" />
<enable-spellchecking value="true" />
<enable-reference-spellchecking value="false" />
</general>
<confirmations>
<save-changes value="false" />
<revert-changes value="true" />
<exit-on-changes value="ASK" />
</confirmations>
</code-editor-settings>
<code-completion-settings>
<filters>
<basic-filter>
<filter-element type="RESERVED_WORD" id="keyword" selected="true" />
<filter-element type="RESERVED_WORD" id="function" selected="true" />
<filter-element type="RESERVED_WORD" id="parameter" selected="true" />
<filter-element type="RESERVED_WORD" id="datatype" selected="true" />
<filter-element type="RESERVED_WORD" id="exception" selected="true" />
<filter-element type="OBJECT" id="schema" selected="true" />
<filter-element type="OBJECT" id="role" selected="true" />
<filter-element type="OBJECT" id="user" selected="true" />
<filter-element type="OBJECT" id="privilege" selected="true" />
<user-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="false" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</user-schema>
<public-schema>
<filter-element type="OBJECT" id="table" selected="false" />
<filter-element type="OBJECT" id="view" selected="false" />
<filter-element type="OBJECT" id="materialized view" selected="false" />
<filter-element type="OBJECT" id="index" selected="false" />
<filter-element type="OBJECT" id="constraint" selected="false" />
<filter-element type="OBJECT" id="trigger" selected="false" />
<filter-element type="OBJECT" id="synonym" selected="false" />
<filter-element type="OBJECT" id="sequence" selected="false" />
<filter-element type="OBJECT" id="procedure" selected="false" />
<filter-element type="OBJECT" id="function" selected="false" />
<filter-element type="OBJECT" id="package" selected="false" />
<filter-element type="OBJECT" id="type" selected="false" />
<filter-element type="OBJECT" id="dimension" selected="false" />
<filter-element type="OBJECT" id="cluster" selected="false" />
<filter-element type="OBJECT" id="dblink" selected="false" />
</public-schema>
<any-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="true" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</any-schema>
</basic-filter>
<extended-filter>
<filter-element type="RESERVED_WORD" id="keyword" selected="true" />
<filter-element type="RESERVED_WORD" id="function" selected="true" />
<filter-element type="RESERVED_WORD" id="parameter" selected="true" />
<filter-element type="RESERVED_WORD" id="datatype" selected="true" />
<filter-element type="RESERVED_WORD" id="exception" selected="true" />
<filter-element type="OBJECT" id="schema" selected="true" />
<filter-element type="OBJECT" id="user" selected="true" />
<filter-element type="OBJECT" id="role" selected="true" />
<filter-element type="OBJECT" id="privilege" selected="true" />
<user-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="true" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</user-schema>
<public-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="true" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</public-schema>
<any-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="true" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</any-schema>
</extended-filter>
</filters>
<sorting enabled="true">
<sorting-element type="RESERVED_WORD" id="keyword" />
<sorting-element type="RESERVED_WORD" id="datatype" />
<sorting-element type="OBJECT" id="column" />
<sorting-element type="OBJECT" id="table" />
<sorting-element type="OBJECT" id="view" />
<sorting-element type="OBJECT" id="materialized view" />
<sorting-element type="OBJECT" id="index" />
<sorting-element type="OBJECT" id="constraint" />
<sorting-element type="OBJECT" id="trigger" />
<sorting-element type="OBJECT" id="synonym" />
<sorting-element type="OBJECT" id="sequence" />
<sorting-element type="OBJECT" id="procedure" />
<sorting-element type="OBJECT" id="function" />
<sorting-element type="OBJECT" id="package" />
<sorting-element type="OBJECT" id="type" />
<sorting-element type="OBJECT" id="dimension" />
<sorting-element type="OBJECT" id="cluster" />
<sorting-element type="OBJECT" id="dblink" />
<sorting-element type="OBJECT" id="schema" />
<sorting-element type="OBJECT" id="role" />
<sorting-element type="OBJECT" id="user" />
<sorting-element type="RESERVED_WORD" id="function" />
<sorting-element type="RESERVED_WORD" id="parameter" />
</sorting>
<format>
<enforce-code-style-case value="true" />
</format>
</code-completion-settings>
<execution-engine-settings>
<statement-execution>
<fetch-block-size value="100" />
<execution-timeout value="20" />
<debug-execution-timeout value="600" />
<focus-result value="false" />
<prompt-execution value="false" />
</statement-execution>
<script-execution>
<command-line-interfaces />
<execution-timeout value="300" />
</script-execution>
<method-execution>
<execution-timeout value="30" />
<debug-execution-timeout value="600" />
<parameter-history-size value="10" />
</method-execution>
</execution-engine-settings>
<operation-settings>
<transactions>
<uncommitted-changes>
<on-project-close value="ASK" />
<on-disconnect value="ASK" />
<on-autocommit-toggle value="ASK" />
</uncommitted-changes>
<multiple-uncommitted-changes>
<on-commit value="ASK" />
<on-rollback value="ASK" />
</multiple-uncommitted-changes>
</transactions>
<session-browser>
<disconnect-session value="ASK" />
<kill-session value="ASK" />
<reload-on-filter-change value="false" />
</session-browser>
<compiler>
<compile-type value="KEEP" />
<compile-dependencies value="ASK" />
<always-show-controls value="false" />
</compiler>
</operation-settings>
<ddl-file-settings>
<extensions>
<mapping file-type-id="VIEW" extensions="vw" />
<mapping file-type-id="TRIGGER" extensions="trg" />
<mapping file-type-id="PROCEDURE" extensions="prc" />
<mapping file-type-id="FUNCTION" extensions="fnc" />
<mapping file-type-id="PACKAGE" extensions="pkg" />
<mapping file-type-id="PACKAGE_SPEC" extensions="pks" />
<mapping file-type-id="PACKAGE_BODY" extensions="pkb" />
<mapping file-type-id="TYPE" extensions="tpe" />
<mapping file-type-id="TYPE_SPEC" extensions="tps" />
<mapping file-type-id="TYPE_BODY" extensions="tpb" />
<mapping file-type-id="JAVA_SOURCE" extensions="sql" />
</extensions>
<general>
<lookup-ddl-files value="true" />
<create-ddl-files value="false" />
<synchronize-ddl-files value="true" />
<use-qualified-names value="false" />
<make-scripts-rerunnable value="true" />
</general>
</ddl-file-settings>
<assistant-settings>
<credential-settings>
<credentials />
</credential-settings>
</assistant-settings>
<general-settings>
<regional-settings>
<date-format value="MEDIUM" />
<number-format value="UNGROUPED" />
<locale value="SYSTEM_DEFAULT" />
<use-custom-formats value="false" />
</regional-settings>
<environment>
<environment-types>
<environment-type id="development" name="Development" description="Development environment" color="-2430209/-12296320" readonly-code="false" readonly-data="false" />
<environment-type id="integration" name="Integration" description="Integration environment" color="-2621494/-12163514" readonly-code="true" readonly-data="false" />
<environment-type id="production" name="Production" description="Productive environment" color="-11574/-10271420" readonly-code="true" readonly-data="true" />
<environment-type id="other" name="Other" description="" color="-1576/-10724543" readonly-code="false" readonly-data="false" />
</environment-types>
<visibility-settings>
<connection-tabs value="true" />
<dialog-headers value="true" />
<object-editor-tabs value="true" />
<script-editor-tabs value="false" />
<execution-result-tabs value="true" />
</visibility-settings>
</environment>
</general-settings>
</component>
</project>

View file

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/.idea.Ombi/riderModule.iml" filepath="$PROJECT_DIR$/.idea/.idea.Ombi/riderModule.iml" />
</modules>
</component>
</project>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RiderProjectSettingsUpdater">
<option name="singleClickDiffPreview" value="1" />
<option name="vcsConfiguration" value="3" />
</component>
</project>

View file

@ -5,9 +5,7 @@
</component>
<component name="ChangeListManager">
<list default="true" id="57001998-efde-494a-80b3-d7acfc91f770" name="Default Changelist" comment="">
<change beforePath="$PROJECT_DIR$/.idea/.idea.Ombi/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/.idea.Ombi/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Ombi/ClientApp/src/app/wizard/welcome/welcome.component.html" beforeDir="false" afterPath="$PROJECT_DIR$/Ombi/ClientApp/src/app/wizard/welcome/welcome.component.html" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Ombi/Controllers/V2/WizardController.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Ombi/Controllers/V2/WizardController.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Ombi/ClientApp/src/app/settings/plex/plex.component.html" beforeDir="false" afterPath="$PROJECT_DIR$/Ombi/ClientApp/src/app/settings/plex/plex.component.html" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@ -234,6 +232,12 @@
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$/.." />
</component>
<component name="GitHubPullRequestSearchHistory">{
&quot;lastFilter&quot;: {
&quot;state&quot;: &quot;OPEN&quot;,
&quot;assignee&quot;: &quot;tidusjar&quot;
}
}</component>
<component name="GitToolBoxStore">
<option name="recentBranches">
<RecentBranches>
@ -267,6 +271,12 @@
</list>
</option>
</component>
<component name="GithubPullRequestsUISettings">{
&quot;selectedUrlAndAccountId&quot;: {
&quot;url&quot;: &quot;https://github.com/ombi-app/ombi&quot;,
&quot;accountId&quot;: &quot;22dd09fe-fb9e-48a4-bfcc-3c152edf3f25&quot;
}
}</component>
<component name="HighlightingSettingsPerFile">
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/990126b794024fe2bd16aebdd37eba1d7b600/93/25662f04/ServerVersion.cs" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/3bd4df5aff92cabbc4d630be64227073db1b8539b3a1e47786b4b189d7cdb7/DbContext.cs" root0="FORCE_HIGHLIGHTING" />
@ -308,12 +318,17 @@
<component name="PackageJsonUpdateNotifier">
<dismissed value="$PROJECT_DIR$/Ombi/ClientApp/package.json" />
</component>
<component name="ProjectColorInfo">{
&quot;customColor&quot;: &quot;&quot;,
&quot;associatedIndex&quot;: 0
}</component>
<component name="ProjectFrameBounds" extendedState="6">
<option name="x" value="1087" />
<option name="y" value="-1113" />
<option name="width" value="1400" />
<option name="height" value="1000" />
</component>
<component name="ProjectId" id="2wGwbN5gDqLwyiO1WJdlwJzZ5M9" />
<component name="ProjectLevelVcsManager" settingsEditedManually="true">
<ConfirmationsSetting value="2" id="Add" />
</component>
@ -376,11 +391,23 @@
<pane id="FileSystemExplorer" />
</panes>
</component>
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
".NET Launch Settings Profile.Ombi.executor": "Run",
"git-widget-placeholder": "#5208 on wizard-database",
"node.js.selected.package.tslint": "(autodetect)"
".NET Launch Settings Profile.Ombi.Schedule.Tests.executor": "Run",
".NET Launch Settings Profile.Ombi.executor": "Debug",
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.git.unshallow": "true",
"fb34c741-04ca-4b4f-8ea1-651a011b42c8.executor": "Debug",
"git-widget-placeholder": "watchlist-expired-notification",
"node.js.detected.package.eslint": "true",
"node.js.selected.package.eslint": "(autodetect)",
"node.js.selected.package.tslint": "(autodetect)",
"nodejs_package_manager_path": "yarn",
"vue.rearranger.settings.migration": "true"
}
}]]></component>
<component name="RunManager" selected=".NET Launch Settings Profile.Ombi">
@ -460,6 +487,7 @@
</method>
</configuration>
</component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="57001998-efde-494a-80b3-d7acfc91f770" name="Default Changelist" comment="" />
@ -468,6 +496,9 @@
<option name="presentableId" value="Default" />
<updated>1563957157468</updated>
<workItem from="1563957162999" duration="5401000" />
<workItem from="1745681294313" duration="1814000" />
<workItem from="1747080279165" duration="838000" />
<workItem from="1747082180432" duration="1994000" />
</task>
<servers />
</component>
@ -512,7 +543,13 @@
<window_info anchor="right" content_ui="combo" id="Hierarchy" order="4" weight="0.25" />
</layout>
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
<component name="UnityProjectConfiguration" hasMinimizedUI="false" />
<component name="VcsManagerConfiguration">
<option name="CLEAR_INITIAL_COMMIT_MESSAGE" value="true" />
</component>
<component name="XDebuggerManager">
<breakpoint-manager>
<breakpoints>
@ -536,12 +573,12 @@
<line-breakpoint enabled="true" type="DotNet Breakpoints">
<url>file://$PROJECT_DIR$/Ombi.Core/Engine/V2/MultiSearchEngine.cs</url>
<line>59</line>
<properties documentPath="$PROJECT_DIR$/Ombi.Core/Engine/V2/MultiSearchEngine.cs">
<properties documentPath="$PROJECT_DIR$/Ombi.Core/Engine/V2/MultiSearchEngine.cs" containingFunctionPresentation="Method 'MultiSearch'">
<startOffsets>
<option value="2276" />
<option value="2369" />
</startOffsets>
<endOffsets>
<option value="2316" />
<option value="2576" />
</endOffsets>
</properties>
<option name="timeStamp" value="4" />
@ -549,12 +586,12 @@
<line-breakpoint enabled="true" type="DotNet Breakpoints">
<url>file://$PROJECT_DIR$/Ombi.Core/Engine/V2/MultiSearchEngine.cs</url>
<line>49</line>
<properties documentPath="$PROJECT_DIR$/Ombi.Core/Engine/V2/MultiSearchEngine.cs">
<properties documentPath="$PROJECT_DIR$/Ombi.Core/Engine/V2/MultiSearchEngine.cs" containingFunctionPresentation="Method 'MultiSearch'">
<startOffsets>
<option value="2001" />
<option value="1903" />
</startOffsets>
<endOffsets>
<option value="2002" />
<option value="1945" />
</endOffsets>
</properties>
<option name="timeStamp" value="5" />
@ -562,16 +599,55 @@
<line-breakpoint enabled="true" type="DotNet Breakpoints">
<url>file://$PROJECT_DIR$/Ombi.Api.MusicBrainz/MusicBrainzApi.cs</url>
<line>30</line>
<properties documentPath="$PROJECT_DIR$/Ombi.Api.MusicBrainz/MusicBrainzApi.cs">
<properties documentPath="$PROJECT_DIR$/Ombi.Api.MusicBrainz/MusicBrainzApi.cs" containingFunctionPresentation="Method 'SearchArtist'">
<startOffsets>
<option value="917" />
<option value="833" />
</startOffsets>
<endOffsets>
<option value="1016" />
<option value="834" />
</endOffsets>
</properties>
<option name="timeStamp" value="7" />
</line-breakpoint>
<line-breakpoint enabled="true" type="DotNet Breakpoints">
<url>file://$PROJECT_DIR$/Ombi.Schedule/Jobs/Plex/PlexWatchlistImport.cs</url>
<line>110</line>
<properties documentPath="$PROJECT_DIR$/Ombi.Schedule/Jobs/Plex/PlexWatchlistImport.cs" containingFunctionPresentation="Method 'Execute'">
<startOffsets>
<option value="5123" />
</startOffsets>
<endOffsets>
<option value="5206" />
</endOffsets>
</properties>
<option name="timeStamp" value="10" />
</line-breakpoint>
<line-breakpoint enabled="true" type="DotNet Breakpoints">
<url>file://$PROJECT_DIR$/Ombi.Schedule/Jobs/Plex/PlexWatchlistImport.cs</url>
<line>77</line>
<properties documentPath="$PROJECT_DIR$/Ombi.Schedule/Jobs/Plex/PlexWatchlistImport.cs" containingFunctionPresentation="Method 'Execute'">
<startOffsets>
<option value="3324" />
</startOffsets>
<endOffsets>
<option value="3365" />
</endOffsets>
</properties>
<option name="timeStamp" value="11" />
</line-breakpoint>
<line-breakpoint enabled="true" type="DotNet Breakpoints">
<url>file://$PROJECT_DIR$/Ombi.Schedule/Jobs/Plex/PlexWatchlistImport.cs</url>
<line>100</line>
<properties documentPath="$PROJECT_DIR$/Ombi.Schedule/Jobs/Plex/PlexWatchlistImport.cs" containingFunctionPresentation="Method 'Execute'">
<startOffsets>
<option value="4602" />
</startOffsets>
<endOffsets>
<option value="4636" />
</endOffsets>
</properties>
<option name="timeStamp" value="12" />
</line-breakpoint>
</breakpoints>
</breakpoint-manager>
<watches-manager>

View file

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="RIDER_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$USER_HOME$/.nuget/packages/microsoft.net.test.sdk/16.0.1/build/netcoreapp1.0" />
<content url="file://$USER_HOME$/.nuget/packages/nunit3testadapter/3.13.0/build/netcoreapp1.0/NUnit3.TestAdapter.dll" />
<content url="file://$USER_HOME$/.nuget/packages/nunit3testadapter/3.13.0/build/netcoreapp1.0/NUnit3.TestAdapter.pdb" />
<content url="file://$USER_HOME$/.nuget/packages/nunit3testadapter/3.13.0/build/netcoreapp1.0/nunit.engine.netstandard.dll" />
<content url="file://$MODULE_DIR$/../../../CHANGELOG.md" />
<content url="file://$MODULE_DIR$/../../../appveyor.yml" />
<content url="file://$MODULE_DIR$/../../../build.cake" />
<content url="file://$MODULE_DIR$/../.." />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View file

@ -1,22 +1,31 @@
{
"version": "0.1.0",
"version": "2.0.0",
"command": "dotnet",
"isShellCommand": true,
"args": [],
"tasks": [
{
"taskName": "build",
"label": "build",
"type": "shell",
"command": "dotnet",
"args": [
"build",
"${workspaceRoot}/Ombi/Ombi.csproj"
],
"isBuildCommand": true,
"problemMatcher": "$msCompile"
"problemMatcher": "$msCompile",
"group": {
"_id": "build",
"isDefault": false
}
},
{
"taskName": "lint",
"label": "lint",
"type": "shell",
"command": "npm",
"isShellCommand": true,
"args": ["run", "lint"]
"args": [
"run",
"lint"
],
"problemMatcher": []
}
]
}

View file

@ -14,6 +14,7 @@
IssueResolved = 9,
IssueComment = 10,
Newsletter = 11,
PartiallyAvailable = 12
PartiallyAvailable = 12,
PlexWatchlistTokenExpired = 13
}
}

View file

@ -2,6 +2,9 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
<Configurations>Debug;Release;NonUiBuild</Configurations>
</PropertyGroup>
@ -13,7 +16,7 @@
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="8.0.0" />
<PackageReference Include="NUnit.ConsoleRunner" Version="3.15.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
<packagereference Include="Microsoft.NET.Test.Sdk" Version="17.6.2"></packagereference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.2" />
</ItemGroup>
<ItemGroup>

View file

@ -20,6 +20,10 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Ombi.Notifications.Models;
using Ombi.Core.Notifications;
using Ombi.Helpers;
using Ombi.Core;
namespace Ombi.Schedule.Tests
{
@ -35,12 +39,13 @@ namespace Ombi.Schedule.Tests
public void Setup()
{
_mocker = new AutoMocker();
var um = MockHelper.MockUserManager(new List<OmbiUser> { new OmbiUser { Id = "abc", UserType = UserType.PlexUser, MediaServerToken = "token1", UserName = "abc", NormalizedUserName = "ABC" } });
var um = MockHelper.MockUserManager(new List<OmbiUser> { new OmbiUser { Id = "abc", Email = "email@email.com", UserType = UserType.PlexUser, MediaServerToken = "token1", UserName = "abc", NormalizedUserName = "ABC" } });
_mocker.Use(um);
_context = _mocker.GetMock<IJobExecutionContext>();
_context.Setup(x => x.CancellationToken).Returns(CancellationToken.None);
_subject = _mocker.CreateInstance<PlexWatchlistImport>();
_mocker.Setup<IRepository<PlexWatchlistUserError>, IQueryable<PlexWatchlistUserError>>(x => x.GetAll()).Returns(new List<PlexWatchlistUserError>().AsQueryable().BuildMock());
_mocker.Setup<INotificationHelper>(x => x.Notify(It.IsAny<NotificationOptions>()));
}
[Test]
@ -777,5 +782,61 @@ namespace Ombi.Schedule.Tests
_mocker.Verify<IExternalRepository<PlexWatchlistHistory>>(x => x.GetAll(), Times.Once);
_mocker.Verify<IExternalRepository<PlexWatchlistHistory>>(x => x.Add(It.IsAny<PlexWatchlistHistory>()), Times.Once);
}
[Test]
public async Task AuthenticationError_NotificationsEnabled_WithEmail_SendsNotification()
{
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync())
.ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true, NotifyOnWatchlistTokenExpiration = true });
_mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new PlexWatchlistContainer { AuthError = true });
// Act
await _subject.Execute(_context.Object);
// Assert
_mocker.Verify<INotificationHelper>(x => x.Notify(It.Is<NotificationOptions>(n =>
n.NotificationType == NotificationType.PlexWatchlistTokenExpired &&
n.Recipient == "email@email.com" &&
n.Substitutes["UserName"] == "abc"
)), Times.Once);
}
[Test]
public async Task AuthenticationError_NotificationsDisabled_WithEmail_DoesNotSendNotification()
{
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync())
.ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true, NotifyOnWatchlistTokenExpiration = false });
_mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new PlexWatchlistContainer { AuthError = true });
// Act
await _subject.Execute(_context.Object);
// Assert
_mocker.Verify<INotificationHelper>(x => x.Notify(It.IsAny<NotificationOptions>()), Times.Never);
}
[Test]
public async Task AuthenticationError_NotificationsEnabled_NoEmail_DoesNotSendNotification()
{
// Arrange
var user = new OmbiUser { Id = "abc", UserType = UserType.PlexUser, MediaServerToken = "token1", UserName = "abc", NormalizedUserName = "ABC" };
var um = MockHelper.MockUserManager(new List<OmbiUser> { user });
_mocker.Use(um);
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync())
.ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true, NotifyOnWatchlistTokenExpiration = true });
_mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new PlexWatchlistContainer { AuthError = true });
_subject = _mocker.CreateInstance<PlexWatchlistImport>();
// Act
await _subject.Execute(_context.Object);
// Assert
_mocker.Verify<INotificationHelper>(x => x.Notify(It.IsAny<NotificationOptions>()), Times.Never);
}
}
}

View file

@ -1,27 +0,0 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:62604/",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Ombi.Schedule.Tests": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:62605/"
}
}
}

View file

@ -22,6 +22,11 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Ombi.Notifications.Models;
using Ombi.Core.Notifications;
using Microsoft.AspNetCore.Identity;
using Ombi.Store.Repository.Requests;
using Ombi.Core;
namespace Ombi.Schedule.Jobs.Plex
{
@ -37,11 +42,12 @@ namespace Ombi.Schedule.Jobs.Plex
private readonly IExternalRepository<PlexWatchlistHistory> _watchlistRepo;
private readonly IRepository<PlexWatchlistUserError> _userError;
private readonly IMovieDbApi _movieDbApi;
private readonly INotificationHelper _notificationHelper;
public PlexWatchlistImport(IPlexApi plexApi, ISettingsService<PlexSettings> settings, OmbiUserManager ombiUserManager,
IMovieRequestEngine movieRequestEngine, ITvRequestEngine tvRequestEngine, INotificationHubService notificationHubService,
ILogger<PlexWatchlistImport> logger, IExternalRepository<PlexWatchlistHistory> watchlistRepo, IRepository<PlexWatchlistUserError> userError,
IMovieDbApi movieDbApi)
IMovieDbApi movieDbApi, INotificationHelper notificationHelper)
{
_plexApi = plexApi;
_settings = settings;
@ -53,6 +59,7 @@ namespace Ombi.Schedule.Jobs.Plex
_watchlistRepo = watchlistRepo;
_userError = userError;
_movieDbApi = movieDbApi;
_notificationHelper = notificationHelper;
}
public async Task Execute(IJobExecutionContext context)
@ -99,6 +106,22 @@ namespace Ombi.Schedule.Jobs.Plex
UserId = user.Id,
MediaServerToken = user.MediaServerToken,
});
// Send notification to user about token expiration
if (settings.NotifyOnWatchlistTokenExpiration && !string.IsNullOrEmpty(user.Email))
{
var notificationModel = new NotificationOptions
{
NotificationType = NotificationType.PlexWatchlistTokenExpired,
Recipient = user.Email,
DateTime = DateTime.Now,
Substitutes = new Dictionary<string, string>
{
{ "UserName", user.UserName }
}
};
await _notificationHelper.Notify(notificationModel);
}
continue;
}
if (watchlist == null || !(watchlist.MediaContainer?.Metadata?.Any() ?? false))

View file

@ -9,6 +9,7 @@ namespace Ombi.Core.Settings.Models.External
public bool Enable { get; set; }
public bool EnableWatchlistImport { get; set; }
public bool MonitorAll { get; set; }
public bool NotifyOnWatchlistTokenExpiration { get; set; }
/// <summary>
/// This is the ClientId for OAuth
/// </summary>

View file

@ -217,6 +217,16 @@ namespace Ombi.Store.Context
Enabled = true,
};
break;
case NotificationType.PlexWatchlistTokenExpired:
notificationToAdd = new NotificationTemplates
{
NotificationType = notificationType,
Message = "Hello {UserName}! Your Plex watchlist token has expired. Please re-authenticate with Ombi to continue using the watchlist feature.",
Subject = "Plex Watchlist Token Expired",
Agent = agent,
Enabled = true,
};
break;
default:
throw new ArgumentOutOfRangeException();
}

View file

@ -52,6 +52,7 @@ export enum NotificationType {
IssueComment = 10,
Newsletter = 11,
PartiallyAvailable = 12,
PlexWatchlistTokenExpired = 13
}
export interface IDiscordNotifcationSettings extends INotificationSettings {

View file

@ -114,6 +114,7 @@ export interface IPlexSettings extends ISettings {
enable: boolean;
enableWatchlistImport: boolean;
monitorAll: boolean;
notifyOnWatchlistTokenExpiration: boolean;
servers: IPlexServer[];
}

View file

@ -1,38 +1,40 @@
<div class="small-middle-container">
<fieldset style="fieldset">
<legend mat-dialog-title>Watchlist User Errors</legend>
<div mat-dialog-content>
<p>
If there is an authentication error, this is because of an authentication issue with Plex (Token has expired).
If this happens the user needs to re-login to Ombi.
</p>
<p>
<em class="fa-solid fa-check key"></em> Successfully syncing the watchlist
<br>
<em class="fa-solid fa-times key"></em> Authentication error syncing the watchlist
<br>
<em class="fas fa-user-alt-slash key"></em> Not enabled for user (They need to log into Ombi via Plex)
</p>
<table mat-table *ngIf="dataSource" [dataSource]="dataSource" matSort class="mat-elevation-z8">
<ng-container matColumnDef="userName">
<th mat-header-cell *matHeaderCellDef> Username </th>
<td mat-cell *matCellDef="let element"> {{element.userName}} </td>
</ng-container>
<ng-container matColumnDef="syncStatus">
<th mat-header-cell *matHeaderCellDef> Watchlist Sync Result </th>
<td mat-cell *matCellDef="let element">
<em *ngIf="element.syncStatus === WatchlistSyncStatus.Successful" class="fa-solid fa-check"></em>
<em *ngIf="element.syncStatus === WatchlistSyncStatus.Failed" class="fa-solid fa-times"></em>
<em *ngIf="element.syncStatus === WatchlistSyncStatus.NotEnabled" class="fas fa-user-alt-slash"></em>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>
<mat-dialog-actions align="end">
<button mat-button mat-dialog-close>Close</button>
</mat-dialog-actions>
</fieldset>
<div class="watchlist-dialog-container">
<mat-card class="watchlist-dialog-card">
<mat-card-header>
<mat-card-title>Watchlist User Errors</mat-card-title>
</mat-card-header>
<mat-card-content>
<div class="watchlist-info-section">
<mat-icon color="warn" class="info-icon">error_outline</mat-icon>
<span>
If there is an authentication error, this is because of an authentication issue with Plex (Token has expired).
If this happens the user needs to re-login to Ombi.
</span>
</div>
<div class="watchlist-legend">
<span><mat-icon color="primary">check_circle</mat-icon> Successfully syncing the watchlist</span>
<span><mat-icon color="warn">cancel</mat-icon> Authentication error syncing the watchlist</span>
<span><mat-icon color="accent">person_off</mat-icon> Not enabled for user (They need to log into Ombi via Plex)</span>
</div>
<table mat-table *ngIf="dataSource" [dataSource]="dataSource" matSort class="mat-elevation-z8 modern-table">
<ng-container matColumnDef="userName">
<th mat-header-cell *matHeaderCellDef> Username </th>
<td mat-cell *matCellDef="let element"> {{element.userName}} </td>
</ng-container>
<ng-container matColumnDef="syncStatus">
<th mat-header-cell *matHeaderCellDef> Watchlist Sync Result </th>
<td mat-cell *matCellDef="let element">
<mat-icon *ngIf="element.syncStatus === WatchlistSyncStatus.Successful" color="primary">check_circle</mat-icon>
<mat-icon *ngIf="element.syncStatus === WatchlistSyncStatus.Failed" color="warn">cancel</mat-icon>
<mat-icon *ngIf="element.syncStatus === WatchlistSyncStatus.NotEnabled" color="accent">person_off</mat-icon>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</mat-card-content>
<mat-card-actions align="end">
<button mat-stroked-button mat-dialog-close color="accent">Close</button>
</mat-card-actions>
</mat-card>
</div>

View file

@ -10,4 +10,96 @@
.key {
width: 40px;
}
.watchlist-dialog-container {
display: flex;
justify-content: center;
align-items: flex-start;
margin: 0 auto;
}
.watchlist-dialog-card {
background: #23272f;
color: #f1f3f6;
border-radius: 12px;
border: 1px solid #353a45;
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.25);
min-width: 420px;
max-width: 600px;
width: 100%;
max-height: 70vh;
display: flex;
flex-direction: column;
}
mat-card-content {
flex: 1 1 auto;
overflow-y: auto;
min-height: 0;
}
mat-card-header {
border-bottom: 1px solid #353a45;
margin-bottom: 12px;
flex: 0 0 auto;
}
mat-card-title {
color: #fff !important;
font-weight: 700 !important;
letter-spacing: 0.5px;
}
.watchlist-info-section {
display: flex;
align-items: center;
background: #23272f;
color: #e0e3ea;
padding: 12px 0 8px 0;
font-size: 15px;
gap: 12px;
}
.info-icon {
font-size: 28px;
color: #ffb300;
}
.watchlist-legend {
display: flex;
flex-direction: column;
gap: 4px;
margin-bottom: 18px;
margin-top: 8px;
font-size: 14px;
color: #b0b6c3;
}
.watchlist-legend mat-icon {
vertical-align: middle;
margin-right: 6px;
}
.modern-table {
background: transparent;
color: #f1f3f6;
border-radius: 8px;
margin-top: 10px;
}
.modern-table th, .modern-table td {
color: #f1f3f6;
font-size: 15px;
}
mat-card-actions {
padding-top: 16px;
flex: 0 0 auto;
background: #23272f;
}
button[mat-stroked-button] {
font-weight: 600;
border-radius: 6px;
}

View file

@ -1,137 +1,179 @@
<settings-menu></settings-menu>
<div class="small-middle-container" *ngIf="settings">
<fieldset style="width:100%;">
<legend>Plex Configuration</legend>
<div class="col-12">
<div class="md-form-field align-right">
<button (click)="openWatchlistUserLog()" type="button" class="mat-focus-indicator mat-flat-button mat-button-base mat-accent">Watchlist User Errors</button>
<div class="plex-settings-container" *ngIf="settings">
<mat-card class="settings-card">
<mat-card-header>
<mat-card-title>Plex Configuration</mat-card-title>
</mat-card-header>
<mat-card-content>
<!-- Watchlist Settings Section -->
<div class="settings-section">
<div class="section-header">
<h2>Watchlist Settings</h2>
</div>
<div class="settings-grid">
<mat-card class="setting-card">
<mat-card-content>
<div class="setting-header">
<h3>Enable Plex</h3>
<mat-slide-toggle [id]="'enable'" [(ngModel)]="settings.enable"></mat-slide-toggle>
</div>
</mat-card-content>
</mat-card>
<mat-card class="setting-card">
<mat-card-content>
<div class="setting-header">
<h3>Enable User Watchlist Requests</h3>
<mat-slide-toggle [id]="'enableWatchlistImport'" [(ngModel)]="settings.enableWatchlistImport"></mat-slide-toggle>
</div>
<p class="setting-description">
When a Plex User adds something to their watchlist in Plex, it will turn up in Ombi as a Request if enabled.
This <strong>only</strong> applies to users that are logging in with their Plex Account.
<br>Request limits if set are all still applied
</p>
</mat-card-content>
</mat-card>
<mat-card class="setting-card" [class.disabled]="!settings.enableWatchlistImport">
<mat-card-content>
<div class="setting-header">
<h3>Request Whole Show</h3>
<mat-slide-toggle [id]="'monitorAll'" [(ngModel)]="settings.monitorAll" [disabled]="!settings.enableWatchlistImport"></mat-slide-toggle>
</div>
<p class="setting-description">
If enabled then watchlist requests for TV Shows will request the <strong>whole</strong> show.
If not enabled it will only request the latest season.
</p>
</mat-card-content>
</mat-card>
<mat-card class="setting-card" [class.disabled]="!settings.enableWatchlistImport">
<mat-card-content>
<div class="setting-header">
<h3>Notify on Token Expiration</h3>
<mat-slide-toggle [id]="'notifyOnWatchlistTokenExpiration'" [(ngModel)]="settings.notifyOnWatchlistTokenExpiration" [disabled]="!settings.enableWatchlistImport"></mat-slide-toggle>
</div>
<p class="setting-description">
When enabled, users will receive a notification if their Plex watchlist token expires and they need to log into Ombi again to continue using the watchlist feature.
<br><strong>Note:</strong> This requires email notifications to be configured in the notification settings, and users must have an email address set on their account to receive these notifications.
</p>
</mat-card-content>
</mat-card>
</div>
<mat-card class="info-banner">
<mat-icon color="primary" style="margin-right: 12px;">info</mat-icon>
<span style="flex:1;">
Some users may need to re-log in to use the watchlist feature.
</span>
<button mat-button color="accent" (click)="openWatchlistUserLog()">
View Users
</button>
</mat-card>
</div>
</div>
<settings-plex-form-field [label]="'Enable'" [type]="'checkbox'" [id]="'enable'" [(value)]="settings.enable"></settings-plex-form-field>
<!-- Main Content Area -->
<div class="main-content">
<!-- Left Column - Servers and Actions -->
<div class="content-column">
<!-- Servers Section -->
<div class="settings-section">
<h2>Plex Servers</h2>
<div class="servers-grid">
<mat-card class="server-card" *ngFor="let server of settings.servers">
<mat-card-content>
<button mat-button (click)="edit(server)" [id]="server.name + '-button'">
<mat-icon>dns</mat-icon>
<span>{{server.name}}</span>
</button>
</mat-card-content>
</mat-card>
<settings-plex-form-field [label]="'Enable User Watchlist Requests'" [type]="'checkbox'" [id]="'enableWatchlistImport'" [(value)]="settings.enableWatchlistImport">
<small bottom>When a Plex User adds something to their watchlist in Plex, it will turn up in Ombi as a Request if enabled. This <b>only</b> applies to users that are logging in with their Plex Account
<br>Request limits if set are all still applied
</small>
</settings-plex-form-field>
<mat-card class="server-card new-server">
<mat-card-content>
<button mat-button (click)="newServer()" id="newServer">
<mat-icon>add_circle</mat-icon>
<span>Add Server</span>
</button>
</mat-card-content>
</mat-card>
</div>
</div>
<settings-plex-form-field [disabled]="!settings.enableWatchlistImport" [label]="'Watchlist - Request Whole Show'" disabled [type]="'checkbox'" [id]="'monitorAll'" [(value)]="settings.monitorAll">
<small bottom>If enabled then watchlist requests for TV Shows will request the <strong><em>whole</em></strong> show. If not enabled it will only request the latest season.
</small>
</settings-plex-form-field>
<!-- Sync Actions Section -->
<div class="settings-section">
<h2>Sync Actions</h2>
<div class="sync-actions-grid">
<button mat-stroked-button (click)="runSync(PlexSyncType.Full)" id="fullSync">
<mat-icon>sync</mat-icon>
Full Sync
</button>
<button mat-stroked-button (click)="runSync(PlexSyncType.RecentlyAdded)" id="recentlyAddedSync">
<mat-icon>update</mat-icon>
Partial Sync
</button>
<button mat-stroked-button (click)="runSync(PlexSyncType.ClearAndReSync)" id="clearData">
<mat-icon>cleaning_services</mat-icon>
Clear & Resync
</button>
<button mat-stroked-button (click)="runSync(PlexSyncType.WatchlistImport)" id="watchlistImport">
<mat-icon>playlist_add</mat-icon>
Run Watchlist Import
</button>
</div>
</div>
</div>
<!-- Right Column - Plex Credentials -->
<div class="content-column">
<div class="settings-section">
<h2>Plex Credentials</h2>
<mat-card class="credentials-card">
<mat-card-content>
<p class="credentials-description">
These fields are optional to automatically fill in your Plex server settings.
<br>This will pass your username and password to the Plex.tv API to grab the servers associated with this user.
<br>If you have 2FA enabled on your account, you need to append the 2FA code to the end of your password.
</p>
<mat-form-field appearance="outline" class="full-width">
<mat-label>Username</mat-label>
<input matInput [id]="'username'" [(ngModel)]="username">
</mat-form-field>
<hr>
<mat-form-field appearance="outline" class="full-width">
<mat-label>Password</mat-label>
<input matInput [id]="'password'" type="password" [(ngModel)]="password">
</mat-form-field>
<div class="row">
<div class="col-md-7">
<h2 style="margin: 1em 0 0 0;">Servers</h2>
<mat-list style="display:flex; flex-flow: wrap;">
<mat-card class="server-card" *ngFor="let server of settings.servers">
<button mat-button (click)="edit(server)" id="{{server.name}}-button">
<h3>{{server.name}}</h3>
</button>
</mat-card>
<button mat-raised-button color="primary" id="loadServers" (click)="requestServers()" class="full-width">
<mat-icon>key</mat-icon>
Load Servers
</button>
<mat-card class="server-card new-server-card">
<button mat-button (click)="newServer()" id="newServer">
<i class="fas fa-plus fa-xl"></i>
<h3>Manually Add Server</h3>
</button>
</mat-card>
</mat-list>
<div class="row">
<br />
<div class="form-group col-2">
<button mat-raised-button (click)="runSync(PlexSyncType.Full)" type="button" id="fullSync"
class="mat-focus-indicator mat-stroked-button mat-button-base">Full
Sync</button><br />
</div>
<div class="form-group col-2">
<button mat-raised-button (click)="runSync(PlexSyncType.RecentlyAdded)" type="button" id="recentlyAddedSync"
class="mat-focus-indicator mat-stroked-button mat-button-base">Partial Sync</button>
</div>
<div class="form-group col-2">
<button mat-raised-button (click)="runSync(PlexSyncType.ClearAndReSync)" type="button" id="clearData"
class="mat-focus-indicator mat-stroked-button mat-button-base">
Clear Data And Resync
</button>
</div>
<div class="form-group col-12">
<button mat-raised-button (click)="runSync(PlexSyncType.WatchlistImport)" type="button" id="watchlistImport"
class="mat-focus-indicator mat-stroked-button mat-button-base">
Run Watchlist Import
</button>
</div>
</div>
<div class="row">
<div class="col-md-2">
<div class="form-group">
<div>
<button mat-raised-button (click)="save()" type="submit" id="save"
class="mat-focus-indicator mat-raised-button mat-button-base mat-accent">Submit</button>
<mat-form-field appearance="outline" class="full-width mt-3">
<mat-label>Select Server</mat-label>
<mat-select [id]="'servers'" *ngIf="loadedServers">
<mat-option (click)="selectServer(s)" *ngFor="let s of loadedServers.servers.server" [value]="s.server">
{{s.name}}
</mat-option>
</mat-select>
<input matInput disabled placeholder="No Servers Loaded" *ngIf="!loadedServers">
</mat-form-field>
</mat-card-content>
</mat-card>
</div>
</div>
</div>
</div>
</div>
</mat-card-content>
<div class="col-md-5">
<div class="md-form-field">
<label for="username" class="control-label">
<h3>Plex Credentials</h3>
<small>These fields are optional to automatically fill in your Plex server settings. <br>
This will pass your username and password to the Plex.tv API to grab the servers associated with this user.
<br>
If you have 2FA enabled on your account, you need to append the 2FA code to the end of your password.</small>
</label>
</div>
<settings-plex-form-field [label]="'Username'" [id]="'username'" [(value)]="username"></settings-plex-form-field>
<settings-plex-form-field [label]="'Password'" [id]="'password'" [type]="'password'" [(value)]="password"></settings-plex-form-field>
<div class="md-form-field">
<div class="right">
<button mat-raised-button id="loadServers" (click)="requestServers()"
class="mat-stroked-button">Load Servers
<i class="fas fa-key"></i>
</button>
</div>
</div>
<div class="row">
<div class="col-2 align-self-center">
Please select the server:
</div>
<div class="md-form-field col-10">
<div *ngIf="!loadedServers">
<mat-form-field appearance="outline" floatLabel=auto>
<input disabled matInput placeholder="No Servers Loaded" id="servers">
</mat-form-field>
</div>
<div *ngIf="loadedServers">
<mat-form-field appearance="outline">
<mat-select placeholder="Servers Loaded! Please Select" id="servers">
<mat-option (click)="selectServer(s)"
*ngFor="let s of loadedServers.servers.server" [value]="s.server">
{{s.name}}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
</div>
</div>
<mat-card-actions align="end">
<button mat-raised-button color="accent" (click)="save()" id="save">
<mat-icon>save</mat-icon>
Save Changes
</button>
</mat-card-actions>
</mat-card>
</div>
</fieldset>
</div>
<!--(){{settings|json}}-->

View file

@ -43,4 +43,258 @@
h3 {
margin: 0;
}
}
}
.plex-settings-container {
padding: 20px;
max-width: 1400px;
margin: 0 auto;
}
.settings-card {
margin-bottom: 20px;
background: transparent;
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.25);
border-radius: 12px;
border: 1px solid #353a45;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.settings-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 24px;
margin-bottom: 30px;
}
.setting-card {
height: 100%;
background: transparent;
border-radius: 10px;
border: 1px solid #353a45;
box-shadow: 0 1px 6px 0 rgba(0,0,0,0.18);
color: #f1f3f6;
}
.setting-card.disabled {
opacity: 0.6;
}
.setting-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
font-weight: 600;
color: #fff;
}
.setting-description {
color: #e0e3ea;
font-size: 15px;
margin: 0;
font-weight: 400;
line-height: 1.6;
}
.main-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 36px;
margin-top: 36px;
}
.servers-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 18px;
}
.server-card {
text-align: center;
background: transparent;
border-radius: 10px;
border: 1px solid #353a45;
box-shadow: 0 1px 6px 0 rgba(0,0,0,0.18);
color: #f1f3f6;
}
.server-card button {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24px;
color: #f1f3f6;
font-weight: 500;
}
.server-card mat-icon {
font-size: 32px;
height: 32px;
width: 32px;
margin-bottom: 10px;
color: #90caf9;
}
.sync-actions-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 18px;
}
.sync-actions-grid button {
display: flex;
flex-direction: column;
align-items: center;
padding: 24px;
color: #f1f3f6;
background: transparent;
border: 1px solid #353a45;
border-radius: 10px;
font-weight: 500;
box-shadow: 0 1px 6px 0 rgba(0,0,0,0.18);
}
.sync-actions-grid mat-icon {
font-size: 32px;
height: 32px;
width: 32px;
margin-bottom: 10px;
color: #90caf9;
}
.credentials-card {
padding: 24px;
background: transparent;
border-radius: 10px;
border: 1px solid #353a45;
box-shadow: 0 1px 6px 0 rgba(0,0,0,0.18);
color: #f1f3f6;
}
.credentials-description {
color: #e0e3ea;
margin-bottom: 20px;
font-size: 15px;
font-weight: 400;
}
mat-card-title, h2, h3 {
color: #fff !important;
font-weight: 700 !important;
letter-spacing: 0.5px;
}
mat-card-header {
border-bottom: 1px solid #353a45;
margin-bottom: 16px;
}
mat-form-field {
color: #f1f3f6 !important;
}
mat-label, .mat-form-field-label {
color: #b0b6c3 !important;
font-weight: 500;
}
input[matInput], .mat-input-element {
color: #f1f3f6 !important;
background: transparent !important;
}
mat-select {
color: #f1f3f6 !important;
background: transparent !important;
}
.full-width {
width: 100%;
}
.mt-3 {
margin-top: 1rem;
}
.mat-slide-toggle.mat-checked .mat-slide-toggle-bar {
background-color: #90caf9 !important;
}
.mat-slide-toggle-thumb {
background-color: #2196f3 !important;
}
button[mat-flat-button], button[mat-raised-button], button[mat-stroked-button], button[mat-button] {
font-weight: 600;
letter-spacing: 0.2px;
color: #f1f3f6;
background: #2196f3;
border-radius: 6px;
box-shadow: 0 1px 4px 0 rgba(0,0,0,0.12);
transition: background 0.2s;
}
button[mat-flat-button]:hover, button[mat-raised-button]:hover, button[mat-stroked-button]:hover, button[mat-button]:hover {
background: #42a5f5;
}
@media (max-width: 1200px) {
.main-content {
grid-template-columns: 1fr;
}
}
@media (max-width: 768px) {
.settings-grid {
grid-template-columns: 1fr;
}
.servers-grid {
grid-template-columns: 1fr;
}
.sync-actions-grid {
grid-template-columns: 1fr;
}
}
.watchlist-errors-btn-row {
display: flex;
justify-content: center;
margin-top: 18px;
}
.info-banner {
display: flex;
align-items: center;
background: #23272f;
color: #e0e3ea;
border: 1px solid #1976d2;
box-shadow: 0 1px 6px 0 rgba(0,0,0,0.12);
border-radius: 8px;
padding: 16px 20px;
margin-top: 18px;
margin-bottom: 0;
font-size: 16px;
font-weight: 500;
gap: 12px;
}
.info-banner mat-icon {
font-size: 28px;
color: #42a5f5;
}
.info-banner button[mat-button] {
margin-left: 16px;
font-weight: 600;
}