mirror of
https://github.com/Ombi-app/Ombi.git
synced 2025-08-22 14:13:36 -07:00
Compare commits
54 commits
Author | SHA1 | Date | |
---|---|---|---|
|
b72f47470c | ||
|
72d4115378 |
||
|
11fd7a5fc8 |
||
|
d2be48a921 | ||
|
a92c76021a | ||
|
97d5167db6 | ||
|
2519cca9f6 | ||
|
cfeee39978 | ||
|
cee40146ee | ||
|
1eff48e58e | ||
|
3b2a0d84be | ||
|
ed5bc3f873 | ||
|
067c029f42 | ||
|
cfe2b6ac0f | ||
|
c9ab4f4f9f | ||
|
acb679f99d | ||
|
f88c5ad818 | ||
|
b3e8ca6950 |
||
|
15a97794f6 | ||
|
ba6e708e18 | ||
|
08c9017a2c | ||
|
f8658fe6d5 | ||
|
7303e7da3b | ||
|
ffb495019f | ||
|
13c1544476 | ||
|
04ddf3d09b | ||
|
ea0b690c18 |
||
|
dae0fe6be4 | ||
|
dbbfdd926f | ||
|
cb6d441ccd | ||
|
6344ae98cd | ||
|
0dfd4533db | ||
|
6e539585f1 | ||
|
cf0c1614a4 | ||
|
2868314a34 | ||
|
3eef2fafee |
||
|
9227bc0b4c |
||
|
fe2fe24158 | ||
|
a801cfdb09 | ||
|
72af4f734d | ||
|
fc94fcfe68 |
||
|
6df9d6e1d2 | ||
|
fefc768aa5 |
||
|
b00e3878a7 | ||
|
cc98fc6aca |
||
|
e8ca519ef9 | ||
|
579d048ba1 | ||
|
cd260ed844 | ||
|
dc2b958915 |
||
|
3234204221 | ||
|
138df1eb25 | ||
|
53a6a092b1 | ||
|
da6665deb6 | ||
|
fcb4082731 |
78 changed files with 2934 additions and 4608 deletions
2
.github/workflows/automation-tests.yml
vendored
2
.github/workflows/automation-tests.yml
vendored
|
@ -24,7 +24,7 @@ jobs:
|
|||
with:
|
||||
node-version: '18'
|
||||
|
||||
- uses: actions/cache@v2
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
'**/node_modules'
|
||||
|
|
10
.github/workflows/build.yml
vendored
10
.github/workflows/build.yml
vendored
|
@ -15,7 +15,7 @@ jobs:
|
|||
node-version: '18'
|
||||
|
||||
- name: NodeModules Cache
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
key: node_modules-${{ hashFiles('**/yarn.lock') }}
|
||||
|
@ -42,7 +42,7 @@ jobs:
|
|||
dotnet-version: '8.0.x'
|
||||
|
||||
- name: Nuget Cache
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.nuget/packages
|
||||
key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
|
||||
|
@ -112,7 +112,7 @@ jobs:
|
|||
dotnet-version: '5.0.x'
|
||||
|
||||
- name: Nuget Cache
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.nuget/packages
|
||||
key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
|
||||
|
@ -130,7 +130,7 @@ jobs:
|
|||
working-directory: src/Ombi
|
||||
|
||||
- name: Download Angular
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: angular_dist
|
||||
path: ~/src/Ombi/dist
|
||||
|
@ -170,7 +170,7 @@ jobs:
|
|||
|
||||
- name: Download Artifacts
|
||||
id: download
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
|
|
2
.github/workflows/chromatic.yml
vendored
2
.github/workflows/chromatic.yml
vendored
|
@ -17,7 +17,7 @@
|
|||
# fetch-depth: 0
|
||||
|
||||
# - name: NodeModules Cache
|
||||
# uses: actions/cache@v2
|
||||
# uses: actions/cache@v4
|
||||
# with:
|
||||
# path: '**/node_modules'
|
||||
# key: node_modules-${{ hashFiles('**/yarn.lock') }}
|
||||
|
|
6
.github/workflows/pr.yml
vendored
6
.github/workflows/pr.yml
vendored
|
@ -20,7 +20,7 @@ jobs:
|
|||
node-version: '18'
|
||||
|
||||
- name: NodeModules Cache
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
key: node_modules-${{ hashFiles('**/yarn.lock') }}
|
||||
|
@ -41,7 +41,7 @@ jobs:
|
|||
dotnet-version: '8.0.x'
|
||||
|
||||
- name: Nuget Cache
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.nuget/packages
|
||||
key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
|
||||
|
@ -101,7 +101,7 @@ jobs:
|
|||
dotnet-version: '8.0.x'
|
||||
|
||||
- name: Nuget Cache
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.nuget/packages
|
||||
key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
|
||||
|
|
1786
CHANGELOG.md
1786
CHANGELOG.md
File diff suppressed because it is too large
Load diff
41
README.md
41
README.md
|
@ -122,10 +122,10 @@ Here are some of the features Ombi has:
|
|||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/MattJeanes">
|
||||
<img src="https://avatars.githubusercontent.com/u/2363642?v=4" width="50;" alt="MattJeanes"/>
|
||||
<a href="https://github.com/AmyJeanes">
|
||||
<img src="https://avatars.githubusercontent.com/u/2363642?v=4" width="50;" alt="AmyJeanes"/>
|
||||
<br />
|
||||
<sub><b>Matt Jeanes</b></sub>
|
||||
<sub><b>Amy Jeanes</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
|
@ -301,8 +301,8 @@ Here are some of the features Ombi has:
|
|||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/deepwather">
|
||||
<img src="https://avatars.githubusercontent.com/u/12274612?v=4" width="50;" alt="deepwather"/>
|
||||
<a href="https://github.com/tuxmi">
|
||||
<img src="https://avatars.githubusercontent.com/u/12274612?v=4" width="50;" alt="tuxmi"/>
|
||||
<br />
|
||||
<sub><b>Michael Reber</b></sub>
|
||||
</a>
|
||||
|
@ -659,10 +659,10 @@ Here are some of the features Ombi has:
|
|||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/Drewster727">
|
||||
<img src="https://avatars.githubusercontent.com/u/4528753?v=4" width="50;" alt="Drewster727"/>
|
||||
<a href="https://github.com/sussycatgirl">
|
||||
<img src="https://avatars.githubusercontent.com/u/26145882?v=4" width="50;" alt="sussycatgirl"/>
|
||||
<br />
|
||||
<sub><b>Drew</b></sub>
|
||||
<sub><b>Lea</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
|
@ -752,13 +752,20 @@ Here are some of the features Ombi has:
|
|||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/Alasano">
|
||||
<img src="https://avatars.githubusercontent.com/u/14372930?v=4" width="50;" alt="Alasano"/>
|
||||
<a href="https://github.com/alasano">
|
||||
<img src="https://avatars.githubusercontent.com/u/14372930?v=4" width="50;" alt="alasano"/>
|
||||
<br />
|
||||
<sub><b>Aljosa Asanovic</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/AlexandrePicavet">
|
||||
<img src="https://avatars.githubusercontent.com/u/25816980?v=4" width="50;" alt="AlexandrePicavet"/>
|
||||
<br />
|
||||
<sub><b>Alexandre Picavet</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/XanderStrike">
|
||||
<img src="https://avatars.githubusercontent.com/u/1565303?v=4" width="50;" alt="XanderStrike"/>
|
||||
|
@ -780,13 +787,6 @@ Here are some of the features Ombi has:
|
|||
<sub><b>Abe Kline</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/sussycatgirl">
|
||||
<img src="https://avatars.githubusercontent.com/u/26145882?v=4" width="50;" alt="sussycatgirl"/>
|
||||
<br />
|
||||
<sub><b>Lea</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/kmlucy">
|
||||
<img src="https://avatars.githubusercontent.com/u/13952475?v=4" width="50;" alt="kmlucy"/>
|
||||
|
@ -901,6 +901,13 @@ Here are some of the features Ombi has:
|
|||
<br />
|
||||
<sub><b>Eli</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/Drewster727">
|
||||
<img src="https://avatars.githubusercontent.com/u/4528753?v=4" width="50;" alt="Drewster727"/>
|
||||
<br />
|
||||
<sub><b>Drew</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
</table>
|
||||
<!-- readme: collaborators,contributors -end -->
|
||||
|
|
2309
src/.idea/.idea.Ombi/.idea/contentModel.xml
generated
2309
src/.idea/.idea.Ombi/.idea/contentModel.xml
generated
File diff suppressed because it is too large
Load diff
440
src/.idea/.idea.Ombi/.idea/dbnavigator.xml
generated
Normal file
440
src/.idea/.idea.Ombi/.idea/dbnavigator.xml
generated
Normal 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>
|
2
src/.idea/.idea.Ombi/.idea/indexLayout.xml
generated
2
src/.idea/.idea.Ombi/.idea/indexLayout.xml
generated
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ContentModelUserStore">
|
||||
<component name="UserContentModel">
|
||||
<attachedFolders />
|
||||
<explicitIncludes />
|
||||
<explicitExcludes />
|
||||
|
|
8
src/.idea/.idea.Ombi/.idea/modules.xml
generated
8
src/.idea/.idea.Ombi/.idea/modules.xml
generated
|
@ -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>
|
7
src/.idea/.idea.Ombi/.idea/projectSettingsUpdater.xml
generated
Normal file
7
src/.idea/.idea.Ombi/.idea/projectSettingsUpdater.xml
generated
Normal 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>
|
222
src/.idea/.idea.Ombi/.idea/workspace.xml
generated
222
src/.idea/.idea.Ombi/.idea/workspace.xml
generated
|
@ -1,25 +1,20 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AutoImportSettings">
|
||||
<option name="autoReloadType" value="SELECTIVE" />
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="57001998-efde-494a-80b3-d7acfc91f770" name="Default Changelist" comment="">
|
||||
<change afterPath="$PROJECT_DIR$/Ombi.Core/Engine/Interfaces/IMusicSearchEngineV2.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/Ombi.Core/Engine/V2/MusicSearchEngineV2.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/Ombi.Core/Models/Search/V2/Music/ArtistInformation.cs" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/.idea.Ombi/.idea/contentModel.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/.idea.Ombi/.idea/contentModel.xml" afterDir="false" />
|
||||
<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$/.idea/config/applicationhost.config" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/config/applicationhost.config" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Ombi.Api.MusicBrainz/Models/Artist/ArtistInformation.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Ombi.Api.MusicBrainz/Models/Artist/ArtistInformation.cs" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Ombi.DependencyInjection/IocExtensions.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Ombi.DependencyInjection/IocExtensions.cs" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Ombi/ClientApp/src/app/media-details/components/artist/artist-details.component.ts" beforeDir="false" afterPath="$PROJECT_DIR$/Ombi/ClientApp/src/app/media-details/components/artist/artist-details.component.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Ombi/ClientApp/src/app/services/searchV2.service.ts" beforeDir="false" afterPath="$PROJECT_DIR$/Ombi/ClientApp/src/app/services/searchV2.service.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Ombi/Controllers/V2/SearchController.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Ombi/Controllers/V2/SearchController.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="EXCLUDED_CONVERTED_TO_IGNORED" value="true" />
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||
</component>
|
||||
<component name="DpaMonitoringSettings">
|
||||
<option name="firstShow" value="false" />
|
||||
</component>
|
||||
<component name="FileEditorManager">
|
||||
<leaf>
|
||||
<file pinned="false" current-in-tab="false">
|
||||
|
@ -237,27 +232,75 @@
|
|||
<component name="Git.Settings">
|
||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$/.." />
|
||||
</component>
|
||||
<component name="GitHubPullRequestSearchHistory">{
|
||||
"lastFilter": {
|
||||
"state": "OPEN",
|
||||
"assignee": "tidusjar"
|
||||
}
|
||||
}</component>
|
||||
<component name="GitToolBoxStore">
|
||||
<option name="recentBranches">
|
||||
<RecentBranches>
|
||||
<option name="branchesForRepo">
|
||||
<list>
|
||||
<RecentBranchesForRepo>
|
||||
<option name="branches">
|
||||
<list>
|
||||
<RecentBranch>
|
||||
<option name="branchName" value="wizard-database" />
|
||||
<option name="lastUsedInstant" value="1735917525" />
|
||||
</RecentBranch>
|
||||
<RecentBranch>
|
||||
<option name="branchName" value="develop" />
|
||||
<option name="lastUsedInstant" value="1735917524" />
|
||||
</RecentBranch>
|
||||
</list>
|
||||
</option>
|
||||
<option name="repositoryRootUrl" value="file://$PROJECT_DIR$/.." />
|
||||
</RecentBranchesForRepo>
|
||||
</list>
|
||||
</option>
|
||||
</RecentBranches>
|
||||
</option>
|
||||
</component>
|
||||
<component name="GithubProjectSettings">
|
||||
<option name="branchProtectionPatterns">
|
||||
<list>
|
||||
<option value="master" />
|
||||
<option value="develop" />
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
<component name="GithubPullRequestsUISettings">{
|
||||
"selectedUrlAndAccountId": {
|
||||
"url": "https://github.com/ombi-app/ombi",
|
||||
"accountId": "22dd09fe-fb9e-48a4-bfcc-3c152edf3f25"
|
||||
}
|
||||
}</component>
|
||||
<component name="HighlightingSettingsPerFile">
|
||||
<setting file="file://$PROJECT_DIR$/Ombi.Helpers.Tests/EmbyHelperTests.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$PROJECT_DIR$/Ombi.Api.MusicBrainz/MusicBrainzApi.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$PROJECT_DIR$/Ombi.Schedule.Tests/OmbiQuartzTests.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$PROJECT_DIR$/Ombi.Api.MusicBrainz/Models/Artist/ArtistInformation.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$PROJECT_DIR$/Ombi/Controllers/V2/SearchController.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$PROJECT_DIR$/Ombi.DependencyInjection/IocExtensions.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$PROJECT_DIR$/Ombi.Core/Engine/V2/MultiSearchEngine.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$PROJECT_DIR$/Ombi.Core/Engine/V2/IMultiSearchEngine.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<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" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/449b441523c469ed34ff5a5e14f0bafcd8f097aa463655303dc19048fa44ac3/EntityFrameworkServiceCollectionExtensions.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/7d81b2d4f22bee75e5438c707251ae43cb0974c207db91ffc159118c84b4eb9/ServiceProvider.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/a424e6912048b4cd25715f158e789aae24db5c2911d9e622d39bc6ac3246c6/MySqlConnectionStringBuilder.cs" root0="SKIP_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/bd1d5c50194fea68ff3559c160230b0ab50f5acf4ce3061bffd6d62958e2182/ExceptionDispatchInfo.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/e9881a453a581134c1a18331ac1f8f1201a5382a685bf2a40777fa22619/DbContextOptions`.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$PROJECT_DIR$/Ombi.Api.MusicBrainz/IMusicBrainzApi.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$PROJECT_DIR$/Ombi.Api.MusicBrainz/MusicBrainzApi.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$PROJECT_DIR$/Ombi.Core/Engine/Interfaces/IMusicSearchEngineV2.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$PROJECT_DIR$/Ombi.Core/Models/Search/V2/Music/ArtistInformation.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="mock:///Dummy.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$PROJECT_DIR$/Ombi.Core/Engine/V2/MusicSearchEngineV2.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$PROJECT_DIR$/Ombi.Core/Engine/RecentlyAddedEngine.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="mock:///Dummy.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$PROJECT_DIR$/Ombi/Controllers/V1/TokenController.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$PROJECT_DIR$/Ombi.Core/Engine/MusicSearchEngine.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$PROJECT_DIR$/Ombi/Program.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$PROJECT_DIR$/Ombi.Core/Engine/RecentlyAddedEngine.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$PROJECT_DIR$/Ombi.Core/Engine/UserStatsEngine.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="mock:///Dummy.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$PROJECT_DIR$/Ombi.Core/Engine/V2/IMultiSearchEngine.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$PROJECT_DIR$/Ombi.Core/Engine/V2/MultiSearchEngine.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$PROJECT_DIR$/Ombi.Core/Engine/V2/MusicSearchEngineV2.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$PROJECT_DIR$/Ombi.Core/Models/Search/V2/Music/ArtistInformation.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$PROJECT_DIR$/Ombi.DependencyInjection/IocExtensions.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$PROJECT_DIR$/Ombi.Helpers.Tests/EmbyHelperTests.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$PROJECT_DIR$/Ombi.Schedule.Tests/OmbiQuartzTests.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$PROJECT_DIR$/Ombi/Controllers/V1/TokenController.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$PROJECT_DIR$/Ombi/Controllers/V2/SearchController.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$PROJECT_DIR$/Ombi/Program.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
</component>
|
||||
<component name="IdeDocumentHistory">
|
||||
<option name="CHANGED_PATHS">
|
||||
|
@ -275,12 +318,17 @@
|
|||
<component name="PackageJsonUpdateNotifier">
|
||||
<dismissed value="$PROJECT_DIR$/Ombi/ClientApp/package.json" />
|
||||
</component>
|
||||
<component name="ProjectColorInfo">{
|
||||
"customColor": "",
|
||||
"associatedIndex": 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>
|
||||
|
@ -343,27 +391,26 @@
|
|||
<pane id="FileSystemExplorer" />
|
||||
</panes>
|
||||
</component>
|
||||
<component name="PropertiesComponent">
|
||||
<property name="ASKED_SHARE_PROJECT_CONFIGURATION_FILES" value="true" />
|
||||
<property name="Rider.DefaultBreakpoints.AreToggled" value="true" />
|
||||
<property name="Rider.ProjectViewActivator.IsNotFirstRun" value="true" />
|
||||
<property name="SHARE_PROJECT_CONFIGURATION_FILES" value="true" />
|
||||
<property name="WebServerToolWindowFactoryState" value="false" />
|
||||
<property name="nodejs_package_manager_path" value="npm" />
|
||||
<component name="ProjectViewState">
|
||||
<option name="hideEmptyMiddlePackages" value="true" />
|
||||
<option name="showLibraryContents" value="true" />
|
||||
</component>
|
||||
<component name="RunDashboard">
|
||||
<option name="ruleStates">
|
||||
<list>
|
||||
<RuleState>
|
||||
<option name="name" value="ConfigurationTypeDashboardGroupingRule" />
|
||||
</RuleState>
|
||||
<RuleState>
|
||||
<option name="name" value="StatusDashboardGroupingRule" />
|
||||
</RuleState>
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
<component name="RunManager" selected=".NET Launch Settings Profile.Ombi: IIS Express">
|
||||
<component name="PropertiesComponent"><![CDATA[{
|
||||
"keyToString": {
|
||||
".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">
|
||||
<configuration name="Ombi" type="LaunchSettings" factoryName=".NET Launch Settings Profile">
|
||||
<option name="LAUNCH_PROFILE_PROJECT_FILE_PATH" value="$PROJECT_DIR$/Ombi/Ombi.csproj" />
|
||||
<option name="LAUNCH_PROFILE_TFM" value=".NETCoreApp,Version=v2.2" />
|
||||
|
@ -376,7 +423,7 @@
|
|||
<option name="SEND_DEBUG_REQUEST" value="1" />
|
||||
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
|
||||
<method v="2">
|
||||
<option name="Build" enabled="true" />
|
||||
<option name="Build" />
|
||||
</method>
|
||||
</configuration>
|
||||
<configuration name="Ombi: IIS Express" type="LaunchSettings" factoryName=".NET Launch Settings Profile">
|
||||
|
@ -391,7 +438,7 @@
|
|||
<option name="SEND_DEBUG_REQUEST" value="1" />
|
||||
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
|
||||
<method v="2">
|
||||
<option name="Build" enabled="true" />
|
||||
<option name="Build" />
|
||||
</method>
|
||||
</configuration>
|
||||
<configuration name="Ombi.Schedule.Tests" type="LaunchSettings" factoryName=".NET Launch Settings Profile">
|
||||
|
@ -406,7 +453,7 @@
|
|||
<option name="SEND_DEBUG_REQUEST" value="1" />
|
||||
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
|
||||
<method v="2">
|
||||
<option name="Build" enabled="true" />
|
||||
<option name="Build" />
|
||||
</method>
|
||||
</configuration>
|
||||
<configuration name="Ombi.Schedule.Tests: IIS Express" type="LaunchSettings" factoryName=".NET Launch Settings Profile">
|
||||
|
@ -421,7 +468,7 @@
|
|||
<option name="SEND_DEBUG_REQUEST" value="1" />
|
||||
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
|
||||
<method v="2">
|
||||
<option name="Build" enabled="true" />
|
||||
<option name="Build" />
|
||||
</method>
|
||||
</configuration>
|
||||
<configuration name="Ombi.Updater" type="LaunchSettings" factoryName=".NET Launch Settings Profile">
|
||||
|
@ -436,10 +483,11 @@
|
|||
<option name="SEND_DEBUG_REQUEST" value="1" />
|
||||
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
|
||||
<method v="2">
|
||||
<option name="Build" enabled="true" />
|
||||
<option name="Build" />
|
||||
</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="" />
|
||||
|
@ -448,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>
|
||||
|
@ -493,7 +544,11 @@
|
|||
</layout>
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
<option name="version" value="1" />
|
||||
<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>
|
||||
|
@ -505,7 +560,7 @@
|
|||
<line-breakpoint enabled="true" type="DotNet Breakpoints">
|
||||
<url>file://$PROJECT_DIR$/Ombi/Controllers/V1/TokenController.cs</url>
|
||||
<line>48</line>
|
||||
<properties documentPath="$PROJECT_DIR$/Ombi/Controllers/V1/TokenController.cs" initialLine="48">
|
||||
<properties documentPath="$PROJECT_DIR$/Ombi/Controllers/V1/TokenController.cs">
|
||||
<startOffsets>
|
||||
<option value="1518" />
|
||||
</startOffsets>
|
||||
|
@ -518,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" initialLine="59">
|
||||
<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" />
|
||||
|
@ -531,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" initialLine="49">
|
||||
<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" />
|
||||
|
@ -544,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" initialLine="30">
|
||||
<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>
|
||||
|
|
14
src/.idea/.idea.Ombi/riderModule.iml
generated
14
src/.idea/.idea.Ombi/riderModule.iml
generated
|
@ -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>
|
25
src/.vscode/tasks.json
vendored
25
src/.vscode/tasks.json
vendored
|
@ -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": []
|
||||
}
|
||||
]
|
||||
}
|
|
@ -29,5 +29,6 @@ namespace Ombi.Api.Plex
|
|||
Task<PlexAddWrapper> AddUser(string emailAddress, string serverId, string authToken, int[] libs);
|
||||
Task<PlexWatchlistContainer> GetWatchlist(string plexToken, CancellationToken cancellationToken);
|
||||
Task<PlexWatchlistMetadataContainer> GetWatchlistMetadata(string ratingKey, string plexToken, CancellationToken cancellationToken);
|
||||
Task<bool> Ping(string authToken, CancellationToken cancellationToken = default);
|
||||
}
|
||||
}
|
|
@ -22,6 +22,18 @@ namespace Ombi.Api.Plex.Models.Friends
|
|||
/// </summary>
|
||||
[XmlAttribute(AttributeName = "home")]
|
||||
public bool HomeUser { get; set; }
|
||||
|
||||
[XmlElement(ElementName = "Server")]
|
||||
public PlexUserServer[] Server { get; set; }
|
||||
}
|
||||
|
||||
public class PlexUserServer
|
||||
{
|
||||
[XmlAttribute(AttributeName = "id")]
|
||||
public string Id { get; set; }
|
||||
|
||||
[XmlAttribute(AttributeName = "serverId")]
|
||||
public string ServerId { get; set; }
|
||||
}
|
||||
|
||||
[XmlRoot(ElementName = "MediaContainer")]
|
||||
|
|
|
@ -68,7 +68,7 @@ namespace Ombi.Api.Plex
|
|||
private const string FriendsUri = "https://plex.tv/api/users";
|
||||
private const string GetAccountUri = "https://plex.tv/users/account.json";
|
||||
private const string ServerUri = "https://plex.tv/pms/servers.xml";
|
||||
private const string WatchlistUri = "https://metadata.provider.plex.tv/";
|
||||
private const string WatchlistUri = "https://discover.provider.plex.tv/";
|
||||
|
||||
/// <summary>
|
||||
/// Sign into the Plex API
|
||||
|
@ -320,6 +320,30 @@ namespace Ombi.Api.Plex
|
|||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pings the Plex API to validate if a token is still valid
|
||||
/// </summary>
|
||||
/// <param name="authToken">The authentication token to validate</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>True if the token is valid, false otherwise</returns>
|
||||
public async Task<bool> Ping(string authToken, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = new Request("api/v2/ping", "https://plex.tv/", HttpMethod.Get);
|
||||
await AddHeaders(request, authToken);
|
||||
|
||||
// We don't need to parse the response, just check if the request succeeds
|
||||
await Api.Request(request, cancellationToken);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If the request fails (401, 403, etc.), the token is invalid
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Adds the required headers and also the authorization header
|
||||
|
|
52
src/Ombi.Core/Authentication/PlexTokenKeepAliveService.cs
Normal file
52
src/Ombi.Core/Authentication/PlexTokenKeepAliveService.cs
Normal file
|
@ -0,0 +1,52 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Ombi.Api.Plex;
|
||||
|
||||
namespace Ombi.Core.Authentication
|
||||
{
|
||||
public interface IPlexTokenKeepAliveService
|
||||
{
|
||||
Task<bool> KeepTokenAliveAsync(string token, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public class PlexTokenKeepAliveService : IPlexTokenKeepAliveService
|
||||
{
|
||||
private readonly IPlexApi _plexApi;
|
||||
private readonly ILogger<PlexTokenKeepAliveService> _logger;
|
||||
|
||||
public PlexTokenKeepAliveService(IPlexApi plexApi, ILogger<PlexTokenKeepAliveService> logger)
|
||||
{
|
||||
_plexApi = plexApi;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<bool> KeepTokenAliveAsync(string token, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
_logger.LogWarning("Token is null or empty");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use the Ping method to validate the token
|
||||
var isValid = await _plexApi.Ping(token, cancellationToken);
|
||||
|
||||
if (!isValid)
|
||||
{
|
||||
_logger.LogWarning("Token validation failed - token may be expired or invalid");
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error occurred while keeping token alive");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -598,13 +598,13 @@ namespace Ombi.Core.Engine
|
|||
|
||||
public async Task<RequestEngineResult> ApproveMovieById(int requestId, bool is4K)
|
||||
{
|
||||
var request = await MovieRepository.Find(requestId);
|
||||
var request = await MovieRepository.GetWithUser().FirstOrDefaultAsync(x => x.Id == requestId);
|
||||
return await ApproveMovie(request, is4K);
|
||||
}
|
||||
|
||||
public async Task<RequestEngineResult> DenyMovieById(int modelId, string denyReason, bool is4K)
|
||||
{
|
||||
var request = await MovieRepository.Find(modelId);
|
||||
var request = await MovieRepository.GetWithUser().FirstOrDefaultAsync(x => x.Id == modelId);
|
||||
if (request == null)
|
||||
{
|
||||
return new RequestEngineResult
|
||||
|
@ -790,7 +790,7 @@ namespace Ombi.Core.Engine
|
|||
|
||||
public async Task<RequestEngineResult> ReProcessRequest(int requestId, bool is4K, CancellationToken cancellationToken)
|
||||
{
|
||||
var request = await MovieRepository.Find(requestId);
|
||||
var request = await MovieRepository.GetWithUser().FirstOrDefaultAsync(x => x.Id == requestId);
|
||||
if (request == null)
|
||||
{
|
||||
return new RequestEngineResult
|
||||
|
@ -805,7 +805,7 @@ namespace Ombi.Core.Engine
|
|||
|
||||
public async Task<RequestEngineResult> MarkUnavailable(int modelId, bool is4K)
|
||||
{
|
||||
var request = await MovieRepository.Find(modelId);
|
||||
var request = await MovieRepository.GetWithUser().FirstOrDefaultAsync(x => x.Id == modelId);
|
||||
if (request == null)
|
||||
{
|
||||
return new RequestEngineResult
|
||||
|
@ -834,7 +834,7 @@ namespace Ombi.Core.Engine
|
|||
|
||||
public async Task<RequestEngineResult> MarkAvailable(int modelId, bool is4K)
|
||||
{
|
||||
var request = await MovieRepository.Find(modelId);
|
||||
var request = await MovieRepository.GetWithUser().FirstOrDefaultAsync(x => x.Id == modelId);
|
||||
if (request == null)
|
||||
{
|
||||
return new RequestEngineResult
|
||||
|
|
67
src/Ombi.Core/Helpers/DatabaseConfigurationSetup.cs
Normal file
67
src/Ombi.Core/Helpers/DatabaseConfigurationSetup.cs
Normal file
|
@ -0,0 +1,67 @@
|
|||
using System;
|
||||
using System.Text;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal;
|
||||
using Ombi.Core.Models;
|
||||
using Polly;
|
||||
using Pomelo.EntityFrameworkCore.MySql.Storage.Internal;
|
||||
|
||||
namespace Ombi.Core.Helpers;
|
||||
|
||||
public static class DatabaseConfigurationSetup
|
||||
{
|
||||
public static void ConfigurePostgres(DbContextOptionsBuilder options, PerDatabaseConfiguration config)
|
||||
{
|
||||
options.UseNpgsql(config.ConnectionString, b =>
|
||||
{
|
||||
b.EnableRetryOnFailure();
|
||||
}).ReplaceService<ISqlGenerationHelper, NpgsqlCaseInsensitiveSqlGenerationHelper>();
|
||||
}
|
||||
|
||||
public static void ConfigureMySql(DbContextOptionsBuilder options, PerDatabaseConfiguration config)
|
||||
{
|
||||
if (string.IsNullOrEmpty(config.ConnectionString))
|
||||
{
|
||||
throw new ArgumentNullException("ConnectionString for the MySql/Mariadb database is empty");
|
||||
}
|
||||
|
||||
options.UseMySql(config.ConnectionString, GetServerVersion(config.ConnectionString), b =>
|
||||
{
|
||||
//b.CharSetBehavior(Pomelo.EntityFrameworkCore.MySql.Infrastructure.CharSetBehavior.NeverAppend); // ##ISSUE, link to migrations?
|
||||
b.EnableRetryOnFailure();
|
||||
});
|
||||
}
|
||||
|
||||
private static ServerVersion GetServerVersion(string connectionString)
|
||||
{
|
||||
// Workaround Windows bug, that can lead to the following exception:
|
||||
//
|
||||
// MySqlConnector.MySqlException (0x80004005): SSL Authentication Error
|
||||
// ---> System.Security.Authentication.AuthenticationException: Authentication failed, see inner exception.
|
||||
// ---> System.ComponentModel.Win32Exception (0x8009030F): The message or signature supplied for verification has been altered
|
||||
//
|
||||
// See https://github.com/dotnet/runtime/issues/17005#issuecomment-305848835
|
||||
//
|
||||
// Also workaround for the fact, that ServerVersion.AutoDetect() does not use any retrying strategy.
|
||||
ServerVersion serverVersion = null;
|
||||
#pragma warning disable EF1001
|
||||
var retryPolicy = Policy.Handle<Exception>(exception => MySqlTransientExceptionDetector.ShouldRetryOn(exception))
|
||||
#pragma warning restore EF1001
|
||||
.WaitAndRetry(3, (count, context) => TimeSpan.FromMilliseconds(count * 250));
|
||||
|
||||
serverVersion = retryPolicy.Execute(() => serverVersion = ServerVersion.AutoDetect(connectionString));
|
||||
|
||||
return serverVersion;
|
||||
}
|
||||
public class NpgsqlCaseInsensitiveSqlGenerationHelper : NpgsqlSqlGenerationHelper
|
||||
{
|
||||
const string EFMigrationsHisory = "__EFMigrationsHistory";
|
||||
public NpgsqlCaseInsensitiveSqlGenerationHelper(RelationalSqlGenerationHelperDependencies dependencies)
|
||||
: base(dependencies) { }
|
||||
public override string DelimitIdentifier(string identifier) =>
|
||||
base.DelimitIdentifier(identifier == EFMigrationsHisory ? identifier : identifier.ToLower());
|
||||
public override void DelimitIdentifier(StringBuilder builder, string identifier)
|
||||
=> base.DelimitIdentifier(builder, identifier == EFMigrationsHisory ? identifier : identifier.ToLower());
|
||||
}
|
||||
}
|
10
src/Ombi.Core/Helpers/FileSystem.cs
Normal file
10
src/Ombi.Core/Helpers/FileSystem.cs
Normal file
|
@ -0,0 +1,10 @@
|
|||
namespace Ombi.Core.Helpers;
|
||||
|
||||
public class FileSystem : IFileSystem
|
||||
{
|
||||
public bool FileExists(string path)
|
||||
{
|
||||
return System.IO.File.Exists(path);
|
||||
}
|
||||
// Implement other file system operations as needed
|
||||
}
|
7
src/Ombi.Core/Helpers/IFileSystem.cs
Normal file
7
src/Ombi.Core/Helpers/IFileSystem.cs
Normal file
|
@ -0,0 +1,7 @@
|
|||
namespace Ombi.Core.Helpers;
|
||||
|
||||
public interface IFileSystem
|
||||
{
|
||||
bool FileExists(string path);
|
||||
// Add other file system operations as needed
|
||||
}
|
40
src/Ombi.Core/Models/DatabaseConfiguration.cs
Normal file
40
src/Ombi.Core/Models/DatabaseConfiguration.cs
Normal file
|
@ -0,0 +1,40 @@
|
|||
using System.IO;
|
||||
|
||||
namespace Ombi.Core.Models;
|
||||
|
||||
public class DatabaseConfiguration
|
||||
{
|
||||
public const string SqliteDatabase = "Sqlite";
|
||||
|
||||
public DatabaseConfiguration()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public DatabaseConfiguration(string defaultSqlitePath)
|
||||
{
|
||||
OmbiDatabase = new PerDatabaseConfiguration(SqliteDatabase, $"Data Source={Path.Combine(defaultSqlitePath, "Ombi.db")}");
|
||||
SettingsDatabase = new PerDatabaseConfiguration(SqliteDatabase, $"Data Source={Path.Combine(defaultSqlitePath, "OmbiSettings.db")}");
|
||||
ExternalDatabase = new PerDatabaseConfiguration(SqliteDatabase, $"Data Source={Path.Combine(defaultSqlitePath, "OmbiExternal.db")}");
|
||||
}
|
||||
public PerDatabaseConfiguration OmbiDatabase { get; set; }
|
||||
public PerDatabaseConfiguration SettingsDatabase { get; set; }
|
||||
public PerDatabaseConfiguration ExternalDatabase { get; set; }
|
||||
}
|
||||
|
||||
public class PerDatabaseConfiguration
|
||||
{
|
||||
public PerDatabaseConfiguration(string type, string connectionString)
|
||||
{
|
||||
Type = type;
|
||||
ConnectionString = connectionString;
|
||||
}
|
||||
|
||||
// Used in Deserialization
|
||||
public PerDatabaseConfiguration()
|
||||
{
|
||||
|
||||
}
|
||||
public string Type { get; set; }
|
||||
public string ConnectionString { get; set; }
|
||||
}
|
|
@ -182,7 +182,10 @@ namespace Ombi.Core.Senders
|
|||
if (settings.SendUserTags)
|
||||
{
|
||||
var userTag = await GetOrCreateTag(model, settings);
|
||||
tags.Add(userTag.id);
|
||||
if (userTag != null)
|
||||
{
|
||||
tags.Add(userTag.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Overrides on the request take priority
|
||||
|
@ -198,7 +201,9 @@ namespace Ombi.Core.Senders
|
|||
List<MovieResponse> movies;
|
||||
// Check if the movie already exists? Since it could be unmonitored
|
||||
|
||||
movies = await _radarrV3Api.GetMovies(settings.ApiKey, settings.FullUri);
|
||||
// Get the appropriate Radarr instance settings for existence check
|
||||
var existenceCheckSettings = is4k ? await _radarr4KSettings.GetSettingsAsync() : settings;
|
||||
movies = await _radarrV3Api.GetMovies(existenceCheckSettings.ApiKey, existenceCheckSettings.FullUri);
|
||||
|
||||
var existingMovie = movies.FirstOrDefault(x => x.tmdbId == model.TheMovieDbId);
|
||||
if (existingMovie == null)
|
||||
|
@ -246,6 +251,12 @@ namespace Ombi.Core.Senders
|
|||
|
||||
private async Task<Tag> GetOrCreateTag(MovieRequests model, RadarrSettings s)
|
||||
{
|
||||
if (model.RequestedUser == null)
|
||||
{
|
||||
_log.LogWarning("Cannot create tag - RequestedUser is null for movie request {MovieTitle}", model.Title);
|
||||
return null;
|
||||
}
|
||||
|
||||
var tagName = model.RequestedUser.UserName;
|
||||
// Does tag exist?
|
||||
|
||||
|
|
|
@ -133,7 +133,14 @@ namespace Ombi.Core.Senders
|
|||
string seriesType;
|
||||
int? tagToUse = null;
|
||||
|
||||
Logger.LogInformation("Starting SendToSonarr for series {Title} (TvDbId: {TvDbId})", model.ParentRequest.Title, model.ParentRequest.TvDbId);
|
||||
Logger.LogInformation("Series type: {SeriesType}", model.SeriesType);
|
||||
|
||||
var profiles = await UserQualityProfiles.GetAll().FirstOrDefaultAsync(x => x.UserId == model.RequestedUserId);
|
||||
if (profiles != null)
|
||||
{
|
||||
Logger.LogInformation("Found user quality profile for user {UserId}", model.RequestedUserId);
|
||||
}
|
||||
|
||||
if (model.SeriesType == SeriesType.Anime)
|
||||
{
|
||||
|
@ -141,8 +148,10 @@ namespace Ombi.Core.Senders
|
|||
// For some reason, if we haven't got one use the first root folder in Sonarr
|
||||
if (!int.TryParse(s.RootPathAnime, out int animePath))
|
||||
{
|
||||
Logger.LogWarning("Failed to parse RootPathAnime: {RootPathAnime}, falling back to main root path", s.RootPathAnime);
|
||||
animePath = int.Parse(s.RootPath); // Set it to the main root folder if we have no anime folder.
|
||||
}
|
||||
Logger.LogInformation("Using anime path ID: {AnimePath}", animePath);
|
||||
rootFolderPath = await GetSonarrRootPath(animePath, s);
|
||||
languageProfileId = s.LanguageProfileAnime > 0 ? s.LanguageProfileAnime : s.LanguageProfile;
|
||||
|
||||
|
@ -154,6 +163,7 @@ namespace Ombi.Core.Senders
|
|||
{
|
||||
if (profiles.SonarrRootPathAnime > 0)
|
||||
{
|
||||
Logger.LogInformation("Using user's anime root path override: {RootPath}", profiles.SonarrRootPathAnime);
|
||||
rootFolderPath = await GetSonarrRootPath(profiles.SonarrRootPathAnime, s);
|
||||
}
|
||||
if (profiles.SonarrQualityProfileAnime > 0)
|
||||
|
@ -169,11 +179,13 @@ namespace Ombi.Core.Senders
|
|||
int.TryParse(s.QualityProfile, out qualityToUse);
|
||||
// Get the root path from the rootfolder selected.
|
||||
// For some reason, if we haven't got one use the first root folder in Sonarr
|
||||
Logger.LogInformation("Using standard path ID: {RootPath}", s.RootPath);
|
||||
rootFolderPath = await GetSonarrRootPath(int.Parse(s.RootPath), s);
|
||||
if (profiles != null)
|
||||
{
|
||||
if (profiles.SonarrRootPath > 0)
|
||||
{
|
||||
Logger.LogInformation("Using user's standard root path override: {RootPath}", profiles.SonarrRootPath);
|
||||
rootFolderPath = await GetSonarrRootPath(profiles.SonarrRootPath, s);
|
||||
}
|
||||
if (profiles.SonarrQualityProfile > 0)
|
||||
|
@ -193,6 +205,7 @@ namespace Ombi.Core.Senders
|
|||
|
||||
if (model.ParentRequest.RootFolder.HasValue && model.ParentRequest.RootFolder.Value > 0)
|
||||
{
|
||||
Logger.LogInformation("Using request root folder override: {RootFolder}", model.ParentRequest.RootFolder.Value);
|
||||
rootFolderPath = await GetSonarrRootPath(model.ParentRequest.RootFolder.Value, s);
|
||||
}
|
||||
|
||||
|
@ -201,6 +214,8 @@ namespace Ombi.Core.Senders
|
|||
languageProfileId = model.ParentRequest.LanguageProfile.Value;
|
||||
}
|
||||
|
||||
Logger.LogInformation("Final root folder path: {RootFolderPath}", rootFolderPath);
|
||||
|
||||
try
|
||||
{
|
||||
if (tagToUse.HasValue)
|
||||
|
@ -520,17 +535,36 @@ namespace Ombi.Core.Senders
|
|||
|
||||
private async Task<string> GetSonarrRootPath(int pathId, SonarrSettings sonarrSettings)
|
||||
{
|
||||
Logger.LogInformation("Getting Sonarr root path for ID: {PathId}", pathId);
|
||||
var rootFoldersResult = await SonarrApi.GetRootFolders(sonarrSettings.ApiKey, sonarrSettings.FullUri);
|
||||
|
||||
if (rootFoldersResult == null || !rootFoldersResult.Any())
|
||||
{
|
||||
Logger.LogError("No root folders returned from Sonarr API");
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
Logger.LogInformation("Found {Count} root folders in Sonarr", rootFoldersResult.Count());
|
||||
foreach (var folder in rootFoldersResult)
|
||||
{
|
||||
Logger.LogDebug("Root folder - ID: {Id}, Path: {Path}", folder.id, folder.path);
|
||||
}
|
||||
|
||||
if (pathId == 0)
|
||||
{
|
||||
return rootFoldersResult.FirstOrDefault().path;
|
||||
var defaultPath = rootFoldersResult.FirstOrDefault()?.path;
|
||||
Logger.LogInformation("Using first root folder as default: {Path}", defaultPath);
|
||||
return defaultPath;
|
||||
}
|
||||
|
||||
foreach (var r in rootFoldersResult?.Where(r => r.id == pathId))
|
||||
var matchingFolder = rootFoldersResult.FirstOrDefault(r => r.id == pathId);
|
||||
if (matchingFolder != null)
|
||||
{
|
||||
return r.path;
|
||||
Logger.LogInformation("Found matching root folder for ID {PathId}: {Path}", pathId, matchingFolder.path);
|
||||
return matchingFolder.path;
|
||||
}
|
||||
|
||||
Logger.LogError("No matching root folder found for ID: {PathId}", pathId);
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
|
69
src/Ombi.Core/Services/DatabaseConfigurationService.cs
Normal file
69
src/Ombi.Core/Services/DatabaseConfigurationService.cs
Normal file
|
@ -0,0 +1,69 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Ombi.Core.Helpers;
|
||||
using Ombi.Core.Models;
|
||||
using Ombi.Helpers;
|
||||
|
||||
namespace Ombi.Core.Services;
|
||||
|
||||
public class DatabaseConfigurationService : IDatabaseConfigurationService
|
||||
{
|
||||
|
||||
private readonly ILogger _logger;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
|
||||
public DatabaseConfigurationService(
|
||||
ILogger<DatabaseConfigurationService> logger,
|
||||
IFileSystem fileSystem)
|
||||
{
|
||||
_logger = logger;
|
||||
_fileSystem = fileSystem;
|
||||
}
|
||||
|
||||
public async Task<bool> ConfigureDatabase(string databaseType, string connectionString, CancellationToken token)
|
||||
{
|
||||
var i = StartupSingleton.Instance;
|
||||
if (string.IsNullOrEmpty(i.StoragePath))
|
||||
{
|
||||
i.StoragePath = string.Empty;
|
||||
}
|
||||
|
||||
var databaseFileLocation = Path.Combine(i.StoragePath, "database.json");
|
||||
if (_fileSystem.FileExists(databaseFileLocation))
|
||||
{
|
||||
var error = $"The database file at '{databaseFileLocation}' already exists";
|
||||
_logger.LogError(error);
|
||||
return false;
|
||||
}
|
||||
|
||||
var configuration = new DatabaseConfiguration
|
||||
{
|
||||
ExternalDatabase = new PerDatabaseConfiguration(databaseType, connectionString),
|
||||
OmbiDatabase = new PerDatabaseConfiguration(databaseType, connectionString),
|
||||
SettingsDatabase = new PerDatabaseConfiguration(databaseType, connectionString)
|
||||
};
|
||||
|
||||
var json = JsonConvert.SerializeObject(configuration, Formatting.Indented);
|
||||
|
||||
_logger.LogInformation("Writing database configuration to file");
|
||||
|
||||
try
|
||||
{
|
||||
await File.WriteAllTextAsync(databaseFileLocation, json, token);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Failed to write database configuration to file");
|
||||
return false;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Database configuration written to file");
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
11
src/Ombi.Core/Services/IDatabaseConfigurationService.cs
Normal file
11
src/Ombi.Core/Services/IDatabaseConfigurationService.cs
Normal file
|
@ -0,0 +1,11 @@
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ombi.Core.Services;
|
||||
|
||||
public interface IDatabaseConfigurationService
|
||||
{
|
||||
const string MySqlDatabase = "MySQL";
|
||||
const string PostgresDatabase = "Postgres";
|
||||
Task<bool> ConfigureDatabase(string databaseType, string connectionString, CancellationToken token);
|
||||
}
|
|
@ -107,6 +107,7 @@ namespace Ombi.DependencyInjection
|
|||
services.AddTransient<IMusicSender, MusicSender>();
|
||||
services.AddTransient<IMassEmailSender, MassEmailSender>();
|
||||
services.AddTransient<IPlexOAuthManager, PlexOAuthManager>();
|
||||
services.AddTransient<IPlexTokenKeepAliveService, PlexTokenKeepAliveService>();
|
||||
services.AddTransient<IVoteEngine, VoteEngine>();
|
||||
services.AddTransient<IDemoMovieSearchEngine, DemoMovieSearchEngine>();
|
||||
services.AddTransient<IDemoTvSearchEngine, DemoTvSearchEngine>();
|
||||
|
@ -236,6 +237,8 @@ namespace Ombi.DependencyInjection
|
|||
services.AddScoped<IFeatureService, FeatureService>();
|
||||
services.AddTransient<IRecentlyRequestedService, RecentlyRequestedService>();
|
||||
services.AddTransient<IPlexService, PlexService>();
|
||||
services.AddSingleton<IFileSystem, FileSystem>();
|
||||
services.AddSingleton<IDatabaseConfigurationService, DatabaseConfigurationService>();
|
||||
}
|
||||
|
||||
public static void RegisterJobs(this IServiceCollection services)
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
IssueResolved = 9,
|
||||
IssueComment = 10,
|
||||
Newsletter = 11,
|
||||
PartiallyAvailable = 12
|
||||
PartiallyAvailable = 12,
|
||||
PlexWatchlistTokenExpired = 13
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -182,8 +182,6 @@ namespace Ombi.Schedule.Tests
|
|||
_mocker.Verify<OmbiUserManager>(x => x.UpdateAsync(It.IsAny<OmbiUser>()), Times.Never);
|
||||
}
|
||||
|
||||
|
||||
|
||||
[Test]
|
||||
public async Task Import_Doesnt_Import_Banned_Users()
|
||||
{
|
||||
|
@ -247,7 +245,15 @@ namespace Ombi.Schedule.Tests
|
|||
Id = "id",
|
||||
Title = "title",
|
||||
Username = "username",
|
||||
HomeUser = true
|
||||
HomeUser = true,
|
||||
Server = new PlexUserServer[]
|
||||
{
|
||||
new PlexUserServer
|
||||
{
|
||||
Id = "1",
|
||||
ServerId = "123"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -257,7 +263,6 @@ namespace Ombi.Schedule.Tests
|
|||
_mocker.Setup<OmbiUserManager, Task<IdentityResult>>(x => x.AddToRoleAsync(It.Is<OmbiUser>(x => x.UserName == "plex"), OmbiRoles.RequestMovie))
|
||||
.ReturnsAsync(IdentityResult.Success);
|
||||
|
||||
|
||||
await _subject.Execute(null);
|
||||
|
||||
_mocker.Verify<OmbiUserManager>(x => x.CreateAsync(It.IsAny<OmbiUser>()), Times.Once);
|
||||
|
@ -306,7 +311,15 @@ namespace Ombi.Schedule.Tests
|
|||
{
|
||||
Email = "email",
|
||||
Id = "id",
|
||||
Username = "plex"
|
||||
Username = "plex",
|
||||
Server = new PlexUserServer[]
|
||||
{
|
||||
new PlexUserServer
|
||||
{
|
||||
Id = "1",
|
||||
ServerId = "123"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -331,9 +344,9 @@ namespace Ombi.Schedule.Tests
|
|||
ImportPlexAdmin = false,
|
||||
ImportPlexUsers = true,
|
||||
DefaultRoles = new List<string>
|
||||
{
|
||||
OmbiRoles.RequestMovie
|
||||
}
|
||||
{
|
||||
OmbiRoles.RequestMovie
|
||||
}
|
||||
});
|
||||
_mocker.Setup<IPlexApi, Task<PlexUsers>>(x => x.GetUsers(It.IsAny<string>())).ReturnsAsync(new PlexUsers
|
||||
{
|
||||
|
@ -343,7 +356,15 @@ namespace Ombi.Schedule.Tests
|
|||
{
|
||||
Email = "email",
|
||||
Id = "PLEX_ID",
|
||||
Username = "user"
|
||||
Username = "user",
|
||||
Server = new PlexUserServer[]
|
||||
{
|
||||
new PlexUserServer
|
||||
{
|
||||
Id = "1",
|
||||
ServerId = "123"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -440,5 +461,98 @@ namespace Ombi.Schedule.Tests
|
|||
|
||||
_mocker.Verify<IUserDeletionEngine>(x => x.DeleteUser(It.Is<OmbiUser>(x => x.ProviderUserId == "ADMIN_ID" && x.Email == "ADMIN@ADMIN.CO" && x.UserName == "Admin")), Times.Never);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Import_Skips_Users_Without_Server_Access()
|
||||
{
|
||||
_mocker.Setup<ISettingsService<UserManagementSettings>, Task<UserManagementSettings>>(x => x.GetSettingsAsync())
|
||||
.ReturnsAsync(new UserManagementSettings { ImportPlexAdmin = false, ImportPlexUsers = true });
|
||||
_mocker.Setup<IPlexApi, Task<PlexUsers>>(x => x.GetUsers(It.IsAny<string>())).ReturnsAsync(new PlexUsers
|
||||
{
|
||||
User = new UserFriends[]
|
||||
{
|
||||
new UserFriends
|
||||
{
|
||||
Email = "email",
|
||||
Id = "NoServer",
|
||||
Title = "title",
|
||||
Username = "username",
|
||||
Server = null
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await _subject.Execute(null);
|
||||
|
||||
_mocker.Verify<OmbiUserManager>(x => x.CreateAsync(It.IsAny<OmbiUser>()), Times.Never);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Import_Skips_Users_With_Empty_Server_Array()
|
||||
{
|
||||
_mocker.Setup<ISettingsService<UserManagementSettings>, Task<UserManagementSettings>>(x => x.GetSettingsAsync())
|
||||
.ReturnsAsync(new UserManagementSettings { ImportPlexAdmin = false, ImportPlexUsers = true });
|
||||
_mocker.Setup<IPlexApi, Task<PlexUsers>>(x => x.GetUsers(It.IsAny<string>())).ReturnsAsync(new PlexUsers
|
||||
{
|
||||
User = new UserFriends[]
|
||||
{
|
||||
new UserFriends
|
||||
{
|
||||
Email = "email",
|
||||
Id = "EmptyServer",
|
||||
Title = "title",
|
||||
Username = "username",
|
||||
Server = new PlexUserServer[0]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await _subject.Execute(null);
|
||||
|
||||
_mocker.Verify<OmbiUserManager>(x => x.CreateAsync(It.IsAny<OmbiUser>()), Times.Never);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Import_Creates_User_With_Server_Access()
|
||||
{
|
||||
_mocker.Setup<ISettingsService<UserManagementSettings>, Task<UserManagementSettings>>(x => x.GetSettingsAsync())
|
||||
.ReturnsAsync(new UserManagementSettings { ImportPlexAdmin = false, ImportPlexUsers = true });
|
||||
_mocker.Setup<IPlexApi, Task<PlexUsers>>(x => x.GetUsers(It.IsAny<string>())).ReturnsAsync(new PlexUsers
|
||||
{
|
||||
User = new UserFriends[]
|
||||
{
|
||||
new UserFriends
|
||||
{
|
||||
Email = "email",
|
||||
Id = "HasServer",
|
||||
Title = "title",
|
||||
Username = "username",
|
||||
Server = new PlexUserServer[]
|
||||
{
|
||||
new PlexUserServer
|
||||
{
|
||||
Id = "1",
|
||||
ServerId = "123"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
_mocker.Setup<OmbiUserManager, Task<IdentityResult>>(x => x.CreateAsync(It.Is<OmbiUser>(x =>
|
||||
x.UserName == "username" &&
|
||||
x.Email == "email" &&
|
||||
x.ProviderUserId == "HasServer" &&
|
||||
x.UserType == UserType.PlexUser)))
|
||||
.ReturnsAsync(IdentityResult.Success);
|
||||
|
||||
await _subject.Execute(null);
|
||||
|
||||
_mocker.Verify<OmbiUserManager>(x => x.CreateAsync(It.Is<OmbiUser>(x =>
|
||||
x.UserName == "username" &&
|
||||
x.Email == "email" &&
|
||||
x.ProviderUserId == "HasServer" &&
|
||||
x.UserType == UserType.PlexUser)), Times.Once);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,11 @@ 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;
|
||||
using Ombi.Core.Authentication;
|
||||
|
||||
namespace Ombi.Schedule.Tests
|
||||
{
|
||||
|
@ -35,12 +40,15 @@ 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);
|
||||
// Mock the keep-alive service to return true by default
|
||||
_mocker.Use<IPlexTokenKeepAliveService>(Mock.Of<IPlexTokenKeepAliveService>(s => s.KeepTokenAliveAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()) == Task.FromResult(true)));
|
||||
_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 +785,99 @@ 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);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task SkipsUserIfTokenKeepAliveFails()
|
||||
{
|
||||
// Arrange: Set up the keep-alive service to return false (token invalid/expired)
|
||||
var keepAliveMock = new Mock<IPlexTokenKeepAliveService>();
|
||||
keepAliveMock.Setup(x => x.KeepTokenAliveAsync(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(false);
|
||||
_mocker.Use(keepAliveMock.Object);
|
||||
_subject = _mocker.CreateInstance<PlexWatchlistImport>();
|
||||
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
|
||||
// Act
|
||||
await _subject.Execute(_context.Object);
|
||||
// Assert: Should not attempt to import watchlist if keep-alive fails
|
||||
keepAliveMock.Verify(x => x.KeepTokenAliveAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
_mocker.Verify<IPlexApi>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
_mocker.Verify<INotificationHelper>(x => x.Notify(It.IsAny<NotificationOptions>()), Times.Never); // or Times.Once if notification is expected
|
||||
}
|
||||
[Test]
|
||||
public async Task CallsKeepAliveForEachPlexUser()
|
||||
{
|
||||
// Arrange: Multiple Plex users
|
||||
var users = new List<OmbiUser>
|
||||
{
|
||||
new OmbiUser { Id = "abc1", UserType = UserType.PlexUser, MediaServerToken = "abc1", UserName = "abc1", NormalizedUserName = "ABC1" },
|
||||
new OmbiUser { Id = "abc2", UserType = UserType.PlexUser, MediaServerToken = "abc2", UserName = "abc2", NormalizedUserName = "ABC2" },
|
||||
};
|
||||
var um = MockHelper.MockUserManager(users);
|
||||
_mocker.Use(um);
|
||||
var keepAliveMock = new Mock<IPlexTokenKeepAliveService>();
|
||||
keepAliveMock.Setup(x => x.KeepTokenAliveAsync(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(true);
|
||||
_mocker.Use(keepAliveMock.Object);
|
||||
_subject = _mocker.CreateInstance<PlexWatchlistImport>();
|
||||
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
|
||||
// Act
|
||||
await _subject.Execute(_context.Object);
|
||||
// Assert: KeepAlive should be called for each user
|
||||
keepAliveMock.Verify(x => x.KeepTokenAliveAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Exactly(users.Count));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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/"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -120,6 +120,13 @@ namespace Ombi.Schedule.Jobs.Plex
|
|||
|
||||
foreach (var plexUser in users.User)
|
||||
{
|
||||
// Skip users without server access
|
||||
if (plexUser.Server == null || !plexUser.Server.Any())
|
||||
{
|
||||
_log.LogInformation($"Skipping user {plexUser.Username ?? plexUser.Id} as they have no server access");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if we should import this user
|
||||
if (userManagementSettings.BannedPlexUserIds.Contains(plexUser.Id))
|
||||
{
|
||||
|
|
|
@ -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,13 @@ namespace Ombi.Schedule.Jobs.Plex
|
|||
private readonly IExternalRepository<PlexWatchlistHistory> _watchlistRepo;
|
||||
private readonly IRepository<PlexWatchlistUserError> _userError;
|
||||
private readonly IMovieDbApi _movieDbApi;
|
||||
private readonly INotificationHelper _notificationHelper;
|
||||
private readonly IPlexTokenKeepAliveService _tokenKeepAliveService;
|
||||
|
||||
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, IPlexTokenKeepAliveService tokenKeepAliveService)
|
||||
{
|
||||
_plexApi = plexApi;
|
||||
_settings = settings;
|
||||
|
@ -53,6 +60,8 @@ namespace Ombi.Schedule.Jobs.Plex
|
|||
_watchlistRepo = watchlistRepo;
|
||||
_userError = userError;
|
||||
_movieDbApi = movieDbApi;
|
||||
_notificationHelper = notificationHelper;
|
||||
_tokenKeepAliveService = tokenKeepAliveService;
|
||||
}
|
||||
|
||||
public async Task Execute(IJobExecutionContext context)
|
||||
|
@ -90,6 +99,36 @@ namespace Ombi.Schedule.Jobs.Plex
|
|||
}
|
||||
|
||||
_logger.LogDebug($"Starting Watchlist Import for {user.UserName} with token {user.MediaServerToken}");
|
||||
|
||||
// Keep the token alive before attempting watchlist import
|
||||
var keepAliveSuccess = await _tokenKeepAliveService.KeepTokenAliveAsync(user.MediaServerToken, context?.CancellationToken ?? CancellationToken.None);
|
||||
if (!keepAliveSuccess)
|
||||
{
|
||||
_logger.LogWarning($"Token for user '{user.UserName}' is invalid or expired (keep-alive failed). Recording error and skipping.");
|
||||
await _userError.Add(new PlexWatchlistUserError
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
var watchlist = await _plexApi.GetWatchlist(user.MediaServerToken, context?.CancellationToken ?? CancellationToken.None);
|
||||
if (watchlist?.AuthError ?? false)
|
||||
{
|
||||
|
@ -99,6 +138,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))
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -5,13 +5,17 @@
|
|||
<mat-button-toggle id="{{id}}Tv" [ngClass]="{'button-active': discoverOptions === DiscoverOption.Tv}" value="{{DiscoverOption.Tv}}" class="discover-filter-button">{{'Discovery.Tv' | translate}}</mat-button-toggle>
|
||||
</mat-button-toggle-group>
|
||||
</div>
|
||||
@defer (when discoverResults.length > 0) {
|
||||
@defer (when discoverResults.length > 0; prefetch on idle) {
|
||||
<p-carousel #carousel [numVisible]="10" [numScroll]="10" [page]="0" [value]="discoverResults" [responsiveOptions]="responsiveOptions" (onPage)="newPage()">
|
||||
<ng-template let-result pTemplate="item">
|
||||
<discover-card [discoverType]="discoverType" [isAdmin]="isAdmin" [result]="result" [is4kEnabled]="is4kEnabled"></discover-card>
|
||||
</ng-template>
|
||||
</p-carousel>
|
||||
}
|
||||
@placeholder(minimum 500) {
|
||||
<p-skeleton width="100%" height="18rem"></p-skeleton>
|
||||
@placeholder(minimum 300) {
|
||||
<div class="row loading-container">
|
||||
<div class="col-2" *ngFor="let item of [1,2,3,4,5,6,7,8,9,10]">
|
||||
<p-skeleton width="100%" height="270px"></p-skeleton>
|
||||
</div>
|
||||
</div>
|
||||
}
|
|
@ -105,6 +105,30 @@
|
|||
padding: 5px;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding: 0 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.loading-container .col-2 {
|
||||
flex: 0 0 auto;
|
||||
width: calc(10% - 9px);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.loading-container .col-2 {
|
||||
width: calc(50% - 5px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.loading-container .col-2 {
|
||||
width: calc(100% - 0px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width:755px){
|
||||
::ng-deep .p-carousel-item{
|
||||
flex: 1 0 200px !important;
|
||||
|
|
|
@ -43,7 +43,7 @@ export class CarouselListComponent implements OnInit {
|
|||
get mediaTypeStorageKey() {
|
||||
return "DiscoverOptions" + this.discoverType.toString();
|
||||
};
|
||||
private amountToLoad = 17;
|
||||
private amountToLoad = 10;
|
||||
private currentlyLoaded = 0;
|
||||
private baseUrl: string = "";
|
||||
|
||||
|
@ -148,6 +148,7 @@ export class CarouselListComponent implements OnInit {
|
|||
}
|
||||
|
||||
public async ngOnInit() {
|
||||
|
||||
this.is4kEnabled = this.featureFacade.is4kEnabled();
|
||||
this.currentlyLoaded = 0;
|
||||
const localDiscoverOptions = +this.storageService.get(this.mediaTypeStorageKey);
|
||||
|
@ -155,11 +156,15 @@ export class CarouselListComponent implements OnInit {
|
|||
this.discoverOptions = DiscoverOption[DiscoverOption[localDiscoverOptions]];
|
||||
}
|
||||
|
||||
let currentIteration = 0;
|
||||
while (this.discoverResults.length <= 14 && currentIteration <= 3) {
|
||||
currentIteration++;
|
||||
// Load initial data - just enough to fill the first carousel page
|
||||
// This reduces initial API calls and improves loading performance
|
||||
await this.loadData(false);
|
||||
|
||||
// If we don't have enough results to fill the carousel, load one more batch
|
||||
if (this.discoverResults.length < 10) {
|
||||
await this.loadData(false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public async toggleChanged(event: MatButtonToggleChange) {
|
||||
|
|
|
@ -1,46 +1,108 @@
|
|||
<div class="small-middle-container">
|
||||
<div class="section">
|
||||
<h2 id="genreHeading" data-toggle="collapse" href="#genreCollapse" role="button">{{ 'Discovery.Genres' | translate }}</h2>
|
||||
<genre-button-select class="collapse show" id="genreCollapse"></genre-button-select>
|
||||
</div>
|
||||
<div class="section">
|
||||
<h2>{{ 'Discovery.RecentlyRequestedTab' | translate }}</h2>
|
||||
<div>
|
||||
<ombi-recently-list [id]="'recentlyRequested'"></ombi-recently-list>
|
||||
@defer (on viewport; prefetch on idle) {
|
||||
<div class="section">
|
||||
<h2 id="genreHeading" data-toggle="collapse" href="#genreCollapse" role="button">{{ 'Discovery.Genres' | translate }}</h2>
|
||||
<genre-button-select class="collapse show" id="genreCollapse"></genre-button-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="section" [hidden]="!showSeasonal">
|
||||
<h2>{{ 'Discovery.SeasonalTab' | translate }}</h2>
|
||||
<div>
|
||||
<carousel-list
|
||||
[id]="'seasonal'"
|
||||
[isAdmin]="isAdmin"
|
||||
[discoverType]="DiscoverType.Seasonal"
|
||||
(movieCount)="setSeasonalMovieCount($event)"
|
||||
></carousel-list>
|
||||
} @placeholder(minimum 300) {
|
||||
<div class="section">
|
||||
<h2>{{ 'Discovery.Genres' | translate }}</h2>
|
||||
<p-skeleton width="100%" height="60px"></p-skeleton>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="section">
|
||||
<h2>{{ 'Discovery.PopularTab' | translate }}</h2>
|
||||
<div>
|
||||
<carousel-list [id]="'popular'" [isAdmin]="isAdmin" [discoverType]="DiscoverType.Popular"></carousel-list>
|
||||
@defer (on viewport; prefetch on idle) {
|
||||
<div class="section">
|
||||
<h2>{{ 'Discovery.RecentlyRequestedTab' | translate }}</h2>
|
||||
<div>
|
||||
<ombi-recently-list [id]="'recentlyRequested'"></ombi-recently-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
} @placeholder(minimum 300) {
|
||||
<div class="section">
|
||||
<h2>{{ 'Discovery.RecentlyRequestedTab' | translate }}</h2>
|
||||
<div class="row loading-container">
|
||||
<div class="col-2" *ngFor="let item of [1,2,3,4,5]">
|
||||
<p-skeleton width="100%" height="270px"></p-skeleton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="section">
|
||||
<h2>{{ 'Discovery.TrendingTab' | translate }}</h2>
|
||||
<div>
|
||||
<carousel-list [id]="'trending'" [isAdmin]="isAdmin" [discoverType]="DiscoverType.Trending"></carousel-list>
|
||||
@defer (on viewport; prefetch on idle) {
|
||||
<div class="section" [hidden]="!showSeasonal">
|
||||
<h2>{{ 'Discovery.SeasonalTab' | translate }}</h2>
|
||||
<div>
|
||||
<carousel-list
|
||||
[id]="'seasonal'"
|
||||
[isAdmin]="isAdmin"
|
||||
[discoverType]="DiscoverType.Seasonal"
|
||||
(movieCount)="setSeasonalMovieCount($event)"
|
||||
></carousel-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
} @placeholder(minimum 300) {
|
||||
<div class="section">
|
||||
<h2>{{ 'Discovery.SeasonalTab' | translate }}</h2>
|
||||
<div class="row loading-container">
|
||||
<div class="col-2" *ngFor="let item of [1,2,3,4,5,6,7,8,9,10]">
|
||||
<p-skeleton width="100%" height="270px"></p-skeleton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="section">
|
||||
<h2>{{ 'Discovery.UpcomingTab' | translate }}</h2>
|
||||
<div>
|
||||
<carousel-list [id]="'upcoming'" [isAdmin]="isAdmin" [discoverType]="DiscoverType.Upcoming"></carousel-list>
|
||||
@defer (on viewport; prefetch on idle) {
|
||||
<div class="section">
|
||||
<h2>{{ 'Discovery.PopularTab' | translate }}</h2>
|
||||
<div>
|
||||
<carousel-list [id]="'popular'" [isAdmin]="isAdmin" [discoverType]="DiscoverType.Popular"></carousel-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
} @placeholder(minimum 300) {
|
||||
<div class="section">
|
||||
<h2>{{ 'Discovery.PopularTab' | translate }}</h2>
|
||||
<div class="row loading-container">
|
||||
<div class="col-2" *ngFor="let item of [1,2,3,4,5,6,7,8,9,10]">
|
||||
<p-skeleton width="100%" height="270px"></p-skeleton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@defer (on viewport; prefetch on idle) {
|
||||
<div class="section">
|
||||
<h2>{{ 'Discovery.TrendingTab' | translate }}</h2>
|
||||
<div>
|
||||
<carousel-list [id]="'trending'" [isAdmin]="isAdmin" [discoverType]="DiscoverType.Trending"></carousel-list>
|
||||
</div>
|
||||
</div>
|
||||
} @placeholder(minimum 300) {
|
||||
<div class="section">
|
||||
<h2>{{ 'Discovery.TrendingTab' | translate }}</h2>
|
||||
<div class="row loading-container">
|
||||
<div class="col-2" *ngFor="let item of [1,2,3,4,5,6,7,8,9,10]">
|
||||
<p-skeleton width="100%" height="270px"></p-skeleton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@defer (on viewport; prefetch on idle) {
|
||||
<div class="section">
|
||||
<h2>{{ 'Discovery.UpcomingTab' | translate }}</h2>
|
||||
<div>
|
||||
<carousel-list [id]="'upcoming'" [isAdmin]="isAdmin" [discoverType]="DiscoverType.Upcoming"></carousel-list>
|
||||
</div>
|
||||
</div>
|
||||
} @placeholder(minimum 300) {
|
||||
<div class="section">
|
||||
<h2>{{ 'Discovery.UpcomingTab' | translate }}</h2>
|
||||
<div class="row loading-container">
|
||||
<div class="col-2" *ngFor="let item of [1,2,3,4,5,6,7,8,9,10]">
|
||||
<p-skeleton width="100%" height="270px"></p-skeleton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
|
|
@ -9,4 +9,28 @@ h2{
|
|||
margin-top:40px;
|
||||
margin-left:40px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding: 0 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.loading-container .col-2 {
|
||||
flex: 0 0 auto;
|
||||
width: calc(10% - 9px);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.loading-container .col-2 {
|
||||
width: calc(50% - 5px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.loading-container .col-2 {
|
||||
width: calc(100% - 0px);
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
@defer (when requests()) {
|
||||
@defer (when requests(); prefetch on idle) {
|
||||
<div *ngIf="requests().length > 0">
|
||||
<p-carousel #carousel [value]="requests()" [numVisible]="3" [numScroll]="1"
|
||||
[responsiveOptions]="responsiveOptions" [page]="0">
|
||||
|
@ -13,21 +13,9 @@
|
|||
</ng-template>
|
||||
</p-carousel>
|
||||
</div>
|
||||
}@placeholder(minimum 500) {
|
||||
}@placeholder(minimum 300) {
|
||||
<div class="row loading-container">
|
||||
<div class="col-2">
|
||||
<p-skeleton width="100%" height="270px"></p-skeleton>
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<p-skeleton width="100%" height="270px"></p-skeleton>
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<p-skeleton width="100%" height="270px"></p-skeleton>
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<p-skeleton width="100%" height="270px"></p-skeleton>
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<div class="col-2" *ngFor="let item of [1,2,3,4,5]">
|
||||
<p-skeleton width="100%" height="270px"></p-skeleton>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -105,12 +105,32 @@
|
|||
padding: 5px;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding: 0 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.loading-container .col-2 {
|
||||
flex: 0 0 auto;
|
||||
width: calc(20% - 8px);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.loading-container .col-2 {
|
||||
width: calc(50% - 5px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.loading-container .col-2 {
|
||||
width: calc(100% - 0px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width:755px){
|
||||
::ng-deep .p-carousel-item{
|
||||
flex: 1 0 200px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
margin-left: 10rem;
|
||||
}
|
|
@ -52,6 +52,7 @@ export enum NotificationType {
|
|||
IssueComment = 10,
|
||||
Newsletter = 11,
|
||||
PartiallyAvailable = 12,
|
||||
PlexWatchlistTokenExpired = 13
|
||||
}
|
||||
|
||||
export interface IDiscordNotifcationSettings extends INotificationSettings {
|
||||
|
|
|
@ -114,6 +114,7 @@ export interface IPlexSettings extends ISettings {
|
|||
enable: boolean;
|
||||
enableWatchlistImport: boolean;
|
||||
monitorAll: boolean;
|
||||
notifyOnWatchlistTokenExpiration: boolean;
|
||||
servers: IPlexServer[];
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { Pipe, PipeTransform } from "@angular/core";
|
||||
import { FormatPipe } from 'ngx-date-fns';
|
||||
import { parseISO, format } from 'date-fns';
|
||||
|
||||
@Pipe({
|
||||
name: "ombiDate",
|
||||
|
@ -10,8 +11,16 @@ export class OmbiDatePipe implements PipeTransform {
|
|||
private FormatPipe: FormatPipe,
|
||||
) {}
|
||||
|
||||
public transform(value: string, format: string ) {
|
||||
const date = new Date(value);
|
||||
return this.FormatPipe.transform(date, format);
|
||||
public transform(value: string, formatStr: string ) {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Parse the ISO string as UTC
|
||||
const utcDate = parseISO(value);
|
||||
|
||||
// Format the date using date-fns format function
|
||||
// This will automatically handle the UTC to local conversion
|
||||
return format(utcDate, formatStr);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
|
@ -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}}-->
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
<settings-menu></settings-menu>
|
||||
<div *ngIf="form" class="small-middle-container">
|
||||
<div *ngIf="form$ | async as form" class="small-middle-container">
|
||||
<fieldset>
|
||||
<legend>Radarr Settings</legend>
|
||||
<div class="md-form-field" style="margin-top:1em;"></div>
|
||||
|
@ -33,4 +33,4 @@
|
|||
</div>
|
||||
</form>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Component, OnInit, QueryList, ViewChildren } from "@angular/core";
|
||||
import { Component, OnDestroy, OnInit, QueryList, ViewChildren } from "@angular/core";
|
||||
import { UntypedFormBuilder, UntypedFormGroup } from "@angular/forms";
|
||||
import { RadarrFacade } from "app/state/radarr";
|
||||
|
||||
|
@ -6,36 +6,41 @@ import { IMinimumAvailability, IRadarrCombined, IRadarrProfile, IRadarrRootFolde
|
|||
import { NotificationService } from "../../services";
|
||||
import { FeaturesFacade } from "../../state/features/features.facade";
|
||||
import { RadarrFormComponent } from "./components/radarr-form.component";
|
||||
import { Observable, ReplaySubject, Subject, combineLatest, map, switchMap, takeUntil, tap } from "rxjs";
|
||||
|
||||
@Component({
|
||||
templateUrl: "./radarr.component.html",
|
||||
styleUrls: ["./radarr.component.scss"]
|
||||
})
|
||||
export class RadarrComponent implements OnInit {
|
||||
export class RadarrComponent implements OnInit, OnDestroy {
|
||||
|
||||
public qualities: IRadarrProfile[];
|
||||
public rootFolders: IRadarrRootFolder[];
|
||||
public minimumAvailabilityOptions: IMinimumAvailability[];
|
||||
public profilesRunning: boolean;
|
||||
public rootFoldersRunning: boolean;
|
||||
public form: UntypedFormGroup;
|
||||
public is4kEnabled: boolean = false;
|
||||
|
||||
@ViewChildren('4kForm') public form4k: QueryList<RadarrFormComponent>;
|
||||
@ViewChildren('normalForm') public normalForm: QueryList<RadarrFormComponent>;
|
||||
public readonly form$: Observable<UntypedFormGroup>;
|
||||
|
||||
constructor(private radarrFacade: RadarrFacade,
|
||||
private notificationService: NotificationService,
|
||||
private featureFacade: FeaturesFacade,
|
||||
private fb: UntypedFormBuilder) { }
|
||||
private readonly form4k$: ReplaySubject<QueryList<RadarrFormComponent>>;
|
||||
private readonly normalForm$: ReplaySubject<QueryList<RadarrFormComponent>>;
|
||||
private readonly destroyed$: Subject<void>;
|
||||
|
||||
constructor(
|
||||
private readonly radarrFacade: RadarrFacade,
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly featureFacade: FeaturesFacade,
|
||||
readonly fb: UntypedFormBuilder
|
||||
) {
|
||||
this.form4k$ = new ReplaySubject();
|
||||
this.normalForm$ = new ReplaySubject();
|
||||
this.destroyed$ = new Subject();
|
||||
|
||||
public ngOnInit() {
|
||||
this.is4kEnabled = this.featureFacade.is4kEnabled();
|
||||
this.radarrFacade.state$()
|
||||
.subscribe(x => {
|
||||
this.form = this.fb.group({
|
||||
radarr: this.fb.group({
|
||||
this.form$ = radarrFacade.state$()
|
||||
.pipe(
|
||||
map(x => fb.group({
|
||||
radarr: fb.group({
|
||||
enabled: [x.settings.radarr.enabled],
|
||||
apiKey: [x.settings.radarr.apiKey],
|
||||
defaultQualityProfile: [+x.settings.radarr.defaultQualityProfile],
|
||||
|
@ -50,7 +55,7 @@ export class RadarrComponent implements OnInit {
|
|||
minimumAvailability: [x.settings.radarr.minimumAvailability],
|
||||
scanForAvailability: [x.settings.radarr.scanForAvailability]
|
||||
}),
|
||||
radarr4K: this.fb.group({
|
||||
radarr4K: fb.group({
|
||||
enabled: [x.settings.radarr4K.enabled],
|
||||
apiKey: [x.settings.radarr4K.apiKey],
|
||||
defaultQualityProfile: [+x.settings.radarr4K.defaultQualityProfile],
|
||||
|
@ -65,19 +70,44 @@ export class RadarrComponent implements OnInit {
|
|||
minimumAvailability: [x.settings.radarr4K.minimumAvailability],
|
||||
scanForAvailability: [x.settings.radarr4K.scanForAvailability]
|
||||
}),
|
||||
});
|
||||
this.normalForm.changes.forEach((comp => {
|
||||
comp.first.toggleValidators();
|
||||
}))
|
||||
if (this.is4kEnabled) {
|
||||
this.form4k.changes.forEach((comp => {
|
||||
comp.first.toggleValidators();
|
||||
}))
|
||||
}
|
||||
});
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
@ViewChildren('4kForm')
|
||||
protected set form4k(component: QueryList<RadarrFormComponent>) {
|
||||
this.form4k$.next(component);
|
||||
}
|
||||
|
||||
@ViewChildren('normalForm')
|
||||
protected set normalForm(component: QueryList<RadarrFormComponent>) {
|
||||
this.normalForm$.next(component);
|
||||
}
|
||||
|
||||
public ngOnInit() {
|
||||
this.is4kEnabled = this.featureFacade.is4kEnabled();
|
||||
|
||||
combineLatest([this.form$, this.normalForm$])
|
||||
.pipe(
|
||||
switchMap(([, normalForm]) => normalForm.changes),
|
||||
tap(comp => comp.first.toggleValidators()),
|
||||
takeUntil(this.destroyed$)
|
||||
).subscribe();
|
||||
|
||||
if (this.is4kEnabled) {
|
||||
combineLatest([this.form$, this.form4k$])
|
||||
.pipe(
|
||||
switchMap(([, form4k]) => form4k.changes),
|
||||
tap(comp => comp.first.toggleValidators()),
|
||||
takeUntil(this.destroyed$)
|
||||
).subscribe();
|
||||
}
|
||||
}
|
||||
|
||||
public ngOnDestroy(): void {
|
||||
this.destroyed$.next();
|
||||
this.destroyed$.complete();
|
||||
}
|
||||
|
||||
public onSubmit(form: UntypedFormGroup) {
|
||||
if (form.invalid) {
|
||||
|
|
|
@ -18,7 +18,7 @@ export class RadarrSettingsState {
|
|||
|
||||
@Action(LoadSettings)
|
||||
public load({ setState }: StateContext<RadarrState>): Observable<RadarrState> {
|
||||
const isAdmin = this.authService.hasRole("Admin");
|
||||
const isAdmin = this.authService.isAdmin();
|
||||
const calls = isAdmin ? [this.settingsService.getRadarr()] : [of({})];
|
||||
|
||||
return combineLatest(calls).pipe(
|
||||
|
|
|
@ -18,7 +18,7 @@ export class SonarrSettingsState {
|
|||
|
||||
@Action(LoadSettings)
|
||||
public load({ setState }: StateContext<SonarrState>): Observable<SonarrState> {
|
||||
const isAdmin = this.authService.hasRole("Admin");
|
||||
const isAdmin = this.authService.isAdmin();
|
||||
const calls = isAdmin ? [this.sonarrService.getVersion(), this.settingsService.getSonarr()] : [of(""), of({})];
|
||||
|
||||
return combineLatest(calls).pipe(
|
||||
|
|
|
@ -37,6 +37,13 @@ export class UserManagementUserComponent implements OnInit {
|
|||
private appUrl: string = this.customizationFacade.appUrl();
|
||||
private accessToken: string;
|
||||
|
||||
// List of excluded notification agents that should not be shown in user preferences
|
||||
private readonly excludedAgents = [
|
||||
INotificationAgent.Email,
|
||||
INotificationAgent.Mobile,
|
||||
INotificationAgent.Webhook
|
||||
];
|
||||
|
||||
constructor(private identityService: IdentityService,
|
||||
private notificationService: MessageService,
|
||||
private router: Router,
|
||||
|
@ -74,9 +81,15 @@ export class UserManagementUserComponent implements OnInit {
|
|||
}
|
||||
});
|
||||
if(this.edit) {
|
||||
this.identityService.getNotificationPreferencesForUser(this.userId).subscribe(x => this.notificationPreferences = x);
|
||||
this.identityService.getNotificationPreferencesForUser(this.userId).subscribe(x => {
|
||||
// Filter out excluded notification agents
|
||||
this.notificationPreferences = x.filter(pref => !this.excludedAgents.includes(pref.agent));
|
||||
});
|
||||
} else {
|
||||
this.identityService.getNotificationPreferences().subscribe(x => this.notificationPreferences = x);
|
||||
this.identityService.getNotificationPreferences().subscribe(x => {
|
||||
// Filter out excluded notification agents
|
||||
this.notificationPreferences = x.filter(pref => !this.excludedAgents.includes(pref.agent));
|
||||
});
|
||||
}
|
||||
this.sonarrService.getQualityProfilesWithoutSettings().subscribe(x => {
|
||||
this.sonarrQualities = x;
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
<div class="mediaserver-container">
|
||||
<div class="left-container mediaserver">
|
||||
<i class="fa fa-database text-logo"></i>
|
||||
</div>
|
||||
<div class="right-container mediaserver">
|
||||
<div class="right-container-content mediaserver">
|
||||
<h1>Choose a Database</h1>
|
||||
<h4>
|
||||
SQLite is the default option and the easiest to set up, as it requires no additional configuration.
|
||||
<br>However, it has significant limitations, including potential performance issues and database locking.
|
||||
<br>While many users start with SQLite and later migrate to MySQL or MariaDB, we <b>recommend</b> beginning with MySQL or MariaDB from the start for a more robust and scalable experience.
|
||||
<br/>
|
||||
<br/>
|
||||
For more information on using alternate databases, <a target="_blank" href="https://docs.ombi.app/info/alternate-databases/">see the documentation.</a>
|
||||
</h4>
|
||||
<form [formGroup]="form">
|
||||
<mat-tab-group (selectedTabChange)="tabChange($event)">
|
||||
<mat-tab label="SQLite">
|
||||
<p class="space-or">
|
||||
Just press next to continue with SQLite
|
||||
</p>
|
||||
</mat-tab>
|
||||
<mat-tab label="MySQL/MariaDB">
|
||||
<p class="space-or">
|
||||
Please enter your MySQL/MariaDB connection details below
|
||||
</p>
|
||||
<div>
|
||||
<mat-form-field>
|
||||
<input matInput type="text" formControlName="host" id="host" placeholder="Host">
|
||||
<mat-error>This field is required</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field>
|
||||
<input matInput type="number" formControlName="port" id="port" placeholder="Port">
|
||||
<mat-error>This field is required</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field>
|
||||
<input matInput type="text" formControlName="name" id="database" placeholder="Database Name">
|
||||
<mat-error>This field is required</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field>
|
||||
<input matInput type="text" formControlName="user" id="user" placeholder="User">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field>
|
||||
<input matInput type="password" formControlName="password" id="password" placeholder="Password">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<p>{{connectionString | async}}</p>
|
||||
<div style="text-align: center; margin-top: 20px">
|
||||
<button (click)="save()" id="databaseSave" mat-raised-button color="accent" type="button" class="viewon-btn database" [disabled]="form.invalid">Save</button>
|
||||
<div id="spinner"></div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
<mat-tab label="Postgres">
|
||||
<p class="space-or">
|
||||
Please enter your Postgres connection details below
|
||||
</p>
|
||||
<div>
|
||||
<mat-form-field>
|
||||
<input matInput type="text" formControlName="host" id="host" placeholder="Host">
|
||||
<mat-error>This field is required</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field>
|
||||
<input matInput type="number" formControlName="port" id="port" placeholder="Port">
|
||||
<mat-error>This field is required</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field>
|
||||
<input matInput type="text" formControlName="name" id="database" placeholder="Database Name">
|
||||
<mat-error>This field is required</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field>
|
||||
<input matInput type="text" formControlName="user" id="user" placeholder="User">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field>
|
||||
<input matInput type="password" formControlName="password" id="password" placeholder="Password">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<p>{{connectionString | async}}</p>
|
||||
<div style="text-align: center; margin-top: 20px">
|
||||
<button (click)="save()" id="databaseSave" mat-raised-button color="accent" type="button" class="viewon-btn database" [disabled]="form.invalid">Save</button>
|
||||
<div id="spinner"></div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,92 @@
|
|||
import { Component, EventEmitter, OnInit, Output } from "@angular/core";
|
||||
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { WizardService } from "../services/wizard.service";
|
||||
import { NotificationService } from "app/services";
|
||||
import { MatTabChangeEvent } from "@angular/material/tabs";
|
||||
|
||||
@Component({
|
||||
templateUrl: "./database.component.html",
|
||||
styleUrls: ["../welcome/welcome.component.scss"],
|
||||
selector: "wizard-database-selector",
|
||||
})
|
||||
export class DatabaseComponent implements OnInit {
|
||||
public constructor(private fb: FormBuilder, private service: WizardService, private notification: NotificationService) { }
|
||||
@Output() public configuredDatabase = new EventEmitter<void>();
|
||||
|
||||
public form: FormGroup;
|
||||
|
||||
public connectionString = new BehaviorSubject<string>("Server=;Port=3306;Database=ombi");
|
||||
|
||||
public ngOnInit(): void {
|
||||
this.form = this.fb.group({
|
||||
type: [""],
|
||||
host: ["", [Validators.required]],
|
||||
port: [3306, [Validators.required]],
|
||||
name: ["ombi", [Validators.required]],
|
||||
user: [""],
|
||||
password: [""],
|
||||
});
|
||||
|
||||
this.form.valueChanges.subscribe(x => {
|
||||
console.log(x);
|
||||
let connection = `Server=${x.host};Port=${x.port};Database=${x.name}`;
|
||||
|
||||
if (x.user) {
|
||||
connection += `;User=${x.user}`;
|
||||
if (x.password) {
|
||||
connection += `;Password=*******`;
|
||||
}
|
||||
}
|
||||
|
||||
if (x.type !== "MySQL") {
|
||||
connection = connection.replace("Server", "Host").replace("User", "Username");
|
||||
}
|
||||
|
||||
this.connectionString.next(connection);
|
||||
});
|
||||
}
|
||||
|
||||
public tabChange(event: MatTabChangeEvent) {
|
||||
if (event.index === 0) {
|
||||
this.form.reset();
|
||||
}
|
||||
if (event.index === 1) {
|
||||
this.form.reset({
|
||||
type: "MySQL",
|
||||
host: "",
|
||||
name: "ombi",
|
||||
port: 3306,
|
||||
});
|
||||
this.form.controls.type.setValue("MySQL");
|
||||
|
||||
}
|
||||
if (event.index === 2) {
|
||||
this.form.reset({
|
||||
type:"Postgres",
|
||||
host: "",
|
||||
name: "ombi",
|
||||
port: 5432,
|
||||
});
|
||||
|
||||
}
|
||||
this.form.markAllAsTouched();
|
||||
}
|
||||
|
||||
public save() {
|
||||
this.service.addDatabaseConfig(this.form.value).subscribe({
|
||||
next: () => {
|
||||
this.notification.success(`Database configuration updated! Please now restart Ombi!`);
|
||||
this.configuredDatabase.emit();
|
||||
},
|
||||
error: error => {
|
||||
if (error.error.message) {
|
||||
this.notification.error(error.error.message);
|
||||
} else {
|
||||
this.notification.error("Something went wrong, please check the logs");
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
}
|
13
src/Ombi/ClientApp/src/app/wizard/models/DatabaseSettings.ts
Normal file
13
src/Ombi/ClientApp/src/app/wizard/models/DatabaseSettings.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
export interface DatabaseSettings {
|
||||
type: string;
|
||||
host: string;
|
||||
port: number;
|
||||
name: string;
|
||||
user: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface DatabaseConfigurationResult {
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
|
@ -5,6 +5,7 @@ import { Observable } from "rxjs";
|
|||
import { ICustomizationSettings } from "../../interfaces";
|
||||
import { ServiceHelpers } from "../../services";
|
||||
import { IOmbiConfigModel } from "../models/OmbiConfigModel";
|
||||
import { DatabaseConfigurationResult, DatabaseSettings } from "../models/DatabaseSettings";
|
||||
|
||||
|
||||
@Injectable()
|
||||
|
@ -16,4 +17,8 @@ export class WizardService extends ServiceHelpers {
|
|||
public addOmbiConfig(config: IOmbiConfigModel): Observable<ICustomizationSettings> {
|
||||
return this.http.post<ICustomizationSettings>(`${this.url}config`, config, {headers: this.headers});
|
||||
}
|
||||
|
||||
public addDatabaseConfig(config: DatabaseSettings): Observable<DatabaseConfigurationResult> {
|
||||
return this.http.post<DatabaseConfigurationResult>(`${this.url}database`, config, {headers: this.headers});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<div class="wizard-background">
|
||||
<div class="container wizard-inner">
|
||||
<mat-stepper linear #stepper>
|
||||
@if (!needsRestart) {
|
||||
<mat-stepper linear #stepper>
|
||||
<mat-step >
|
||||
<form >
|
||||
<ng-template matStepLabel>Welcome</ng-template>
|
||||
|
@ -29,6 +30,12 @@
|
|||
</div>
|
||||
</form>
|
||||
</mat-step>
|
||||
<mat-step>
|
||||
<ng-template matStepLabel>Database</ng-template>
|
||||
<wizard-database-selector (configuredDatabase)="databaseConfigured()"></wizard-database-selector>
|
||||
<button mat-button matStepperPrevious class="mat-raised-button mat-error left">Back</button>
|
||||
<button mat-button matStepperNext class="mat-raised-button mat-accent right" data-test="nextDatabase">Next</button>
|
||||
</mat-step>
|
||||
|
||||
<mat-step [optional]="true">
|
||||
<form >
|
||||
|
@ -82,5 +89,22 @@
|
|||
</div>
|
||||
</mat-step>
|
||||
</mat-stepper>
|
||||
} @else {
|
||||
<mat-stepper linear>
|
||||
<mat-step >
|
||||
<ng-template matStepLabel>Restart</ng-template>
|
||||
<div class="welcome-container">
|
||||
<div class="left-container mediaserver">
|
||||
<i class="fa fa-database text-logo"></i>
|
||||
</div>
|
||||
<div class="right-container">
|
||||
<div class="right-container-content">
|
||||
<h1>Please Restart Ombi for the database changes to take effect!</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-step>
|
||||
</mat-stepper>
|
||||
}
|
||||
</div>
|
||||
</div>
|
|
@ -151,6 +151,12 @@ p.space-or{
|
|||
color: #A45FC4;
|
||||
}
|
||||
|
||||
|
||||
.viewon-btn.database {
|
||||
border: 1px solid #A45FC4;
|
||||
color: #A45FC4;
|
||||
}
|
||||
|
||||
.text-logo{
|
||||
font-size:12em;
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ export class WelcomeComponent implements OnInit {
|
|||
|
||||
@ViewChild('stepper', {static: false}) public stepper: MatStepper;
|
||||
public localUser: ICreateWizardUser;
|
||||
public needsRestart: boolean = false;
|
||||
public config: IOmbiConfigModel;
|
||||
|
||||
constructor(private router: Router, private identityService: IdentityService,
|
||||
|
@ -48,7 +49,7 @@ export class WelcomeComponent implements OnInit {
|
|||
this.settingsService.verifyUrl(this.config.applicationUrl).subscribe(x => {
|
||||
if (!x) {
|
||||
this.notificationService.error(`The URL "${this.config.applicationUrl}" is not valid. Please format it correctly e.g. http://www.google.com/`);
|
||||
this.stepper.selectedIndex = 3;
|
||||
this.stepper.selectedIndex = 4;
|
||||
return;
|
||||
}
|
||||
this.saveConfig();
|
||||
|
@ -58,6 +59,10 @@ export class WelcomeComponent implements OnInit {
|
|||
}
|
||||
}
|
||||
|
||||
public databaseConfigured() {
|
||||
this.needsRestart = true;
|
||||
}
|
||||
|
||||
private saveConfig() {
|
||||
this.WizardService.addOmbiConfig(this.config).subscribe({
|
||||
next: (config) => {
|
||||
|
|
|
@ -12,6 +12,7 @@ import { MediaServerComponent } from "./mediaserver/mediaserver.component";
|
|||
import { PlexComponent } from "./plex/plex.component";
|
||||
import { WelcomeComponent } from "./welcome/welcome.component";
|
||||
import { OmbiConfigComponent } from "./ombiconfig/ombiconfig.component";
|
||||
import { DatabaseComponent } from "./database/database.component";
|
||||
|
||||
import { EmbyService } from "../services";
|
||||
import { JellyfinService } from "../services";
|
||||
|
@ -48,6 +49,7 @@ const routes: Routes = [
|
|||
EmbyComponent,
|
||||
JellyfinComponent,
|
||||
OmbiConfigComponent,
|
||||
DatabaseComponent,
|
||||
],
|
||||
exports: [
|
||||
RouterModule,
|
||||
|
|
|
@ -40,7 +40,6 @@ namespace Ombi.Controllers.V1
|
|||
/// <summary>
|
||||
/// The Settings Controller
|
||||
/// </summary>
|
||||
[Admin]
|
||||
[ApiV1]
|
||||
[Produces("application/json")]
|
||||
[ApiController]
|
||||
|
@ -78,6 +77,7 @@ namespace Ombi.Controllers.V1
|
|||
/// Gets the Ombi settings.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Admin]
|
||||
[HttpGet("ombi")]
|
||||
public async Task<OmbiSettings> OmbiSettings()
|
||||
{
|
||||
|
@ -110,6 +110,7 @@ namespace Ombi.Controllers.V1
|
|||
/// </summary>
|
||||
/// <param name="ombi">The ombi.</param>
|
||||
/// <returns></returns>
|
||||
[Admin]
|
||||
[HttpPost("ombi")]
|
||||
public async Task<bool> OmbiSettings([FromBody]OmbiSettings ombi)
|
||||
{
|
||||
|
@ -145,6 +146,7 @@ namespace Ombi.Controllers.V1
|
|||
return model;
|
||||
}
|
||||
|
||||
[Admin]
|
||||
[HttpPost("ombi/resetApi")]
|
||||
public async Task<string> ResetApiKey()
|
||||
{
|
||||
|
@ -159,6 +161,7 @@ namespace Ombi.Controllers.V1
|
|||
/// Gets the Plex Settings.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Admin]
|
||||
[HttpGet("plex")]
|
||||
public async Task<PlexSettings> PlexSettings()
|
||||
{
|
||||
|
@ -185,6 +188,7 @@ namespace Ombi.Controllers.V1
|
|||
/// </summary>
|
||||
/// <param name="plex">The plex.</param>
|
||||
/// <returns></returns>
|
||||
[Admin]
|
||||
[HttpPost("plex")]
|
||||
public async Task<bool> PlexSettings([FromBody]PlexSettings plex)
|
||||
{
|
||||
|
@ -207,6 +211,7 @@ namespace Ombi.Controllers.V1
|
|||
/// Gets the Emby Settings.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Admin]
|
||||
[HttpGet("emby")]
|
||||
public async Task<EmbySettings> EmbySettings()
|
||||
{
|
||||
|
@ -218,6 +223,7 @@ namespace Ombi.Controllers.V1
|
|||
/// </summary>
|
||||
/// <param name="emby">The emby.</param>
|
||||
/// <returns></returns>
|
||||
[Admin]
|
||||
[HttpPost("emby")]
|
||||
public async Task<bool> EmbySettings([FromBody]EmbySettings emby)
|
||||
{
|
||||
|
@ -243,6 +249,7 @@ namespace Ombi.Controllers.V1
|
|||
/// Gets the Jellyfin Settings.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Admin]
|
||||
[HttpGet("jellyfin")]
|
||||
public async Task<JellyfinSettings> JellyfinSettings()
|
||||
{
|
||||
|
@ -254,6 +261,7 @@ namespace Ombi.Controllers.V1
|
|||
/// </summary>
|
||||
/// <param name="jellyfin">The jellyfin.</param>
|
||||
/// <returns></returns>
|
||||
[Admin]
|
||||
[HttpPost("jellyfin")]
|
||||
public async Task<bool> JellyfinSettings([FromBody]JellyfinSettings jellyfin)
|
||||
{
|
||||
|
@ -291,6 +299,7 @@ namespace Ombi.Controllers.V1
|
|||
/// </summary>
|
||||
/// <param name="settings">The settings.</param>
|
||||
/// <returns></returns>
|
||||
[Admin]
|
||||
[HttpPost("landingpage")]
|
||||
public async Task<bool> LandingPageSettings([FromBody]LandingPageSettings settings)
|
||||
{
|
||||
|
@ -326,6 +335,7 @@ namespace Ombi.Controllers.V1
|
|||
/// </summary>
|
||||
/// <param name="settings">The settings.</param>
|
||||
/// <returns></returns>
|
||||
[Admin]
|
||||
[HttpPost("customization")]
|
||||
public async Task<bool> CustomizationSettings([FromBody]CustomizationSettings settings)
|
||||
{
|
||||
|
@ -344,6 +354,7 @@ namespace Ombi.Controllers.V1
|
|||
/// Get's the preset themes available
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Admin]
|
||||
[HttpGet("themes")]
|
||||
public async Task<IEnumerable<PresetThemeViewModel>> GetThemes()
|
||||
{
|
||||
|
@ -377,6 +388,7 @@ namespace Ombi.Controllers.V1
|
|||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet("sonarr")]
|
||||
[PowerUser]
|
||||
public async Task<SonarrSettings> SonarrSettings()
|
||||
{
|
||||
return await Get<SonarrSettings>();
|
||||
|
@ -388,6 +400,7 @@ namespace Ombi.Controllers.V1
|
|||
/// <param name="settings">The settings.</param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("sonarr")]
|
||||
[Admin]
|
||||
public async Task<bool> SonarrSettings([FromBody]SonarrSettings settings)
|
||||
{
|
||||
var result = await Save(settings);
|
||||
|
@ -403,6 +416,7 @@ namespace Ombi.Controllers.V1
|
|||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet("radarr")]
|
||||
[PowerUser]
|
||||
public async Task<RadarrCombinedModel> RadarrSettings()
|
||||
{
|
||||
return new RadarrCombinedModel
|
||||
|
@ -416,6 +430,7 @@ namespace Ombi.Controllers.V1
|
|||
/// Gets the Lidarr Settings.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Admin]
|
||||
[HttpGet("lidarr")]
|
||||
public async Task<LidarrSettings> LidarrSettings()
|
||||
{
|
||||
|
@ -439,6 +454,7 @@ namespace Ombi.Controllers.V1
|
|||
/// </summary>
|
||||
/// <param name="settings">The settings.</param>
|
||||
/// <returns></returns>
|
||||
[Admin]
|
||||
[HttpPost("lidarr")]
|
||||
public async Task<bool> LidarrSettings([FromBody]LidarrSettings settings)
|
||||
{
|
||||
|
@ -455,6 +471,7 @@ namespace Ombi.Controllers.V1
|
|||
/// </summary>
|
||||
/// <param name="settings">The settings.</param>
|
||||
/// <returns></returns>
|
||||
[Admin]
|
||||
[HttpPost("authentication")]
|
||||
public async Task<bool> AuthenticationsSettings([FromBody]AuthenticationSettings settings)
|
||||
{
|
||||
|
@ -477,6 +494,7 @@ namespace Ombi.Controllers.V1
|
|||
/// </summary>
|
||||
/// <param name="settings">The settings.</param>
|
||||
/// <returns></returns>
|
||||
[Admin]
|
||||
[HttpPost("radarr")]
|
||||
public async Task<bool> RadarrSettings([FromBody]RadarrCombinedModel settings)
|
||||
{
|
||||
|
@ -498,6 +516,7 @@ namespace Ombi.Controllers.V1
|
|||
/// </summary>
|
||||
/// <param name="settings">The settings.</param>
|
||||
/// <returns></returns>
|
||||
[Admin]
|
||||
[HttpPost("Update")]
|
||||
public async Task<bool> UpdateSettings([FromBody]UpdateSettings settings)
|
||||
{
|
||||
|
@ -508,6 +527,7 @@ namespace Ombi.Controllers.V1
|
|||
/// Gets the UserManagement Settings.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Admin]
|
||||
[HttpGet("UserManagement")]
|
||||
public async Task<UserManagementSettings> UserManagementSettings()
|
||||
{
|
||||
|
@ -519,6 +539,7 @@ namespace Ombi.Controllers.V1
|
|||
/// </summary>
|
||||
/// <param name="settings">The settings.</param>
|
||||
/// <returns></returns>
|
||||
[Admin]
|
||||
[HttpPost("UserManagement")]
|
||||
public async Task<bool> UserManagementSettings([FromBody]UserManagementSettings settings)
|
||||
{
|
||||
|
@ -529,6 +550,7 @@ namespace Ombi.Controllers.V1
|
|||
/// Gets the Update Settings.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Admin]
|
||||
[HttpGet("Update")]
|
||||
public async Task<UpdateSettings> UpdateSettings()
|
||||
{
|
||||
|
@ -541,6 +563,7 @@ namespace Ombi.Controllers.V1
|
|||
/// Gets the CouchPotatoSettings Settings.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Admin]
|
||||
[HttpGet("CouchPotato")]
|
||||
public async Task<CouchPotatoSettings> CouchPotatoSettings()
|
||||
{
|
||||
|
@ -552,6 +575,7 @@ namespace Ombi.Controllers.V1
|
|||
/// </summary>
|
||||
/// <param name="settings">The settings.</param>
|
||||
/// <returns></returns>
|
||||
[Admin]
|
||||
[HttpPost("CouchPotato")]
|
||||
public async Task<bool> CouchPotatoSettings([FromBody]CouchPotatoSettings settings)
|
||||
{
|
||||
|
@ -562,6 +586,7 @@ namespace Ombi.Controllers.V1
|
|||
/// Gets the DogNzbSettings Settings.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Admin]
|
||||
[HttpGet("DogNzb")]
|
||||
public async Task<DogNzbSettings> DogNzbSettings()
|
||||
{
|
||||
|
@ -573,6 +598,7 @@ namespace Ombi.Controllers.V1
|
|||
/// </summary>
|
||||
/// <param name="settings">The settings.</param>
|
||||
/// <returns></returns>
|
||||
[Admin]
|
||||
[HttpPost("DogNzb")]
|
||||
public async Task<bool> DogNzbSettings([FromBody]DogNzbSettings settings)
|
||||
{
|
||||
|
@ -584,6 +610,7 @@ namespace Ombi.Controllers.V1
|
|||
/// </summary>
|
||||
/// <param name="settings">The settings.</param>
|
||||
/// <returns></returns>
|
||||
[Admin]
|
||||
[HttpPost("SickRage")]
|
||||
public async Task<bool> SickRageSettings([FromBody]SickRageSettings settings)
|
||||
{
|
||||
|
@ -594,6 +621,7 @@ namespace Ombi.Controllers.V1
|
|||
/// Gets the SickRage Settings.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Admin]
|
||||
[HttpGet("SickRage")]
|
||||
public async Task<SickRageSettings> SickRageSettings()
|
||||
{
|
||||
|
@ -604,6 +632,7 @@ namespace Ombi.Controllers.V1
|
|||
/// Gets the JobSettings Settings.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Admin]
|
||||
[HttpGet("jobs")]
|
||||
public async Task<JobSettings> JobSettings()
|
||||
{
|
||||
|
@ -636,6 +665,7 @@ namespace Ombi.Controllers.V1
|
|||
/// </summary>
|
||||
/// <param name="settings">The settings.</param>
|
||||
/// <returns></returns>
|
||||
[Admin]
|
||||
[HttpPost("jobs")]
|
||||
public async Task<JobSettingsViewModel> JobSettings([FromBody]JobSettings settings)
|
||||
{
|
||||
|
@ -679,6 +709,7 @@ namespace Ombi.Controllers.V1
|
|||
}
|
||||
|
||||
[HttpPost("testcron")]
|
||||
[Admin]
|
||||
public CronTestModel TestCron([FromBody] CronViewModelBody body)
|
||||
{
|
||||
var model = new CronTestModel();
|
||||
|
@ -712,6 +743,7 @@ namespace Ombi.Controllers.V1
|
|||
/// <param name="settings">The settings.</param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("Issues")]
|
||||
[Admin]
|
||||
public async Task<bool> IssueSettings([FromBody]IssueSettings settings)
|
||||
{
|
||||
return await Save(settings);
|
||||
|
@ -742,6 +774,7 @@ namespace Ombi.Controllers.V1
|
|||
/// <param name="settings">The settings.</param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("vote")]
|
||||
[Admin]
|
||||
public async Task<bool> VoteSettings([FromBody]VoteSettings settings)
|
||||
{
|
||||
return await Save(settings);
|
||||
|
@ -752,6 +785,7 @@ namespace Ombi.Controllers.V1
|
|||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet("vote")]
|
||||
[Admin]
|
||||
public async Task<VoteSettings> VoteSettings()
|
||||
{
|
||||
return await Get<VoteSettings>();
|
||||
|
@ -770,6 +804,7 @@ namespace Ombi.Controllers.V1
|
|||
/// </summary>
|
||||
/// <param name="settings">The settings.</param>
|
||||
[HttpPost("themoviedb")]
|
||||
[Admin]
|
||||
public async Task<bool> TheMovieDbSettings([FromBody]TheMovieDbSettings settings)
|
||||
{
|
||||
return await Save(settings);
|
||||
|
@ -778,6 +813,7 @@ namespace Ombi.Controllers.V1
|
|||
/// <summary>
|
||||
/// Get The Movie DB settings.
|
||||
/// </summary>
|
||||
[Admin]
|
||||
[HttpGet("themoviedb")]
|
||||
public async Task<TheMovieDbSettings> TheMovieDbSettings()
|
||||
{
|
||||
|
@ -789,6 +825,7 @@ namespace Ombi.Controllers.V1
|
|||
/// </summary>
|
||||
/// <param name="model">The model.</param>
|
||||
/// <returns></returns>
|
||||
[Admin]
|
||||
[HttpPost("notifications/email")]
|
||||
public async Task<bool> EmailNotificationSettings([FromBody] EmailNotificationsViewModel model)
|
||||
{
|
||||
|
@ -806,6 +843,7 @@ namespace Ombi.Controllers.V1
|
|||
/// Gets the Email Notification Settings.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Admin]
|
||||
[HttpGet("notifications/email")]
|
||||
public async Task<EmailNotificationsViewModel> EmailNotificationSettings()
|
||||
{
|
||||
|
@ -836,6 +874,7 @@ namespace Ombi.Controllers.V1
|
|||
/// </summary>
|
||||
/// <param name="model">The model.</param>
|
||||
/// <returns></returns>
|
||||
[Admin]
|
||||
[HttpPost("notifications/discord")]
|
||||
public async Task<bool> DiscordNotificationSettings([FromBody] DiscordNotificationsViewModel model)
|
||||
{
|
||||
|
@ -853,6 +892,7 @@ namespace Ombi.Controllers.V1
|
|||
/// Gets the discord Notification Settings.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Admin]
|
||||
[HttpGet("notifications/discord")]
|
||||
public async Task<DiscordNotificationsViewModel> DiscordNotificationSettings()
|
||||
{
|
||||
|
@ -871,6 +911,7 @@ namespace Ombi.Controllers.V1
|
|||
/// </summary>
|
||||
/// <param name="model">The model.</param>
|
||||
/// <returns></returns>
|
||||
[Admin]
|
||||
[HttpPost("notifications/telegram")]
|
||||
public async Task<bool> TelegramNotificationSettings([FromBody] TelegramNotificationsViewModel model)
|
||||
{
|
||||
|
@ -888,6 +929,7 @@ namespace Ombi.Controllers.V1
|
|||
/// Gets the telegram Notification Settings.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Admin]
|
||||
[HttpGet("notifications/telegram")]
|
||||
public async Task<TelegramNotificationsViewModel> TelegramNotificationSettings()
|
||||
{
|
||||
|
@ -905,6 +947,7 @@ namespace Ombi.Controllers.V1
|
|||
/// </summary>
|
||||
/// <param name="model">The model.</param>
|
||||
/// <returns></returns>
|
||||
[Admin]
|
||||
[HttpPost("notifications/pushbullet")]
|
||||
public async Task<bool> PushbulletNotificationSettings([FromBody] PushbulletNotificationViewModel model)
|
||||
{
|
||||
|
@ -922,6 +965,7 @@ namespace Ombi.Controllers.V1
|
|||
/// Gets the pushbullet Notification Settings.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Admin]
|
||||
[HttpGet("notifications/pushbullet")]
|
||||
public async Task<PushbulletNotificationViewModel> PushbulletNotificationSettings()
|
||||
{
|
||||
|
@ -939,6 +983,7 @@ namespace Ombi.Controllers.V1
|
|||
/// </summary>
|
||||
/// <param name="model">The model.</param>
|
||||
/// <returns></returns>
|
||||
[Admin]
|
||||
[HttpPost("notifications/pushover")]
|
||||
public async Task<bool> PushoverNotificationSettings([FromBody] PushoverNotificationViewModel model)
|
||||
{
|
||||
|
@ -956,6 +1001,7 @@ namespace Ombi.Controllers.V1
|
|||
/// Gets the pushover Notification Settings.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Admin]
|
||||
[HttpGet("notifications/pushover")]
|
||||
public async Task<PushoverNotificationViewModel> PushoverNotificationSettings()
|
||||
{
|
||||
|
@ -974,6 +1020,7 @@ namespace Ombi.Controllers.V1
|
|||
/// </summary>
|
||||
/// <param name="model">The model.</param>
|
||||
/// <returns></returns>
|
||||
[Admin]
|
||||
[HttpPost("notifications/slack")]
|
||||
public async Task<bool> SlacktNotificationSettings([FromBody] SlackNotificationsViewModel model)
|
||||
{
|
||||
|
@ -991,6 +1038,7 @@ namespace Ombi.Controllers.V1
|
|||
/// Gets the slack Notification Settings.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Admin]
|
||||
[HttpGet("notifications/slack")]
|
||||
public async Task<SlackNotificationsViewModel> SlackNotificationSettings()
|
||||
{
|
||||
|
@ -1008,6 +1056,7 @@ namespace Ombi.Controllers.V1
|
|||
/// </summary>
|
||||
/// <param name="model">The model.</param>
|
||||
/// <returns></returns>
|
||||
[Admin]
|
||||
[HttpPost("notifications/mattermost")]
|
||||
public async Task<bool> MattermostNotificationSettings([FromBody] MattermostNotificationsViewModel model)
|
||||
{
|
||||
|
@ -1025,6 +1074,7 @@ namespace Ombi.Controllers.V1
|
|||
/// Gets the Mattermost Notification Settings.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Admin]
|
||||
[HttpGet("notifications/mattermost")]
|
||||
public async Task<MattermostNotificationsViewModel> MattermostNotificationSettings()
|
||||
{
|
||||
|
@ -1041,6 +1091,7 @@ namespace Ombi.Controllers.V1
|
|||
/// Gets the Twilio Notification Settings.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Admin]
|
||||
[HttpGet("notifications/twilio")]
|
||||
public async Task<TwilioSettingsViewModel> TwilioNotificationSettings()
|
||||
{
|
||||
|
@ -1062,6 +1113,7 @@ namespace Ombi.Controllers.V1
|
|||
/// </summary>
|
||||
/// <param name="model">The model.</param>
|
||||
/// <returns></returns>
|
||||
[Admin]
|
||||
[HttpPost("notifications/twilio")]
|
||||
public async Task<bool> TwilioNotificationSettings([FromBody] TwilioSettingsViewModel model)
|
||||
{
|
||||
|
@ -1080,6 +1132,7 @@ namespace Ombi.Controllers.V1
|
|||
/// </summary>
|
||||
/// <param name="model">The model.</param>
|
||||
/// <returns></returns>
|
||||
[Admin]
|
||||
[HttpPost("notifications/mobile")]
|
||||
public async Task<bool> MobileNotificationSettings([FromBody] MobileNotificationsViewModel model)
|
||||
{
|
||||
|
@ -1097,6 +1150,7 @@ namespace Ombi.Controllers.V1
|
|||
/// Gets the Mobile Notification Settings.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Admin]
|
||||
[HttpGet("notifications/mobile")]
|
||||
public async Task<MobileNotificationsViewModel> MobileNotificationSettings()
|
||||
{
|
||||
|
@ -1114,6 +1168,7 @@ namespace Ombi.Controllers.V1
|
|||
/// </summary>
|
||||
/// <param name="model">The model.</param>
|
||||
/// <returns></returns>
|
||||
[Admin]
|
||||
[HttpPost("notifications/gotify")]
|
||||
public async Task<bool> GotifyNotificationSettings([FromBody] GotifyNotificationViewModel model)
|
||||
{
|
||||
|
@ -1131,6 +1186,7 @@ namespace Ombi.Controllers.V1
|
|||
/// Gets the gotify Notification Settings.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Admin]
|
||||
[HttpGet("notifications/gotify")]
|
||||
public async Task<GotifyNotificationViewModel> GotifyNotificationSettings()
|
||||
{
|
||||
|
@ -1148,6 +1204,7 @@ namespace Ombi.Controllers.V1
|
|||
/// </summary>
|
||||
/// <param name="model">The model.</param>
|
||||
/// <returns></returns>
|
||||
[Admin]
|
||||
[HttpPost("notifications/webhook")]
|
||||
public async Task<bool> WebhookNotificationSettings([FromBody] WebhookNotificationViewModel model)
|
||||
{
|
||||
|
@ -1161,6 +1218,7 @@ namespace Ombi.Controllers.V1
|
|||
/// Gets the webhook notification settings.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Admin]
|
||||
[HttpGet("notifications/webhook")]
|
||||
public async Task<WebhookNotificationViewModel> WebhookNotificationSettings()
|
||||
{
|
||||
|
@ -1175,6 +1233,7 @@ namespace Ombi.Controllers.V1
|
|||
/// </summary>
|
||||
/// <param name="model">The model.</param>
|
||||
/// <returns></returns>
|
||||
[Admin]
|
||||
[HttpPost("notifications/newsletter")]
|
||||
public async Task<bool> NewsletterSettings([FromBody] NewsletterNotificationViewModel model)
|
||||
{
|
||||
|
@ -1189,6 +1248,7 @@ namespace Ombi.Controllers.V1
|
|||
}
|
||||
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
[Admin]
|
||||
[HttpPost("notifications/newsletterdatabase")]
|
||||
public async Task<bool> UpdateNewsletterDatabase()
|
||||
{
|
||||
|
@ -1199,6 +1259,7 @@ namespace Ombi.Controllers.V1
|
|||
/// Gets the Newsletter Notification Settings.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Admin]
|
||||
[HttpGet("notifications/newsletter")]
|
||||
public async Task<NewsletterNotificationViewModel> NewsletterSettings()
|
||||
{
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Ombi.Attributes;
|
||||
using Ombi.Core.Settings;
|
||||
|
@ -6,6 +8,10 @@ using Ombi.Helpers;
|
|||
using Ombi.Models.V2;
|
||||
using Ombi.Settings.Settings.Models;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MySqlConnector;
|
||||
using Npgsql;
|
||||
using Ombi.Core.Services;
|
||||
|
||||
namespace Ombi.Controllers.V2
|
||||
{
|
||||
|
@ -13,15 +19,25 @@ namespace Ombi.Controllers.V2
|
|||
[AllowAnonymous]
|
||||
public class WizardController : V2Controller
|
||||
{
|
||||
private readonly ISettingsService<OmbiSettings> _ombiSettings;
|
||||
private readonly IDatabaseConfigurationService _databaseConfigurationService;
|
||||
private readonly ILogger _logger;
|
||||
private ISettingsService<CustomizationSettings> _customizationSettings { get; }
|
||||
|
||||
public WizardController(ISettingsService<CustomizationSettings> customizationSettings)
|
||||
public WizardController(
|
||||
ISettingsService<CustomizationSettings> customizationSettings,
|
||||
ISettingsService<OmbiSettings> ombiSettings,
|
||||
IDatabaseConfigurationService databaseConfigurationService,
|
||||
ILogger<WizardController> logger)
|
||||
{
|
||||
_ombiSettings = ombiSettings;
|
||||
_databaseConfigurationService = databaseConfigurationService;
|
||||
_logger = logger;
|
||||
_customizationSettings = customizationSettings;
|
||||
}
|
||||
|
||||
[HttpPost("config")]
|
||||
[ApiExplorerSettings(IgnoreApi =true)]
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public async Task<IActionResult> OmbiConfig([FromBody] OmbiConfigModel config)
|
||||
{
|
||||
if (config == null)
|
||||
|
@ -29,6 +45,13 @@ namespace Ombi.Controllers.V2
|
|||
return BadRequest();
|
||||
}
|
||||
|
||||
var ombiSettings = await _ombiSettings.GetSettingsAsync();
|
||||
if (ombiSettings.Wizard)
|
||||
{
|
||||
_logger.LogError("Wizard has already been completed");
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
var settings = await _customizationSettings.GetSettingsAsync();
|
||||
|
||||
if (config.ApplicationName.HasValue())
|
||||
|
@ -50,5 +73,67 @@ namespace Ombi.Controllers.V2
|
|||
|
||||
return new OkObjectResult(settings);
|
||||
}
|
||||
|
||||
[HttpPost("database")]
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public async Task<IActionResult> DatabaseConfig([FromBody] WizardDatabaseConfiguration config, CancellationToken token)
|
||||
{
|
||||
if (config == null)
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
var ombiSettings = await _ombiSettings.GetSettingsAsync();
|
||||
if (ombiSettings.Wizard)
|
||||
{
|
||||
_logger.LogError("Wizard has already been completed");
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
var sanitizedType = config.Type.Replace(Environment.NewLine, "").Replace("\n", "").Replace("\r", "");
|
||||
_logger.LogInformation("Setting up database type: {0}", sanitizedType);
|
||||
|
||||
var connectionString = string.Empty;
|
||||
if (config.Type == IDatabaseConfigurationService.MySqlDatabase)
|
||||
{
|
||||
_logger.LogInformation("Building MySQL connectionstring");
|
||||
var builder = new MySqlConnectionStringBuilder
|
||||
{
|
||||
Database = config.Name,
|
||||
Port = Convert.ToUInt32(config.Port),
|
||||
Server = config.Host,
|
||||
UserID = config.User,
|
||||
Password = config.Password
|
||||
};
|
||||
|
||||
connectionString = builder.ToString();
|
||||
}
|
||||
|
||||
if (config.Type == IDatabaseConfigurationService.PostgresDatabase)
|
||||
{
|
||||
_logger.LogInformation("Building Postgres connectionstring");
|
||||
var builder = new NpgsqlConnectionStringBuilder
|
||||
{
|
||||
Host = config.Host,
|
||||
Port = config.Port,
|
||||
Database = config.Name,
|
||||
Username = config.User,
|
||||
Password = config.Password
|
||||
};
|
||||
connectionString = builder.ToString();
|
||||
}
|
||||
|
||||
var result = await _databaseConfigurationService.ConfigureDatabase(config.Type, connectionString, token);
|
||||
|
||||
if (!result)
|
||||
{
|
||||
return BadRequest(new DatabaseConfigurationResult(false, "Could not configure the database, please check the logs"));
|
||||
}
|
||||
|
||||
return Ok(new DatabaseConfigurationResult(true, "Database configured successfully"));
|
||||
}
|
||||
|
||||
public record DatabaseConfigurationResult(bool Success, string Message);
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,8 @@ using Microsoft.Extensions.Diagnostics.HealthChecks;
|
|||
using MySqlConnector;
|
||||
using Newtonsoft.Json;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal;
|
||||
using Ombi.Core.Helpers;
|
||||
using Ombi.Core.Models;
|
||||
using Ombi.Helpers;
|
||||
using Ombi.Store.Context;
|
||||
using Ombi.Store.Context.MySql;
|
||||
|
@ -38,11 +40,11 @@ namespace Ombi.Extensions
|
|||
AddSqliteHealthCheck(hcBuilder, "Ombi Database", configuration.OmbiDatabase);
|
||||
break;
|
||||
case var type when type.Equals(MySqlDatabase, StringComparison.InvariantCultureIgnoreCase):
|
||||
services.AddDbContext<OmbiContext, OmbiMySqlContext>(x => ConfigureMySql(x, configuration.OmbiDatabase));
|
||||
services.AddDbContext<OmbiContext, OmbiMySqlContext>(x => DatabaseConfigurationSetup.ConfigureMySql(x, configuration.OmbiDatabase));
|
||||
AddMySqlHealthCheck(hcBuilder, "Ombi Database", configuration.OmbiDatabase);
|
||||
break;
|
||||
case var type when type.Equals(PostgresDatabase, StringComparison.InvariantCultureIgnoreCase):
|
||||
services.AddDbContext<OmbiContext, OmbiPostgresContext>(x => ConfigurePostgres(x, configuration.OmbiDatabase));
|
||||
services.AddDbContext<OmbiContext, OmbiPostgresContext>(x => DatabaseConfigurationSetup.ConfigurePostgres(x, configuration.OmbiDatabase));
|
||||
AddPostgresHealthCheck(hcBuilder, "Ombi Database", configuration.OmbiDatabase);
|
||||
break;
|
||||
}
|
||||
|
@ -54,11 +56,11 @@ namespace Ombi.Extensions
|
|||
AddSqliteHealthCheck(hcBuilder, "External Database", configuration.ExternalDatabase);
|
||||
break;
|
||||
case var type when type.Equals(MySqlDatabase, StringComparison.InvariantCultureIgnoreCase):
|
||||
services.AddDbContext<ExternalContext, ExternalMySqlContext>(x => ConfigureMySql(x, configuration.ExternalDatabase));
|
||||
services.AddDbContext<ExternalContext, ExternalMySqlContext>(x => DatabaseConfigurationSetup.ConfigureMySql(x, configuration.ExternalDatabase));
|
||||
AddMySqlHealthCheck(hcBuilder, "External Database", configuration.ExternalDatabase);
|
||||
break;
|
||||
case var type when type.Equals(PostgresDatabase, StringComparison.InvariantCultureIgnoreCase):
|
||||
services.AddDbContext<ExternalContext, ExternalPostgresContext>(x => ConfigurePostgres(x, configuration.ExternalDatabase));
|
||||
services.AddDbContext<ExternalContext, ExternalPostgresContext>(x => DatabaseConfigurationSetup.ConfigurePostgres(x, configuration.ExternalDatabase));
|
||||
AddPostgresHealthCheck(hcBuilder, "External Database", configuration.ExternalDatabase);
|
||||
break;
|
||||
}
|
||||
|
@ -70,11 +72,11 @@ namespace Ombi.Extensions
|
|||
AddSqliteHealthCheck(hcBuilder, "Settings Database", configuration.SettingsDatabase);
|
||||
break;
|
||||
case var type when type.Equals(MySqlDatabase, StringComparison.InvariantCultureIgnoreCase):
|
||||
services.AddDbContext<SettingsContext, SettingsMySqlContext>(x => ConfigureMySql(x, configuration.SettingsDatabase));
|
||||
services.AddDbContext<SettingsContext, SettingsMySqlContext>(x => DatabaseConfigurationSetup.ConfigureMySql(x, configuration.SettingsDatabase));
|
||||
AddMySqlHealthCheck(hcBuilder, "Settings Database", configuration.SettingsDatabase);
|
||||
break;
|
||||
case var type when type.Equals(PostgresDatabase, StringComparison.InvariantCultureIgnoreCase):
|
||||
services.AddDbContext<SettingsContext, SettingsPostgresContext>(x => ConfigurePostgres(x, configuration.SettingsDatabase));
|
||||
services.AddDbContext<SettingsContext, SettingsPostgresContext>(x => DatabaseConfigurationSetup.ConfigurePostgres(x, configuration.SettingsDatabase));
|
||||
AddPostgresHealthCheck(hcBuilder, "Settings Database", configuration.SettingsDatabase);
|
||||
break;
|
||||
}
|
||||
|
@ -150,95 +152,5 @@ namespace Ombi.Extensions
|
|||
SQLitePCL.raw.sqlite3_config(raw.SQLITE_CONFIG_MULTITHREAD);
|
||||
options.UseSqlite(config.ConnectionString);
|
||||
}
|
||||
|
||||
public static void ConfigureMySql(DbContextOptionsBuilder options, PerDatabaseConfiguration config)
|
||||
{
|
||||
if (string.IsNullOrEmpty(config.ConnectionString))
|
||||
{
|
||||
throw new ArgumentNullException("ConnectionString for the MySql/Mariadb database is empty");
|
||||
}
|
||||
|
||||
options.UseMySql(config.ConnectionString, GetServerVersion(config.ConnectionString), b =>
|
||||
{
|
||||
//b.CharSetBehavior(Pomelo.EntityFrameworkCore.MySql.Infrastructure.CharSetBehavior.NeverAppend); // ##ISSUE, link to migrations?
|
||||
b.EnableRetryOnFailure();
|
||||
});
|
||||
}
|
||||
|
||||
public static void ConfigurePostgres(DbContextOptionsBuilder options, PerDatabaseConfiguration config)
|
||||
{
|
||||
options.UseNpgsql(config.ConnectionString, b =>
|
||||
{
|
||||
b.EnableRetryOnFailure();
|
||||
}).ReplaceService<ISqlGenerationHelper, NpgsqlCaseInsensitiveSqlGenerationHelper>();
|
||||
}
|
||||
|
||||
private static ServerVersion GetServerVersion(string connectionString)
|
||||
{
|
||||
// Workaround Windows bug, that can lead to the following exception:
|
||||
//
|
||||
// MySqlConnector.MySqlException (0x80004005): SSL Authentication Error
|
||||
// ---> System.Security.Authentication.AuthenticationException: Authentication failed, see inner exception.
|
||||
// ---> System.ComponentModel.Win32Exception (0x8009030F): The message or signature supplied for verification has been altered
|
||||
//
|
||||
// See https://github.com/dotnet/runtime/issues/17005#issuecomment-305848835
|
||||
//
|
||||
// Also workaround for the fact, that ServerVersion.AutoDetect() does not use any retrying strategy.
|
||||
ServerVersion serverVersion = null;
|
||||
#pragma warning disable EF1001
|
||||
var retryPolicy = Policy.Handle<Exception>(exception => MySqlTransientExceptionDetector.ShouldRetryOn(exception))
|
||||
#pragma warning restore EF1001
|
||||
.WaitAndRetry(3, (count, context) => TimeSpan.FromMilliseconds(count * 250));
|
||||
|
||||
serverVersion = retryPolicy.Execute(() => serverVersion = ServerVersion.AutoDetect(connectionString));
|
||||
|
||||
return serverVersion;
|
||||
}
|
||||
|
||||
public class DatabaseConfiguration
|
||||
{
|
||||
public DatabaseConfiguration()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public DatabaseConfiguration(string defaultSqlitePath)
|
||||
{
|
||||
OmbiDatabase = new PerDatabaseConfiguration(SqliteDatabase, $"Data Source={Path.Combine(defaultSqlitePath, "Ombi.db")}");
|
||||
SettingsDatabase = new PerDatabaseConfiguration(SqliteDatabase, $"Data Source={Path.Combine(defaultSqlitePath, "OmbiSettings.db")}");
|
||||
ExternalDatabase = new PerDatabaseConfiguration(SqliteDatabase, $"Data Source={Path.Combine(defaultSqlitePath, "OmbiExternal.db")}");
|
||||
}
|
||||
public PerDatabaseConfiguration OmbiDatabase { get; set; }
|
||||
public PerDatabaseConfiguration SettingsDatabase { get; set; }
|
||||
public PerDatabaseConfiguration ExternalDatabase { get; set; }
|
||||
}
|
||||
|
||||
public class PerDatabaseConfiguration
|
||||
{
|
||||
public PerDatabaseConfiguration(string type, string connectionString)
|
||||
{
|
||||
Type = type;
|
||||
ConnectionString = connectionString;
|
||||
}
|
||||
|
||||
// Used in Deserialization
|
||||
public PerDatabaseConfiguration()
|
||||
{
|
||||
|
||||
}
|
||||
public string Type { get; set; }
|
||||
public string ConnectionString { get; set; }
|
||||
}
|
||||
|
||||
public class NpgsqlCaseInsensitiveSqlGenerationHelper : NpgsqlSqlGenerationHelper
|
||||
{
|
||||
const string EFMigrationsHisory = "__EFMigrationsHistory";
|
||||
public NpgsqlCaseInsensitiveSqlGenerationHelper(RelationalSqlGenerationHelperDependencies dependencies)
|
||||
: base(dependencies) { }
|
||||
public override string DelimitIdentifier(string identifier) =>
|
||||
base.DelimitIdentifier(identifier == EFMigrationsHisory ? identifier : identifier.ToLower());
|
||||
public override void DelimitIdentifier(StringBuilder builder, string identifier)
|
||||
=> base.DelimitIdentifier(builder, identifier == EFMigrationsHisory ? identifier : identifier.ToLower());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
3
src/Ombi/Models/V2/WizardDatabaseConfiguration.cs
Normal file
3
src/Ombi/Models/V2/WizardDatabaseConfiguration.cs
Normal file
|
@ -0,0 +1,3 @@
|
|||
namespace Ombi.Models.V2;
|
||||
|
||||
public record WizardDatabaseConfiguration(string Type, string Host, int Port, string Name, string User, string Password);
|
|
@ -54,10 +54,6 @@
|
|||
<Content Remove="database.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="database.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\.editorconfig" Link=".editorconfig" />
|
||||
</ItemGroup>
|
||||
|
|
|
@ -159,7 +159,7 @@
|
|||
"RequestedBy": "Sol·licitat per",
|
||||
"Status": "Estat",
|
||||
"RequestStatus": "Estat de la sol·licitud",
|
||||
"Watched": "Watched",
|
||||
"Watched": "Vist",
|
||||
"WatchedTooltip": "The user who made the request has watched it",
|
||||
"WatchedProgressTooltip": "Shows how much the user who made the request has watched it",
|
||||
"WatchedByUsersCount": "{{count}} users have watched this.",
|
||||
|
@ -408,7 +408,7 @@
|
|||
"Movies": "Pel·lícules",
|
||||
"Combined": "Combinat",
|
||||
"Tv": "TV",
|
||||
"Genres": "Genres",
|
||||
"Genres": "Gèneres",
|
||||
"CardDetails": {
|
||||
"Availability": "Disponibilitat",
|
||||
"Studio": "Estudi",
|
||||
|
|
|
@ -159,10 +159,10 @@
|
|||
"RequestedBy": "Verzocht Door",
|
||||
"Status": "Status",
|
||||
"RequestStatus": "Aanvraagstatus",
|
||||
"Watched": "Watched",
|
||||
"WatchedTooltip": "The user who made the request has watched it",
|
||||
"WatchedProgressTooltip": "Shows how much the user who made the request has watched it",
|
||||
"WatchedByUsersCount": "{{count}} users have watched this.",
|
||||
"Watched": "Bekeken",
|
||||
"WatchedTooltip": "De gebruiker die het verzoek heeft ingediend, heeft het bekeken",
|
||||
"WatchedProgressTooltip": "Laat zien hoeveel de gebruiker die het verzoek heeft gemaakt het heeft bekeken",
|
||||
"WatchedByUsersCount": "{{count}} gebruikers hebben dit bekeken.",
|
||||
"Denied": " Geweigerd:",
|
||||
"TheatricalRelease": "Cinema Uitgave: {{date}}",
|
||||
"ReleaseDate": "Uitgekomen: {{date}}",
|
||||
|
@ -225,7 +225,7 @@
|
|||
"Denied": "Geselecteerde items succesvol afgekeurd"
|
||||
},
|
||||
"SuccessfullyApproved": "Succesvol goedgekeurd",
|
||||
"SuccessfullyDenied": "Successfully Denied",
|
||||
"SuccessfullyDenied": "Succesvol Geweigerd",
|
||||
"SuccessfullyDeleted": "Verzoek succesvol verwijderd",
|
||||
"NowAvailable": "Verzoek is nu beschikbaar",
|
||||
"NowUnavailable": "Verzoek is nu niet beschikbaar",
|
||||
|
@ -241,7 +241,7 @@
|
|||
"NoPermissionsOnBehalf": "Je hebt niet de juiste rechten om namens gebruikers aan te vragen!",
|
||||
"NoPermissions": "Je hebt de juiste rechten niet!",
|
||||
"RequestDoesNotExist": "Verzoek bestaat niet",
|
||||
"ChildRequestDoesNotExist": "Child Request does not exist",
|
||||
"ChildRequestDoesNotExist": "Kindverzoek bestaat niet",
|
||||
"NoPermissionsRequestMovie": "Je bent niet gemachtigd om een film aan te vragen",
|
||||
"NoPermissionsRequestTV": "Je bent niet gemachtigd om een serie aan te vragen",
|
||||
"NoPermissionsRequestAlbum": "Je bent niet gemachtigd om een album aan te vragen",
|
||||
|
|
|
@ -5,6 +5,7 @@ import createEsbuildPlugin from "@badeball/cypress-cucumber-preprocessor/esbuild
|
|||
|
||||
export default defineConfig({
|
||||
watchForFileChanges: true,
|
||||
video: true,
|
||||
chromeWebSecurity: false,
|
||||
viewportWidth: 2560,
|
||||
viewportHeight: 1440,
|
||||
|
|
|
@ -11,6 +11,7 @@ When("I visit Ombi", () => {
|
|||
|
||||
When("I click through all of the pages", () => {
|
||||
Page.welcomeTab.next.click();
|
||||
Page.databaseTab.next.click();
|
||||
Page.mediaServerTab.next.click();
|
||||
Page.localUserTab.next.click();
|
||||
Page.ombiConfigTab.next.click();
|
||||
|
@ -22,6 +23,7 @@ When("I click through all of the pages", () => {
|
|||
|
||||
When("I click through to the user page", () => {
|
||||
Page.welcomeTab.next.click();
|
||||
Page.databaseTab.next.click();
|
||||
Page.mediaServerTab.next.click();
|
||||
});
|
||||
|
||||
|
@ -48,6 +50,6 @@ Then("I should get a notification {string}", (string) => {
|
|||
|
||||
Then("I should be on the User tab", () => {
|
||||
Page.matStepsHeader.then((_) => {
|
||||
cy.get('#cdk-step-label-0-2').should('have.attr', 'aria-selected', 'true');
|
||||
cy.get('#cdk-step-label-0-3').should('have.attr', 'aria-selected', 'true');
|
||||
});
|
||||
});
|
|
@ -25,7 +25,7 @@
|
|||
{
|
||||
"episodeNumber": 1,
|
||||
"title": "Our Cup Runneth Over",
|
||||
"airDate": "2015-01-13T00:00:00",
|
||||
"airDate": "2015-01-13T00:00:00Z",
|
||||
"url": "https://www.tvmaze.com/episodes/153107/schitts-creek-1x01-our-cup-runneth-over",
|
||||
"available": false,
|
||||
"approved": false,
|
||||
|
|
|
@ -20,6 +20,12 @@ class WelcomeTab {
|
|||
}
|
||||
}
|
||||
|
||||
class DatabaseTab {
|
||||
get next(): Cypress.Chainable<any> {
|
||||
return cy.getByData('nextDatabase');
|
||||
}
|
||||
}
|
||||
|
||||
class MediaServerTab {
|
||||
get next(): Cypress.Chainable<any> {
|
||||
return cy.getByData('nextMediaServer');
|
||||
|
@ -35,6 +41,7 @@ class OmbiConfigTab {
|
|||
|
||||
class WizardPage extends BasePage {
|
||||
|
||||
databaseTab: DatabaseTab;
|
||||
localUserTab: LocalUserTab;
|
||||
welcomeTab: WelcomeTab;
|
||||
mediaServerTab: MediaServerTab;
|
||||
|
@ -54,6 +61,7 @@ class WizardPage extends BasePage {
|
|||
this.welcomeTab = new WelcomeTab();
|
||||
this.mediaServerTab = new MediaServerTab();
|
||||
this.ombiConfigTab = new OmbiConfigTab();
|
||||
this.databaseTab = new DatabaseTab();
|
||||
}
|
||||
|
||||
visit(options: Cypress.VisitOptions): Cypress.Chainable<Cypress.AUTWindow>;
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"@badeball/cypress-cucumber-preprocessor": "^14.0.0",
|
||||
"@bahmutov/cy-api": "^1.5.0",
|
||||
"@bahmutov/cypress-esbuild-preprocessor": "^2.1.5",
|
||||
"cypress": "12.14.0",
|
||||
"cypress": "13.17.0",
|
||||
"cypress-wait-until": "^1.7.1",
|
||||
"typescript": "^4.2.3"
|
||||
},
|
||||
|
|
319
tests/yarn.lock
319
tests/yarn.lock
|
@ -126,10 +126,10 @@
|
|||
resolved "https://registry.yarnpkg.com/@cucumber/tag-expressions/-/tag-expressions-4.1.0.tgz#9a91b0e0dd2f2ba703e3038c52b49b9ac06c2c6f"
|
||||
integrity sha512-chTnjxV3vryL75N90wJIMdMafXmZoO2JgNJLYpsfcALL2/IQrRiny3vM9DgD5RDCSt1LNloMtb7rGey9YWxCsA==
|
||||
|
||||
"@cypress/request@^2.88.10":
|
||||
version "2.88.10"
|
||||
resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.10.tgz#b66d76b07f860d3a4b8d7a0604d020c662752cce"
|
||||
integrity sha512-Zp7F+R93N0yZyG34GutyTNr+okam7s/Fzc1+i3kcqOP8vk6OuajuE9qZJ6Rs+10/1JFtXFYMdyarnU1rZuJesg==
|
||||
"@cypress/request@^3.0.6":
|
||||
version "3.0.7"
|
||||
resolved "https://registry.yarnpkg.com/@cypress/request/-/request-3.0.7.tgz#6a74a4da98d9e5ae9121d6e2d9c14780c9b5cf1a"
|
||||
integrity sha512-LzxlLEMbBOPYB85uXrDqvD4MgcenjRBLIns3zyhx7vTPj/0u2eQhzXvPiGcaJrV38Q9dbkExWp6cOHPJ+EtFYg==
|
||||
dependencies:
|
||||
aws-sign2 "~0.7.0"
|
||||
aws4 "^1.8.0"
|
||||
|
@ -137,16 +137,16 @@
|
|||
combined-stream "~1.0.6"
|
||||
extend "~3.0.2"
|
||||
forever-agent "~0.6.1"
|
||||
form-data "~2.3.2"
|
||||
http-signature "~1.3.6"
|
||||
form-data "~4.0.0"
|
||||
http-signature "~1.4.0"
|
||||
is-typedarray "~1.0.0"
|
||||
isstream "~0.1.2"
|
||||
json-stringify-safe "~5.0.1"
|
||||
mime-types "~2.1.19"
|
||||
performance-now "^2.1.0"
|
||||
qs "~6.5.2"
|
||||
qs "6.13.1"
|
||||
safe-buffer "^5.1.2"
|
||||
tough-cookie "~2.5.0"
|
||||
tough-cookie "^5.0.0"
|
||||
tunnel-agent "^0.6.0"
|
||||
uuid "^8.3.2"
|
||||
|
||||
|
@ -168,11 +168,6 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.10.tgz#4c64759f3c2343b7e6c4b9caf761c7a3a05cee34"
|
||||
integrity sha512-juG3RWMBOqcOuXC643OAdSA525V44cVgGV6dUDuiFtss+8Fk5x1hI93Rsld43VeJVIeqlP9I7Fn9/qaVqoEAuQ==
|
||||
|
||||
"@types/node@^14.14.31":
|
||||
version "14.18.34"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.34.tgz#cd2e6fa0dbfb08a62582a7b967558e73c32061ec"
|
||||
integrity sha512-hcU9AIQVHmPnmjRK+XUUYlILlr9pQrsqSrwov/JK1pnf3GTQowVBhx54FbvM0AU/VXGH4i3+vgXS5EguR7fysA==
|
||||
|
||||
"@types/parse-json@^4.0.0":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
|
||||
|
@ -326,7 +321,7 @@ buffer-crc32@~0.2.3:
|
|||
resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
|
||||
integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==
|
||||
|
||||
buffer@^5.6.0:
|
||||
buffer@^5.7.1:
|
||||
version "5.7.1"
|
||||
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
|
||||
integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
|
||||
|
@ -339,6 +334,22 @@ cachedir@^2.3.0:
|
|||
resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-2.3.0.tgz#0c75892a052198f0b21c7c1804d8331edfcae0e8"
|
||||
integrity sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==
|
||||
|
||||
call-bind-apply-helpers@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz#32e5892e6361b29b0b545ba6f7763378daca2840"
|
||||
integrity sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==
|
||||
dependencies:
|
||||
es-errors "^1.3.0"
|
||||
function-bind "^1.1.2"
|
||||
|
||||
call-bound@^1.0.2:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.3.tgz#41cfd032b593e39176a71533ab4f384aa04fd681"
|
||||
integrity sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==
|
||||
dependencies:
|
||||
call-bind-apply-helpers "^1.0.1"
|
||||
get-intrinsic "^1.2.6"
|
||||
|
||||
callsites@^3.0.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
|
||||
|
@ -371,10 +382,10 @@ check-more-types@^2.24.0:
|
|||
resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600"
|
||||
integrity sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==
|
||||
|
||||
ci-info@^3.2.0:
|
||||
version "3.7.0"
|
||||
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.7.0.tgz#6d01b3696c59915b6ce057e4aa4adfc2fa25f5ef"
|
||||
integrity sha512-2CpRNYmImPx+RXKLq6jko/L07phmS9I02TyqkcNU20GCF/GgaWvc58hPtjxDX8lPpkdwc9sNh72V9k00S7ezog==
|
||||
ci-info@^4.0.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-4.1.0.tgz#92319d2fa29d2620180ea5afed31f589bc98cf83"
|
||||
integrity sha512-HutrvTNsF48wnxkzERIXOe5/mlcfFcbfCmwcg6CJnizbSue78AbDt+1cgl26zwn61WFxhcPykPfZrbqjGmBb4A==
|
||||
|
||||
class-transformer@0.5.1:
|
||||
version "0.5.1"
|
||||
|
@ -451,7 +462,7 @@ colors@1.0.3:
|
|||
resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b"
|
||||
integrity sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==
|
||||
|
||||
combined-stream@^1.0.6, combined-stream@~1.0.6:
|
||||
combined-stream@^1.0.8, combined-stream@~1.0.6:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
|
||||
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
|
||||
|
@ -513,23 +524,23 @@ cypress-wait-until@^1.7.1:
|
|||
resolved "https://registry.yarnpkg.com/cypress-wait-until/-/cypress-wait-until-1.7.2.tgz#7f534dd5a11c89b65359e7a0210f20d3dfc22107"
|
||||
integrity sha512-uZ+M8/MqRcpf+FII/UZrU7g1qYZ4aVlHcgyVopnladyoBrpoaMJ4PKZDrdOJ05H5RHbr7s9Tid635X3E+ZLU/Q==
|
||||
|
||||
cypress@12.14.0:
|
||||
version "12.14.0"
|
||||
resolved "https://registry.yarnpkg.com/cypress/-/cypress-12.14.0.tgz#37a19b85f5e9d881995e9fee1ddf41b3d3a623dd"
|
||||
integrity sha512-HiLIXKXZaIT1RT7sw1sVPt+qKtis3uYNm6KwC4qoYjabwLKaqZlyS/P+uVvvlBNcHIwL/BC6nQZajpbUd7hOgQ==
|
||||
cypress@13.17.0:
|
||||
version "13.17.0"
|
||||
resolved "https://registry.yarnpkg.com/cypress/-/cypress-13.17.0.tgz#34c3d68080c4497eace0f353bd1629587a5f600d"
|
||||
integrity sha512-5xWkaPurwkIljojFidhw8lFScyxhtiFHl/i/3zov+1Z5CmY4t9tjIdvSXfu82Y3w7wt0uR9KkucbhkVvJZLQSA==
|
||||
dependencies:
|
||||
"@cypress/request" "^2.88.10"
|
||||
"@cypress/request" "^3.0.6"
|
||||
"@cypress/xvfb" "^1.2.4"
|
||||
"@types/node" "^14.14.31"
|
||||
"@types/sinonjs__fake-timers" "8.1.1"
|
||||
"@types/sizzle" "^2.3.2"
|
||||
arch "^2.2.0"
|
||||
blob-util "^2.0.2"
|
||||
bluebird "^3.7.2"
|
||||
buffer "^5.6.0"
|
||||
buffer "^5.7.1"
|
||||
cachedir "^2.3.0"
|
||||
chalk "^4.1.0"
|
||||
check-more-types "^2.24.0"
|
||||
ci-info "^4.0.0"
|
||||
cli-cursor "^3.1.0"
|
||||
cli-table3 "~0.6.1"
|
||||
commander "^6.2.1"
|
||||
|
@ -544,7 +555,6 @@ cypress@12.14.0:
|
|||
figures "^3.2.0"
|
||||
fs-extra "^9.1.0"
|
||||
getos "^3.2.1"
|
||||
is-ci "^3.0.0"
|
||||
is-installed-globally "~0.4.0"
|
||||
lazy-ass "^1.6.0"
|
||||
listr2 "^3.8.3"
|
||||
|
@ -553,11 +563,13 @@ cypress@12.14.0:
|
|||
minimist "^1.2.8"
|
||||
ospath "^1.2.2"
|
||||
pretty-bytes "^5.6.0"
|
||||
process "^0.11.10"
|
||||
proxy-from-env "1.0.0"
|
||||
request-progress "^3.0.0"
|
||||
semver "^7.3.2"
|
||||
semver "^7.5.3"
|
||||
supports-color "^8.1.1"
|
||||
tmp "~0.2.1"
|
||||
tmp "~0.2.3"
|
||||
tree-kill "1.2.2"
|
||||
untildify "^4.0.0"
|
||||
yauzl "^2.10.0"
|
||||
|
||||
|
@ -592,6 +604,15 @@ delayed-stream@~1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
||||
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
|
||||
|
||||
dunder-proto@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a"
|
||||
integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==
|
||||
dependencies:
|
||||
call-bind-apply-helpers "^1.0.1"
|
||||
es-errors "^1.3.0"
|
||||
gopd "^1.2.0"
|
||||
|
||||
ecc-jsbn@~0.1.1:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
|
||||
|
@ -633,6 +654,23 @@ error-stack-parser@^2.0.7:
|
|||
dependencies:
|
||||
stackframe "^1.3.4"
|
||||
|
||||
es-define-property@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa"
|
||||
integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==
|
||||
|
||||
es-errors@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
|
||||
integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
|
||||
|
||||
es-object-atoms@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941"
|
||||
integrity sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==
|
||||
dependencies:
|
||||
es-errors "^1.3.0"
|
||||
|
||||
esbuild-android-64@0.14.54:
|
||||
version "0.14.54"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz#505f41832884313bbaffb27704b8bcaa2d8616be"
|
||||
|
@ -837,13 +875,13 @@ forever-agent@~0.6.1:
|
|||
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
|
||||
integrity sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==
|
||||
|
||||
form-data@~2.3.2:
|
||||
version "2.3.3"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
|
||||
integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==
|
||||
form-data@~4.0.0:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.1.tgz#ba1076daaaa5bfd7e99c1a6cb02aa0a5cff90d48"
|
||||
integrity sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==
|
||||
dependencies:
|
||||
asynckit "^0.4.0"
|
||||
combined-stream "^1.0.6"
|
||||
combined-stream "^1.0.8"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
fs-extra@^9.1.0:
|
||||
|
@ -861,6 +899,35 @@ fs.realpath@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
|
||||
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
|
||||
|
||||
function-bind@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
|
||||
integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
|
||||
|
||||
get-intrinsic@^1.2.5, get-intrinsic@^1.2.6:
|
||||
version "1.2.7"
|
||||
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.7.tgz#dcfcb33d3272e15f445d15124bc0a216189b9044"
|
||||
integrity sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==
|
||||
dependencies:
|
||||
call-bind-apply-helpers "^1.0.1"
|
||||
es-define-property "^1.0.1"
|
||||
es-errors "^1.3.0"
|
||||
es-object-atoms "^1.0.0"
|
||||
function-bind "^1.1.2"
|
||||
get-proto "^1.0.0"
|
||||
gopd "^1.2.0"
|
||||
has-symbols "^1.1.0"
|
||||
hasown "^2.0.2"
|
||||
math-intrinsics "^1.1.0"
|
||||
|
||||
get-proto@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1"
|
||||
integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==
|
||||
dependencies:
|
||||
dunder-proto "^1.0.1"
|
||||
es-object-atoms "^1.0.0"
|
||||
|
||||
get-stream@^5.0.0, get-stream@^5.1.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3"
|
||||
|
@ -882,7 +949,7 @@ getpass@^0.1.1:
|
|||
dependencies:
|
||||
assert-plus "^1.0.0"
|
||||
|
||||
glob@^7.1.3, glob@^7.1.6, glob@^7.2.0:
|
||||
glob@^7.1.6, glob@^7.2.0:
|
||||
version "7.2.3"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
|
||||
integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
|
||||
|
@ -901,6 +968,11 @@ global-dirs@^3.0.0:
|
|||
dependencies:
|
||||
ini "2.0.0"
|
||||
|
||||
gopd@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1"
|
||||
integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==
|
||||
|
||||
graceful-fs@^4.1.6, graceful-fs@^4.2.0:
|
||||
version "4.2.10"
|
||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c"
|
||||
|
@ -916,14 +988,26 @@ has-flag@^4.0.0:
|
|||
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
|
||||
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
|
||||
|
||||
http-signature@~1.3.6:
|
||||
version "1.3.6"
|
||||
resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.3.6.tgz#cb6fbfdf86d1c974f343be94e87f7fc128662cf9"
|
||||
integrity sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==
|
||||
has-symbols@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338"
|
||||
integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==
|
||||
|
||||
hasown@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003"
|
||||
integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
|
||||
dependencies:
|
||||
function-bind "^1.1.2"
|
||||
|
||||
http-signature@~1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.4.0.tgz#dee5a9ba2bf49416abc544abd6d967f6a94c8c3f"
|
||||
integrity sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==
|
||||
dependencies:
|
||||
assert-plus "^1.0.0"
|
||||
jsprim "^2.0.2"
|
||||
sshpk "^1.14.1"
|
||||
sshpk "^1.18.0"
|
||||
|
||||
human-signals@^1.1.1:
|
||||
version "1.1.1"
|
||||
|
@ -971,13 +1055,6 @@ is-arrayish@^0.2.1:
|
|||
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
|
||||
integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==
|
||||
|
||||
is-ci@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-3.0.1.tgz#db6ecbed1bd659c43dac0f45661e7674103d1867"
|
||||
integrity sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==
|
||||
dependencies:
|
||||
ci-info "^3.2.0"
|
||||
|
||||
is-fullwidth-code-point@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
|
||||
|
@ -1117,12 +1194,10 @@ log-update@^4.0.0:
|
|||
slice-ansi "^4.0.0"
|
||||
wrap-ansi "^6.2.0"
|
||||
|
||||
lru-cache@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
|
||||
integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
|
||||
dependencies:
|
||||
yallist "^4.0.0"
|
||||
math-intrinsics@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9"
|
||||
integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==
|
||||
|
||||
merge-stream@^2.0.0:
|
||||
version "2.0.0"
|
||||
|
@ -1185,6 +1260,11 @@ npm-run-path@^4.0.0:
|
|||
dependencies:
|
||||
path-key "^3.0.0"
|
||||
|
||||
object-inspect@^1.13.3:
|
||||
version "1.13.3"
|
||||
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.3.tgz#f14c183de51130243d6d18ae149375ff50ea488a"
|
||||
integrity sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==
|
||||
|
||||
once@^1.3.0, once@^1.3.1, once@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
|
||||
|
@ -1263,16 +1343,16 @@ pretty-bytes@^5.6.0:
|
|||
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
|
||||
integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==
|
||||
|
||||
process@^0.11.10:
|
||||
version "0.11.10"
|
||||
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
|
||||
integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==
|
||||
|
||||
proxy-from-env@1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee"
|
||||
integrity sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==
|
||||
|
||||
psl@^1.1.28:
|
||||
version "1.9.0"
|
||||
resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7"
|
||||
integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==
|
||||
|
||||
pump@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
|
||||
|
@ -1281,15 +1361,12 @@ pump@^3.0.0:
|
|||
end-of-stream "^1.1.0"
|
||||
once "^1.3.1"
|
||||
|
||||
punycode@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
|
||||
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
|
||||
|
||||
qs@~6.5.2:
|
||||
version "6.5.3"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad"
|
||||
integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==
|
||||
qs@6.13.1:
|
||||
version "6.13.1"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.1.tgz#3ce5fc72bd3a8171b85c99b93c65dd20b7d1b16e"
|
||||
integrity sha512-EJPeIn0CYrGu+hli1xilKAPXODtJ12T0sP63Ijx2/khC2JtuaN3JyNIpvmnkmaEtha9ocbG4A4cMcr+TvqvwQg==
|
||||
dependencies:
|
||||
side-channel "^1.0.6"
|
||||
|
||||
reflect-metadata@0.1.13:
|
||||
version "0.1.13"
|
||||
|
@ -1345,13 +1422,6 @@ rfdc@^1.3.0:
|
|||
resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b"
|
||||
integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==
|
||||
|
||||
rimraf@^3.0.0:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
|
||||
integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
|
||||
dependencies:
|
||||
glob "^7.1.3"
|
||||
|
||||
rxjs@^7.5.1:
|
||||
version "7.5.7"
|
||||
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.7.tgz#2ec0d57fdc89ece220d2e702730ae8f1e49def39"
|
||||
|
@ -1369,12 +1439,10 @@ safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
|
|||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||
|
||||
semver@^7.3.2:
|
||||
version "7.3.8"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798"
|
||||
integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==
|
||||
dependencies:
|
||||
lru-cache "^6.0.0"
|
||||
semver@^7.5.3:
|
||||
version "7.6.3"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143"
|
||||
integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==
|
||||
|
||||
shebang-command@^2.0.0:
|
||||
version "2.0.0"
|
||||
|
@ -1388,6 +1456,46 @@ shebang-regex@^3.0.0:
|
|||
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
|
||||
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
|
||||
|
||||
side-channel-list@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad"
|
||||
integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==
|
||||
dependencies:
|
||||
es-errors "^1.3.0"
|
||||
object-inspect "^1.13.3"
|
||||
|
||||
side-channel-map@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42"
|
||||
integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==
|
||||
dependencies:
|
||||
call-bound "^1.0.2"
|
||||
es-errors "^1.3.0"
|
||||
get-intrinsic "^1.2.5"
|
||||
object-inspect "^1.13.3"
|
||||
|
||||
side-channel-weakmap@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea"
|
||||
integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==
|
||||
dependencies:
|
||||
call-bound "^1.0.2"
|
||||
es-errors "^1.3.0"
|
||||
get-intrinsic "^1.2.5"
|
||||
object-inspect "^1.13.3"
|
||||
side-channel-map "^1.0.1"
|
||||
|
||||
side-channel@^1.0.6:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9"
|
||||
integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==
|
||||
dependencies:
|
||||
es-errors "^1.3.0"
|
||||
object-inspect "^1.13.3"
|
||||
side-channel-list "^1.0.0"
|
||||
side-channel-map "^1.0.1"
|
||||
side-channel-weakmap "^1.0.2"
|
||||
|
||||
signal-exit@^3.0.2:
|
||||
version "3.0.7"
|
||||
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
|
||||
|
@ -1416,10 +1524,10 @@ source-map@^0.7.4:
|
|||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656"
|
||||
integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==
|
||||
|
||||
sshpk@^1.14.1:
|
||||
version "1.17.0"
|
||||
resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.17.0.tgz#578082d92d4fe612b13007496e543fa0fbcbe4c5"
|
||||
integrity sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==
|
||||
sshpk@^1.18.0:
|
||||
version "1.18.0"
|
||||
resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.18.0.tgz#1663e55cddf4d688b86a46b77f0d5fe363aba028"
|
||||
integrity sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==
|
||||
dependencies:
|
||||
asn1 "~0.2.3"
|
||||
assert-plus "^1.0.0"
|
||||
|
@ -1488,20 +1596,34 @@ through@^2.3.8:
|
|||
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
|
||||
integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==
|
||||
|
||||
tmp@~0.2.1:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14"
|
||||
integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==
|
||||
dependencies:
|
||||
rimraf "^3.0.0"
|
||||
tldts-core@^6.1.70:
|
||||
version "6.1.70"
|
||||
resolved "https://registry.yarnpkg.com/tldts-core/-/tldts-core-6.1.70.tgz#a954e93237ece2e1705b438600793c86a25f8c00"
|
||||
integrity sha512-RNnIXDB1FD4T9cpQRErEqw6ZpjLlGdMOitdV+0xtbsnwr4YFka1zpc7D4KD+aAn8oSG5JyFrdasZTE04qDE9Yg==
|
||||
|
||||
tough-cookie@~2.5.0:
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
|
||||
integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==
|
||||
tldts@^6.1.32:
|
||||
version "6.1.70"
|
||||
resolved "https://registry.yarnpkg.com/tldts/-/tldts-6.1.70.tgz#b571e5645ab9dc6f289453115d52602b8a384cfe"
|
||||
integrity sha512-/W1YVgYVJd9ZDjey5NXadNh0mJXkiUMUue9Zebd0vpdo1sU+H4zFFTaJ1RKD4N6KFoHfcXy6l+Vu7bh+bdWCzA==
|
||||
dependencies:
|
||||
psl "^1.1.28"
|
||||
punycode "^2.1.1"
|
||||
tldts-core "^6.1.70"
|
||||
|
||||
tmp@~0.2.3:
|
||||
version "0.2.3"
|
||||
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.3.tgz#eb783cc22bc1e8bebd0671476d46ea4eb32a79ae"
|
||||
integrity sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==
|
||||
|
||||
tough-cookie@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-5.0.0.tgz#6b6518e2b5c070cf742d872ee0f4f92d69eac1af"
|
||||
integrity sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==
|
||||
dependencies:
|
||||
tldts "^6.1.32"
|
||||
|
||||
tree-kill@1.2.2:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc"
|
||||
integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==
|
||||
|
||||
tslib@^2.1.0:
|
||||
version "2.4.1"
|
||||
|
@ -1589,11 +1711,6 @@ wrappy@1:
|
|||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
|
||||
|
||||
yallist@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
|
||||
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
|
||||
|
||||
yaml@^1.10.0:
|
||||
version "1.10.2"
|
||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"version": "4.46.5"
|
||||
"version": "4.49.3"
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue