mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2026-04-17 21:44:48 -04:00
Compare commits
1 Commits
v1.0.1.221
...
instance-n
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0be2aee9c3 |
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -5,9 +5,9 @@ body:
|
|||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
attributes:
|
attributes:
|
||||||
label: Is there an existing issue for this?
|
label: Is there an existing issue for this?
|
||||||
description: Please search to see if an open or closed issue already exists for the bug you encountered. If a bug exists and is closed note that it may only be fixed in an unstable branch.
|
description: Please search to see if an issue already exists for the bug you encountered.
|
||||||
options:
|
options:
|
||||||
- label: I have searched the existing open and closed issues
|
- label: I have searched the existing issues
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
4
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
4
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -5,9 +5,9 @@ body:
|
|||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
attributes:
|
attributes:
|
||||||
label: Is there an existing issue for this?
|
label: Is there an existing issue for this?
|
||||||
description: Please search to see if an open or closed issue already exists for the feature you are requesting. If a request exists and is closed note that it may only be fixed in an unstable branch.
|
description: Please search to see if an issue already exists for the feature you are requesting.
|
||||||
options:
|
options:
|
||||||
- label: I have searched the existing open and closed issues
|
- label: I have searched the existing issues
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
@@ -1,132 +0,0 @@
|
|||||||
# Contributor Covenant Code of Conduct
|
|
||||||
|
|
||||||
## Our Pledge
|
|
||||||
|
|
||||||
We as members, contributors, and leaders pledge to make participation in our
|
|
||||||
community a harassment-free experience for everyone, regardless of age, body
|
|
||||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
|
||||||
identity and expression, level of experience, education, socio-economic status,
|
|
||||||
nationality, personal appearance, race, caste, color, religion, or sexual
|
|
||||||
identity and orientation.
|
|
||||||
|
|
||||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
|
||||||
diverse, inclusive, and healthy community.
|
|
||||||
|
|
||||||
## Our Standards
|
|
||||||
|
|
||||||
Examples of behavior that contributes to a positive environment for our
|
|
||||||
community include:
|
|
||||||
|
|
||||||
* Demonstrating empathy and kindness toward other people
|
|
||||||
* Being respectful of differing opinions, viewpoints, and experiences
|
|
||||||
* Giving and gracefully accepting constructive feedback
|
|
||||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
|
||||||
and learning from the experience
|
|
||||||
* Focusing on what is best not just for us as individuals, but for the overall
|
|
||||||
community
|
|
||||||
|
|
||||||
Examples of unacceptable behavior include:
|
|
||||||
|
|
||||||
* The use of sexualized language or imagery, and sexual attention or advances of
|
|
||||||
any kind
|
|
||||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
|
||||||
* Public or private harassment
|
|
||||||
* Publishing others' private information, such as a physical or email address,
|
|
||||||
without their explicit permission
|
|
||||||
* Other conduct which could reasonably be considered inappropriate in a
|
|
||||||
professional setting
|
|
||||||
|
|
||||||
## Enforcement Responsibilities
|
|
||||||
|
|
||||||
Community leaders are responsible for clarifying and enforcing our standards of
|
|
||||||
acceptable behavior and will take appropriate and fair corrective action in
|
|
||||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
|
||||||
or harmful.
|
|
||||||
|
|
||||||
Community leaders have the right and responsibility to remove, edit, or reject
|
|
||||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
|
||||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
|
||||||
decisions when appropriate.
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
This Code of Conduct applies within all community spaces, and also applies when
|
|
||||||
an individual is officially representing the community in public spaces.
|
|
||||||
Examples of representing our community include using an official e-mail address,
|
|
||||||
posting via an official social media account, or acting as an appointed
|
|
||||||
representative at an online or offline event.
|
|
||||||
|
|
||||||
## Enforcement
|
|
||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
|
||||||
reported to the community leaders responsible for enforcement at
|
|
||||||
<development@prowlarr.com>.
|
|
||||||
All complaints will be reviewed and investigated promptly and fairly.
|
|
||||||
|
|
||||||
All community leaders are obligated to respect the privacy and security of the
|
|
||||||
reporter of any incident.
|
|
||||||
|
|
||||||
## Enforcement Guidelines
|
|
||||||
|
|
||||||
Community leaders will follow these Community Impact Guidelines in determining
|
|
||||||
the consequences for any action they deem in violation of this Code of Conduct:
|
|
||||||
|
|
||||||
### 1. Correction
|
|
||||||
|
|
||||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
|
||||||
unprofessional or unwelcome in the community.
|
|
||||||
|
|
||||||
**Consequence**: A private, written warning from community leaders, providing
|
|
||||||
clarity around the nature of the violation and an explanation of why the
|
|
||||||
behavior was inappropriate. A public apology may be requested.
|
|
||||||
|
|
||||||
### 2. Warning
|
|
||||||
|
|
||||||
**Community Impact**: A violation through a single incident or series of
|
|
||||||
actions.
|
|
||||||
|
|
||||||
**Consequence**: A warning with consequences for continued behavior. No
|
|
||||||
interaction with the people involved, including unsolicited interaction with
|
|
||||||
those enforcing the Code of Conduct, for a specified period of time. This
|
|
||||||
includes avoiding interactions in community spaces as well as external channels
|
|
||||||
like social media. Violating these terms may lead to a temporary or permanent
|
|
||||||
ban.
|
|
||||||
|
|
||||||
### 3. Temporary Ban
|
|
||||||
|
|
||||||
**Community Impact**: A serious violation of community standards, including
|
|
||||||
sustained inappropriate behavior.
|
|
||||||
|
|
||||||
**Consequence**: A temporary ban from any sort of interaction or public
|
|
||||||
communication with the community for a specified period of time. No public or
|
|
||||||
private interaction with the people involved, including unsolicited interaction
|
|
||||||
with those enforcing the Code of Conduct, is allowed during this period.
|
|
||||||
Violating these terms may lead to a permanent ban.
|
|
||||||
|
|
||||||
### 4. Permanent Ban
|
|
||||||
|
|
||||||
**Community Impact**: Demonstrating a pattern of violation of community
|
|
||||||
standards, including sustained inappropriate behavior, harassment of an
|
|
||||||
individual, or aggression toward or disparagement of classes of individuals.
|
|
||||||
|
|
||||||
**Consequence**: A permanent ban from any sort of public interaction within the
|
|
||||||
community.
|
|
||||||
|
|
||||||
## Attribution
|
|
||||||
|
|
||||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
|
||||||
version 2.1, available at
|
|
||||||
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
|
||||||
|
|
||||||
Community Impact Guidelines were inspired by
|
|
||||||
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
|
||||||
|
|
||||||
For answers to common questions about this code of conduct, see the FAQ at
|
|
||||||
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
|
|
||||||
[https://www.contributor-covenant.org/translations][translations].
|
|
||||||
|
|
||||||
[homepage]: https://www.contributor-covenant.org
|
|
||||||
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
|
||||||
[Mozilla CoC]: https://github.com/mozilla/diversity
|
|
||||||
[FAQ]: https://www.contributor-covenant.org/faq
|
|
||||||
[translations]: https://www.contributor-covenant.org/translations
|
|
||||||
@@ -27,7 +27,10 @@ Prowlarr is an indexer manager/proxy built on the popular \*arr .net/reactjs bas
|
|||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
|
||||||
|
Note: Prowlarr is currently early in life, thus bugs should be expected
|
||||||
|
|
||||||
[](https://wiki.servarr.com/prowlarr)
|
[](https://wiki.servarr.com/prowlarr)
|
||||||
|
|
||||||
[](https://prowlarr.com/discord)
|
[](https://prowlarr.com/discord)
|
||||||
[](https://www.reddit.com/r/Prowlarr)
|
[](https://www.reddit.com/r/Prowlarr)
|
||||||
|
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ variables:
|
|||||||
testsFolder: './_tests'
|
testsFolder: './_tests'
|
||||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||||
majorVersion: '1.0.1'
|
majorVersion: '0.4.0'
|
||||||
minorVersion: $[counter('minorVersion', 1)]
|
minorVersion: $[counter('minorVersion', 1)]
|
||||||
prowlarrVersion: '$(majorVersion).$(minorVersion)'
|
prowlarrVersion: '$(majorVersion).$(minorVersion)'
|
||||||
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
|
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
|
||||||
sentryOrg: 'servarr'
|
sentryOrg: 'servarr'
|
||||||
sentryUrl: 'https://sentry.servarr.com'
|
sentryUrl: 'https://sentry.servarr.com'
|
||||||
dotnetVersion: '6.0.404'
|
dotnetVersion: '6.0.201'
|
||||||
innoVersion: '6.2.0'
|
innoVersion: '6.2.0'
|
||||||
nodeVersion: '16.x'
|
nodeVersion: '16.x'
|
||||||
windowsImage: 'windows-2022'
|
windowsImage: 'windows-2022'
|
||||||
@@ -97,14 +97,15 @@ stages:
|
|||||||
- bash: |
|
- bash: |
|
||||||
BUNDLEDVERSIONS=${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}/Microsoft.NETCoreSdk.BundledVersions.props
|
BUNDLEDVERSIONS=${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}/Microsoft.NETCoreSdk.BundledVersions.props
|
||||||
echo $BUNDLEDVERSIONS
|
echo $BUNDLEDVERSIONS
|
||||||
|
grep osx-x64 $BUNDLEDVERSIONS
|
||||||
if grep -q freebsd-x64 $BUNDLEDVERSIONS; then
|
if grep -q freebsd-x64 $BUNDLEDVERSIONS; then
|
||||||
echo "Extra platforms already enabled"
|
echo "BSD already enabled"
|
||||||
else
|
else
|
||||||
echo "Enabling extra platform support"
|
echo "Enabling BSD support"
|
||||||
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
|
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64/' $BUNDLEDVERSIONS
|
||||||
fi
|
fi
|
||||||
displayName: Enable Extra Platform Support
|
displayName: Enable FreeBSD Support
|
||||||
- bash: ./build.sh --backend --enable-extra-platforms
|
- bash: ./build.sh --backend --enable-bsd
|
||||||
displayName: Build Prowlarr Backend
|
displayName: Build Prowlarr Backend
|
||||||
- bash: |
|
- bash: |
|
||||||
find ${OUTPUTFOLDER} -type f ! -path "*/publish/*" -exec rm -rf {} \;
|
find ${OUTPUTFOLDER} -type f ! -path "*/publish/*" -exec rm -rf {} \;
|
||||||
@@ -118,28 +119,24 @@ stages:
|
|||||||
displayName: Publish Backend
|
displayName: Publish Backend
|
||||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||||
- publish: '$(testsFolder)/net6.0/win-x64/publish'
|
- publish: '$(testsFolder)/net6.0/win-x64/publish'
|
||||||
artifact: win-x64-tests
|
artifact: WindowsCoreTests
|
||||||
displayName: Publish win-x64 Test Package
|
displayName: Publish Windows Test Package
|
||||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||||
- publish: '$(testsFolder)/net6.0/linux-x64/publish'
|
- publish: '$(testsFolder)/net6.0/linux-x64/publish'
|
||||||
artifact: linux-x64-tests
|
artifact: LinuxCoreTests
|
||||||
displayName: Publish linux-x64 Test Package
|
displayName: Publish Linux Test Package
|
||||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
|
||||||
- publish: '$(testsFolder)/net6.0/linux-x86/publish'
|
|
||||||
artifact: linux-x86-tests
|
|
||||||
displayName: Publish linux-x86 Test Package
|
|
||||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||||
- publish: '$(testsFolder)/net6.0/linux-musl-x64/publish'
|
- publish: '$(testsFolder)/net6.0/linux-musl-x64/publish'
|
||||||
artifact: linux-musl-x64-tests
|
artifact: LinuxMuslCoreTests
|
||||||
displayName: Publish linux-musl-x64 Test Package
|
displayName: Publish Linux Musl Test Package
|
||||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||||
- publish: '$(testsFolder)/net6.0/freebsd-x64/publish'
|
- publish: '$(testsFolder)/net6.0/freebsd-x64/publish'
|
||||||
artifact: freebsd-x64-tests
|
artifact: FreebsdCoreTests
|
||||||
displayName: Publish freebsd-x64 Test Package
|
displayName: Publish FreeBSD Test Package
|
||||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||||
- publish: '$(testsFolder)/net6.0/osx-x64/publish'
|
- publish: '$(testsFolder)/net6.0/osx-x64/publish'
|
||||||
artifact: osx-x64-tests
|
artifact: MacCoreTests
|
||||||
displayName: Publish osx-x64 Test Package
|
displayName: Publish MacOS Test Package
|
||||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||||
|
|
||||||
- stage: Build_Frontend
|
- stage: Build_Frontend
|
||||||
@@ -242,35 +239,35 @@ stages:
|
|||||||
artifactName: WindowsFrontend
|
artifactName: WindowsFrontend
|
||||||
targetPath: _output
|
targetPath: _output
|
||||||
displayName: Fetch Frontend
|
displayName: Fetch Frontend
|
||||||
- bash: ./build.sh --packages --enable-extra-platforms
|
- bash: ./build.sh --packages --enable-bsd
|
||||||
displayName: Create Packages
|
displayName: Create Packages
|
||||||
- bash: |
|
- bash: |
|
||||||
find . -name "Prowlarr" -exec chmod a+x {} \;
|
find . -name "Prowlarr" -exec chmod a+x {} \;
|
||||||
find . -name "Prowlarr.Update" -exec chmod a+x {} \;
|
find . -name "Prowlarr.Update" -exec chmod a+x {} \;
|
||||||
displayName: Set executable bits
|
displayName: Set executable bits
|
||||||
- task: ArchiveFiles@2
|
- task: ArchiveFiles@2
|
||||||
displayName: Create win-x64 zip
|
displayName: Create Windows Core zip
|
||||||
inputs:
|
inputs:
|
||||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).windows-core-x64.zip'
|
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).windows-core-x64.zip'
|
||||||
archiveType: 'zip'
|
archiveType: 'zip'
|
||||||
includeRootFolder: false
|
includeRootFolder: false
|
||||||
rootFolderOrFile: $(artifactsFolder)/win-x64/net6.0
|
rootFolderOrFile: $(artifactsFolder)/win-x64/net6.0
|
||||||
- task: ArchiveFiles@2
|
- task: ArchiveFiles@2
|
||||||
displayName: Create win-x86 zip
|
displayName: Create Windows x86 Core zip
|
||||||
inputs:
|
inputs:
|
||||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).windows-core-x86.zip'
|
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).windows-core-x86.zip'
|
||||||
archiveType: 'zip'
|
archiveType: 'zip'
|
||||||
includeRootFolder: false
|
includeRootFolder: false
|
||||||
rootFolderOrFile: $(artifactsFolder)/win-x86/net6.0
|
rootFolderOrFile: $(artifactsFolder)/win-x86/net6.0
|
||||||
- task: ArchiveFiles@2
|
- task: ArchiveFiles@2
|
||||||
displayName: Create osx-x64 app
|
displayName: Create MacOS x64 Core app
|
||||||
inputs:
|
inputs:
|
||||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).osx-app-core-x64.zip'
|
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).osx-app-core-x64.zip'
|
||||||
archiveType: 'zip'
|
archiveType: 'zip'
|
||||||
includeRootFolder: false
|
includeRootFolder: false
|
||||||
rootFolderOrFile: $(artifactsFolder)/osx-x64-app/net6.0
|
rootFolderOrFile: $(artifactsFolder)/osx-x64-app/net6.0
|
||||||
- task: ArchiveFiles@2
|
- task: ArchiveFiles@2
|
||||||
displayName: Create osx-x64 tar
|
displayName: Create MacOS x64 Core tar
|
||||||
inputs:
|
inputs:
|
||||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).osx-core-x64.tar.gz'
|
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).osx-core-x64.tar.gz'
|
||||||
archiveType: 'tar'
|
archiveType: 'tar'
|
||||||
@@ -278,14 +275,14 @@ stages:
|
|||||||
includeRootFolder: false
|
includeRootFolder: false
|
||||||
rootFolderOrFile: $(artifactsFolder)/osx-x64/net6.0
|
rootFolderOrFile: $(artifactsFolder)/osx-x64/net6.0
|
||||||
- task: ArchiveFiles@2
|
- task: ArchiveFiles@2
|
||||||
displayName: Create osx-arm64 app
|
displayName: Create MacOS arm64 Core app
|
||||||
inputs:
|
inputs:
|
||||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).osx-app-core-arm64.zip'
|
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).osx-app-core-arm64.zip'
|
||||||
archiveType: 'zip'
|
archiveType: 'zip'
|
||||||
includeRootFolder: false
|
includeRootFolder: false
|
||||||
rootFolderOrFile: $(artifactsFolder)/osx-arm64-app/net6.0
|
rootFolderOrFile: $(artifactsFolder)/osx-arm64-app/net6.0
|
||||||
- task: ArchiveFiles@2
|
- task: ArchiveFiles@2
|
||||||
displayName: Create osx-arm64 tar
|
displayName: Create MacOS arm64 Core tar
|
||||||
inputs:
|
inputs:
|
||||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).osx-core-arm64.tar.gz'
|
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).osx-core-arm64.tar.gz'
|
||||||
archiveType: 'tar'
|
archiveType: 'tar'
|
||||||
@@ -293,7 +290,7 @@ stages:
|
|||||||
includeRootFolder: false
|
includeRootFolder: false
|
||||||
rootFolderOrFile: $(artifactsFolder)/osx-arm64/net6.0
|
rootFolderOrFile: $(artifactsFolder)/osx-arm64/net6.0
|
||||||
- task: ArchiveFiles@2
|
- task: ArchiveFiles@2
|
||||||
displayName: Create linux-x64 tar
|
displayName: Create Linux Core tar
|
||||||
inputs:
|
inputs:
|
||||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-core-x64.tar.gz'
|
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-core-x64.tar.gz'
|
||||||
archiveType: 'tar'
|
archiveType: 'tar'
|
||||||
@@ -301,7 +298,7 @@ stages:
|
|||||||
includeRootFolder: false
|
includeRootFolder: false
|
||||||
rootFolderOrFile: $(artifactsFolder)/linux-x64/net6.0
|
rootFolderOrFile: $(artifactsFolder)/linux-x64/net6.0
|
||||||
- task: ArchiveFiles@2
|
- task: ArchiveFiles@2
|
||||||
displayName: Create linux-musl-x64 tar
|
displayName: Create Linux Musl Core tar
|
||||||
inputs:
|
inputs:
|
||||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-musl-core-x64.tar.gz'
|
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-musl-core-x64.tar.gz'
|
||||||
archiveType: 'tar'
|
archiveType: 'tar'
|
||||||
@@ -309,15 +306,7 @@ stages:
|
|||||||
includeRootFolder: false
|
includeRootFolder: false
|
||||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/net6.0
|
rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/net6.0
|
||||||
- task: ArchiveFiles@2
|
- task: ArchiveFiles@2
|
||||||
displayName: Create linux-x86 tar
|
displayName: Create ARM32 Linux Core tar
|
||||||
inputs:
|
|
||||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-core-x86.tar.gz'
|
|
||||||
archiveType: 'tar'
|
|
||||||
tarCompression: 'gz'
|
|
||||||
includeRootFolder: false
|
|
||||||
rootFolderOrFile: $(artifactsFolder)/linux-x86/net6.0
|
|
||||||
- task: ArchiveFiles@2
|
|
||||||
displayName: Create linux-arm tar
|
|
||||||
inputs:
|
inputs:
|
||||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-core-arm.tar.gz'
|
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-core-arm.tar.gz'
|
||||||
archiveType: 'tar'
|
archiveType: 'tar'
|
||||||
@@ -325,7 +314,7 @@ stages:
|
|||||||
includeRootFolder: false
|
includeRootFolder: false
|
||||||
rootFolderOrFile: $(artifactsFolder)/linux-arm/net6.0
|
rootFolderOrFile: $(artifactsFolder)/linux-arm/net6.0
|
||||||
- task: ArchiveFiles@2
|
- task: ArchiveFiles@2
|
||||||
displayName: Create linux-musl-arm tar
|
displayName: Create ARM32 Linux Musl Core tar
|
||||||
inputs:
|
inputs:
|
||||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-musl-core-arm.tar.gz'
|
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-musl-core-arm.tar.gz'
|
||||||
archiveType: 'tar'
|
archiveType: 'tar'
|
||||||
@@ -333,7 +322,7 @@ stages:
|
|||||||
includeRootFolder: false
|
includeRootFolder: false
|
||||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm/net6.0
|
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm/net6.0
|
||||||
- task: ArchiveFiles@2
|
- task: ArchiveFiles@2
|
||||||
displayName: Create linux-arm64 tar
|
displayName: Create ARM64 Linux Core tar
|
||||||
inputs:
|
inputs:
|
||||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-core-arm64.tar.gz'
|
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-core-arm64.tar.gz'
|
||||||
archiveType: 'tar'
|
archiveType: 'tar'
|
||||||
@@ -341,7 +330,7 @@ stages:
|
|||||||
includeRootFolder: false
|
includeRootFolder: false
|
||||||
rootFolderOrFile: $(artifactsFolder)/linux-arm64/net6.0
|
rootFolderOrFile: $(artifactsFolder)/linux-arm64/net6.0
|
||||||
- task: ArchiveFiles@2
|
- task: ArchiveFiles@2
|
||||||
displayName: Create linux-musl-arm64 tar
|
displayName: Create ARM64 Linux Musl Core tar
|
||||||
inputs:
|
inputs:
|
||||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-musl-core-arm64.tar.gz'
|
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-musl-core-arm64.tar.gz'
|
||||||
archiveType: 'tar'
|
archiveType: 'tar'
|
||||||
@@ -416,22 +405,22 @@ stages:
|
|||||||
matrix:
|
matrix:
|
||||||
MacCore:
|
MacCore:
|
||||||
osName: 'Mac'
|
osName: 'Mac'
|
||||||
testName: 'osx-x64'
|
testName: 'MacCore'
|
||||||
poolName: 'Azure Pipelines'
|
poolName: 'Azure Pipelines'
|
||||||
imageName: ${{ variables.macImage }}
|
imageName: ${{ variables.macImage }}
|
||||||
WindowsCore:
|
WindowsCore:
|
||||||
osName: 'Windows'
|
osName: 'Windows'
|
||||||
testName: 'win-x64'
|
testName: 'WindowsCore'
|
||||||
poolName: 'Azure Pipelines'
|
poolName: 'Azure Pipelines'
|
||||||
imageName: ${{ variables.windowsImage }}
|
imageName: ${{ variables.windowsImage }}
|
||||||
LinuxCore:
|
LinuxCore:
|
||||||
osName: 'Linux'
|
osName: 'Linux'
|
||||||
testName: 'linux-x64'
|
testName: 'LinuxCore'
|
||||||
poolName: 'Azure Pipelines'
|
poolName: 'Azure Pipelines'
|
||||||
imageName: ${{ variables.linuxImage }}
|
imageName: ${{ variables.linuxImage }}
|
||||||
FreebsdCore:
|
FreebsdCore:
|
||||||
osName: 'Linux'
|
osName: 'Linux'
|
||||||
testName: 'freebsd-x64'
|
testName: 'FreebsdCore'
|
||||||
poolName: 'FreeBSD'
|
poolName: 'FreeBSD'
|
||||||
imageName:
|
imageName:
|
||||||
|
|
||||||
@@ -450,7 +439,7 @@ stages:
|
|||||||
displayName: Download Test Artifact
|
displayName: Download Test Artifact
|
||||||
inputs:
|
inputs:
|
||||||
buildType: 'current'
|
buildType: 'current'
|
||||||
artifactName: '$(testName)-tests'
|
artifactName: '$(testName)Tests'
|
||||||
targetPath: $(testsFolder)
|
targetPath: $(testsFolder)
|
||||||
- powershell: Set-Service SCardSvr -StartupType Manual
|
- powershell: Set-Service SCardSvr -StartupType Manual
|
||||||
displayName: Enable Windows Test Service
|
displayName: Enable Windows Test Service
|
||||||
@@ -480,12 +469,8 @@ stages:
|
|||||||
matrix:
|
matrix:
|
||||||
alpine:
|
alpine:
|
||||||
testName: 'Musl Net Core'
|
testName: 'Musl Net Core'
|
||||||
artifactName: linux-musl-x64-tests
|
artifactName: LinuxMuslCoreTests
|
||||||
containerImage: ghcr.io/servarr/testimages:alpine
|
containerImage: ghcr.io/servarr/testimages:alpine
|
||||||
linux-x86:
|
|
||||||
testName: 'linux-x86'
|
|
||||||
artifactName: linux-x86-tests
|
|
||||||
containerImage: ghcr.io/servarr/testimages:linux-x86
|
|
||||||
|
|
||||||
pool:
|
pool:
|
||||||
vmImage: ${{ variables.linuxImage }}
|
vmImage: ${{ variables.linuxImage }}
|
||||||
@@ -496,15 +481,9 @@ stages:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- task: UseDotNet@2
|
- task: UseDotNet@2
|
||||||
displayName: 'Install .NET'
|
displayName: 'Install .net core'
|
||||||
inputs:
|
inputs:
|
||||||
version: $(dotnetVersion)
|
version: $(dotnetVersion)
|
||||||
condition: and(succeeded(), ne(variables['testName'], 'linux-x86'))
|
|
||||||
- bash: |
|
|
||||||
SDKURL=$(curl -s https://api.github.com/repos/Servarr/dotnet-linux-x86/releases | jq -rc '.[].assets[].browser_download_url' | grep sdk-${DOTNETVERSION}.*gz$)
|
|
||||||
curl -fsSL $SDKURL | tar xzf - -C /opt/dotnet
|
|
||||||
displayName: 'Install .NET'
|
|
||||||
condition: and(succeeded(), eq(variables['testName'], 'linux-x86'))
|
|
||||||
- checkout: none
|
- checkout: none
|
||||||
- task: DownloadPipelineArtifact@2
|
- task: DownloadPipelineArtifact@2
|
||||||
displayName: Download Test Artifact
|
displayName: Download Test Artifact
|
||||||
@@ -527,58 +506,6 @@ stages:
|
|||||||
testResultsFiles: '**/TestResult.xml'
|
testResultsFiles: '**/TestResult.xml'
|
||||||
testRunTitle: '$(testName) Unit Tests'
|
testRunTitle: '$(testName) Unit Tests'
|
||||||
failTaskOnFailedTests: true
|
failTaskOnFailedTests: true
|
||||||
|
|
||||||
- job: Unit_LinuxCore_Postgres
|
|
||||||
displayName: Unit Native LinuxCore with Postgres Database
|
|
||||||
dependsOn: Prepare
|
|
||||||
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
|
|
||||||
variables:
|
|
||||||
pattern: 'Prowlarr.*.linux-core-x64.tar.gz'
|
|
||||||
artifactName: linux-x64-tests
|
|
||||||
Prowlarr__Postgres__Host: 'localhost'
|
|
||||||
Prowlarr__Postgres__Port: '5432'
|
|
||||||
Prowlarr__Postgres__User: 'prowlarr'
|
|
||||||
Prowlarr__Postgres__Password: 'prowlarr'
|
|
||||||
|
|
||||||
pool:
|
|
||||||
vmImage: ${{ variables.linuxImage }}
|
|
||||||
|
|
||||||
timeoutInMinutes: 10
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- task: UseDotNet@2
|
|
||||||
displayName: 'Install .net core'
|
|
||||||
inputs:
|
|
||||||
version: $(dotnetVersion)
|
|
||||||
- checkout: none
|
|
||||||
- task: DownloadPipelineArtifact@2
|
|
||||||
displayName: Download Test Artifact
|
|
||||||
inputs:
|
|
||||||
buildType: 'current'
|
|
||||||
artifactName: $(artifactName)
|
|
||||||
targetPath: $(testsFolder)
|
|
||||||
- bash: find ${TESTSFOLDER} -name "Prowlarr.Test.Dummy" -exec chmod a+x {} \;
|
|
||||||
displayName: Make Test Dummy Executable
|
|
||||||
condition: and(succeeded(), ne(variables['osName'], 'Windows'))
|
|
||||||
- bash: |
|
|
||||||
docker run -d --name=postgres14 \
|
|
||||||
-e POSTGRES_PASSWORD=prowlarr \
|
|
||||||
-e POSTGRES_USER=prowlarr \
|
|
||||||
-p 5432:5432/tcp \
|
|
||||||
postgres:14
|
|
||||||
displayName: Start postgres
|
|
||||||
- bash: |
|
|
||||||
chmod a+x ${TESTSFOLDER}/test.sh
|
|
||||||
ls -lR ${TESTSFOLDER}
|
|
||||||
${TESTSFOLDER}/test.sh Linux Unit Test
|
|
||||||
displayName: Run Tests
|
|
||||||
- task: PublishTestResults@2
|
|
||||||
displayName: Publish Test Results
|
|
||||||
inputs:
|
|
||||||
testResultsFormat: 'NUnit'
|
|
||||||
testResultsFiles: '**/TestResult.xml'
|
|
||||||
testRunTitle: 'LinuxCore Postgres Unit Tests'
|
|
||||||
failTaskOnFailedTests: true
|
|
||||||
|
|
||||||
- stage: Integration
|
- stage: Integration
|
||||||
displayName: Integration
|
displayName: Integration
|
||||||
@@ -606,17 +533,17 @@ stages:
|
|||||||
matrix:
|
matrix:
|
||||||
MacCore:
|
MacCore:
|
||||||
osName: 'Mac'
|
osName: 'Mac'
|
||||||
testName: 'osx-x64'
|
testName: 'MacCore'
|
||||||
imageName: ${{ variables.macImage }}
|
imageName: ${{ variables.macImage }}
|
||||||
pattern: 'Prowlarr.*.osx-core-x64.tar.gz'
|
pattern: 'Prowlarr.*.osx-core-x64.tar.gz'
|
||||||
WindowsCore:
|
WindowsCore:
|
||||||
osName: 'Windows'
|
osName: 'Windows'
|
||||||
testName: 'win-x64'
|
testName: 'WindowsCore'
|
||||||
imageName: ${{ variables.windowsImage }}
|
imageName: ${{ variables.windowsImage }}
|
||||||
pattern: 'Prowlarr.*.windows-core-x64.zip'
|
pattern: 'Prowlarr.*.windows-core-x64.zip'
|
||||||
LinuxCore:
|
LinuxCore:
|
||||||
osName: 'Linux'
|
osName: 'Linux'
|
||||||
testName: 'linux-x64'
|
testName: 'LinuxCore'
|
||||||
imageName: ${{ variables.linuxImage }}
|
imageName: ${{ variables.linuxImage }}
|
||||||
pattern: 'Prowlarr.*.linux-core-x64.tar.gz'
|
pattern: 'Prowlarr.*.linux-core-x64.tar.gz'
|
||||||
|
|
||||||
@@ -633,7 +560,7 @@ stages:
|
|||||||
displayName: Download Test Artifact
|
displayName: Download Test Artifact
|
||||||
inputs:
|
inputs:
|
||||||
buildType: 'current'
|
buildType: 'current'
|
||||||
artifactName: '$(testName)-tests'
|
artifactName: '$(testName)Tests'
|
||||||
targetPath: $(testsFolder)
|
targetPath: $(testsFolder)
|
||||||
- task: DownloadPipelineArtifact@2
|
- task: DownloadPipelineArtifact@2
|
||||||
displayName: Download Build Artifact
|
displayName: Download Build Artifact
|
||||||
@@ -663,67 +590,6 @@ stages:
|
|||||||
failTaskOnFailedTests: true
|
failTaskOnFailedTests: true
|
||||||
displayName: Publish Test Results
|
displayName: Publish Test Results
|
||||||
|
|
||||||
- job: Integration_LinuxCore_Postgres
|
|
||||||
displayName: Integration Native LinuxCore with Postgres Database
|
|
||||||
dependsOn: Prepare
|
|
||||||
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
|
|
||||||
variables:
|
|
||||||
pattern: 'Prowlarr.*.linux-core-x64.tar.gz'
|
|
||||||
Prowlarr__Postgres__Host: 'localhost'
|
|
||||||
Prowlarr__Postgres__Port: '5432'
|
|
||||||
Prowlarr__Postgres__User: 'prowlarr'
|
|
||||||
Prowlarr__Postgres__Password: 'prowlarr'
|
|
||||||
|
|
||||||
pool:
|
|
||||||
vmImage: ${{ variables.linuxImage }}
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- task: UseDotNet@2
|
|
||||||
displayName: 'Install .net core'
|
|
||||||
inputs:
|
|
||||||
version: $(dotnetVersion)
|
|
||||||
- checkout: none
|
|
||||||
- task: DownloadPipelineArtifact@2
|
|
||||||
displayName: Download Test Artifact
|
|
||||||
inputs:
|
|
||||||
buildType: 'current'
|
|
||||||
artifactName: 'linux-x64-tests'
|
|
||||||
targetPath: $(testsFolder)
|
|
||||||
- task: DownloadPipelineArtifact@2
|
|
||||||
displayName: Download Build Artifact
|
|
||||||
inputs:
|
|
||||||
buildType: 'current'
|
|
||||||
artifactName: Packages
|
|
||||||
itemPattern: '**/$(pattern)'
|
|
||||||
targetPath: $(Build.ArtifactStagingDirectory)
|
|
||||||
- task: ExtractFiles@1
|
|
||||||
inputs:
|
|
||||||
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
|
|
||||||
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
|
|
||||||
displayName: Extract Package
|
|
||||||
- bash: |
|
|
||||||
mkdir -p ./bin/
|
|
||||||
cp -r -v ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin/Prowlarr/. ./bin/
|
|
||||||
displayName: Move Package Contents
|
|
||||||
- bash: |
|
|
||||||
docker run -d --name=postgres14 \
|
|
||||||
-e POSTGRES_PASSWORD=prowlarr \
|
|
||||||
-e POSTGRES_USER=prowlarr \
|
|
||||||
-p 5432:5432/tcp \
|
|
||||||
postgres:14
|
|
||||||
displayName: Start postgres
|
|
||||||
- bash: |
|
|
||||||
chmod a+x ${TESTSFOLDER}/test.sh
|
|
||||||
${TESTSFOLDER}/test.sh Linux Integration Test
|
|
||||||
displayName: Run Integration Tests
|
|
||||||
- task: PublishTestResults@2
|
|
||||||
inputs:
|
|
||||||
testResultsFormat: 'NUnit'
|
|
||||||
testResultsFiles: '**/TestResult.xml'
|
|
||||||
testRunTitle: 'Integration LinuxCore Postgres Database Integration Tests'
|
|
||||||
failTaskOnFailedTests: true
|
|
||||||
displayName: Publish Test Results
|
|
||||||
|
|
||||||
- job: Integration_FreeBSD
|
- job: Integration_FreeBSD
|
||||||
displayName: Integration Native FreeBSD
|
displayName: Integration Native FreeBSD
|
||||||
dependsOn: Prepare
|
dependsOn: Prepare
|
||||||
@@ -741,14 +607,14 @@ stages:
|
|||||||
displayName: Download Test Artifact
|
displayName: Download Test Artifact
|
||||||
inputs:
|
inputs:
|
||||||
buildType: 'current'
|
buildType: 'current'
|
||||||
artifactName: 'freebsd-x64-tests'
|
artifactName: 'FreebsdCoreTests'
|
||||||
targetPath: $(testsFolder)
|
targetPath: $(testsFolder)
|
||||||
- task: DownloadPipelineArtifact@2
|
- task: DownloadPipelineArtifact@2
|
||||||
displayName: Download Build Artifact
|
displayName: Download Build Artifact
|
||||||
inputs:
|
inputs:
|
||||||
buildType: 'current'
|
buildType: 'current'
|
||||||
artifactName: Packages
|
artifactName: Packages
|
||||||
itemPattern: '**/$(pattern)'
|
itemPattern: '/$(pattern)'
|
||||||
targetPath: $(Build.ArtifactStagingDirectory)
|
targetPath: $(Build.ArtifactStagingDirectory)
|
||||||
- bash: |
|
- bash: |
|
||||||
mkdir -p ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin
|
mkdir -p ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin
|
||||||
@@ -777,15 +643,11 @@ stages:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
alpine:
|
alpine:
|
||||||
testName: 'linux-musl-x64'
|
testName: 'Musl Net Core'
|
||||||
artifactName: linux-musl-x64-tests
|
artifactName: LinuxMuslCoreTests
|
||||||
containerImage: ghcr.io/servarr/testimages:alpine
|
containerImage: ghcr.io/servarr/testimages:alpine
|
||||||
pattern: 'Prowlarr.*.linux-musl-core-x64.tar.gz'
|
pattern: 'Prowlarr.*.linux-musl-core-x64.tar.gz'
|
||||||
linux-x86:
|
|
||||||
testName: 'linux-x86'
|
|
||||||
artifactName: linux-x86-tests
|
|
||||||
containerImage: ghcr.io/servarr/testimages:linux-x86
|
|
||||||
pattern: 'Prowlarr.*.linux-core-x86.tar.gz'
|
|
||||||
pool:
|
pool:
|
||||||
vmImage: ${{ variables.linuxImage }}
|
vmImage: ${{ variables.linuxImage }}
|
||||||
|
|
||||||
@@ -795,15 +657,9 @@ stages:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- task: UseDotNet@2
|
- task: UseDotNet@2
|
||||||
displayName: 'Install .NET'
|
displayName: 'Install .net core'
|
||||||
inputs:
|
inputs:
|
||||||
version: $(dotnetVersion)
|
version: $(dotnetVersion)
|
||||||
condition: and(succeeded(), ne(variables['testName'], 'linux-x86'))
|
|
||||||
- bash: |
|
|
||||||
SDKURL=$(curl -s https://api.github.com/repos/Servarr/dotnet-linux-x86/releases | jq -rc '.[].assets[].browser_download_url' | grep sdk-${DOTNETVERSION}.*gz$)
|
|
||||||
curl -fsSL $SDKURL | tar xzf - -C /opt/dotnet
|
|
||||||
displayName: 'Install .NET'
|
|
||||||
condition: and(succeeded(), eq(variables['testName'], 'linux-x86'))
|
|
||||||
- checkout: none
|
- checkout: none
|
||||||
- task: DownloadPipelineArtifact@2
|
- task: DownloadPipelineArtifact@2
|
||||||
displayName: Download Test Artifact
|
displayName: Download Test Artifact
|
||||||
@@ -849,19 +705,16 @@ stages:
|
|||||||
matrix:
|
matrix:
|
||||||
Linux:
|
Linux:
|
||||||
osName: 'Linux'
|
osName: 'Linux'
|
||||||
artifactName: 'linux-x64'
|
|
||||||
imageName: ${{ variables.linuxImage }}
|
imageName: ${{ variables.linuxImage }}
|
||||||
pattern: 'Prowlarr.*.linux-core-x64.tar.gz'
|
pattern: 'Prowlarr.*.linux-core-x64.tar.gz'
|
||||||
failBuild: true
|
failBuild: true
|
||||||
Mac:
|
Mac:
|
||||||
osName: 'Mac'
|
osName: 'Mac'
|
||||||
artifactName: 'osx-x64'
|
|
||||||
imageName: ${{ variables.macImage }}
|
imageName: ${{ variables.macImage }}
|
||||||
pattern: 'Prowlarr.*.osx-core-x64.tar.gz'
|
pattern: 'Prowlarr.*.osx-core-x64.tar.gz'
|
||||||
failBuild: true
|
failBuild: true
|
||||||
Windows:
|
Windows:
|
||||||
osName: 'Windows'
|
osName: 'Windows'
|
||||||
artifactName: 'win-x64'
|
|
||||||
imageName: ${{ variables.windowsImage }}
|
imageName: ${{ variables.windowsImage }}
|
||||||
pattern: 'Prowlarr.*.windows-core-x64.zip'
|
pattern: 'Prowlarr.*.windows-core-x64.zip'
|
||||||
failBuild: true
|
failBuild: true
|
||||||
@@ -879,7 +732,7 @@ stages:
|
|||||||
displayName: Download Test Artifact
|
displayName: Download Test Artifact
|
||||||
inputs:
|
inputs:
|
||||||
buildType: 'current'
|
buildType: 'current'
|
||||||
artifactName: '$(artifactName)-tests'
|
artifactName: '$(osName)CoreTests'
|
||||||
targetPath: $(testsFolder)
|
targetPath: $(testsFolder)
|
||||||
- task: DownloadPipelineArtifact@2
|
- task: DownloadPipelineArtifact@2
|
||||||
displayName: Download Build Artifact
|
displayName: Download Build Artifact
|
||||||
@@ -1108,5 +961,4 @@ stages:
|
|||||||
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
|
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
|
||||||
DISCORDCHANNELID: $(discordChannelId)
|
DISCORDCHANNELID: $(discordChannelId)
|
||||||
DISCORDWEBHOOKKEY: $(discordWebhookKey)
|
DISCORDWEBHOOKKEY: $(discordWebhookKey)
|
||||||
DISCORDTHREADID: $(discordThreadId)
|
|
||||||
|
|
||||||
|
|||||||
45
build.sh
45
build.sh
@@ -25,22 +25,15 @@ UpdateVersionNumber()
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
EnableExtraPlatformsInSDK()
|
EnableBsdSupport()
|
||||||
{
|
{
|
||||||
SDK_PATH=$(dotnet --list-sdks | grep -P '6\.\d\.\d+' | head -1 | sed 's/\(6\.[0-9]*\.[0-9]*\).*\[\(.*\)\]/\2\/\1/g')
|
#todo enable sdk with
|
||||||
BUNDLEDVERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props"
|
#SDK_PATH=$(dotnet --list-sdks | grep -P '5\.\d\.\d+' | head -1 | sed 's/\(5\.[0-9]*\.[0-9]*\).*\[\(.*\)\]/\2\/\1/g')
|
||||||
if grep -q freebsd-x64 $BUNDLEDVERSIONS; then
|
# BUNDLED_VERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props"
|
||||||
echo "Extra platforms already enabled"
|
|
||||||
else
|
|
||||||
echo "Enabling extra platform support"
|
|
||||||
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
EnableExtraPlatforms()
|
|
||||||
{
|
|
||||||
if grep -qv freebsd-x64 src/Directory.Build.props; then
|
if grep -qv freebsd-x64 src/Directory.Build.props; then
|
||||||
sed -i'' -e "s^<RuntimeIdentifiers>\(.*\)</RuntimeIdentifiers>^<RuntimeIdentifiers>\1;freebsd-x64;linux-x86</RuntimeIdentifiers>^g" src/Directory.Build.props
|
sed -i'' -e "s^<RuntimeIdentifiers>\(.*\)</RuntimeIdentifiers>^<RuntimeIdentifiers>\1;freebsd-x64</RuntimeIdentifiers>^g" src/Directory.Build.props
|
||||||
|
sed -i'' -e "s^<ExcludedRuntimeFrameworkPairs>\(.*\)</ExcludedRuntimeFrameworkPairs>^<ExcludedRuntimeFrameworkPairs>\1;freebsd-x64:net472</ExcludedRuntimeFrameworkPairs>^g" src/Directory.Build.props
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,8 +293,7 @@ if [ $# -eq 0 ]; then
|
|||||||
PACKAGES=YES
|
PACKAGES=YES
|
||||||
INSTALLER=NO
|
INSTALLER=NO
|
||||||
LINT=YES
|
LINT=YES
|
||||||
ENABLE_EXTRA_PLATFORMS=NO
|
ENABLE_BSD=NO
|
||||||
ENABLE_EXTRA_PLATFORMS_IN_SDK=NO
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]
|
while [[ $# -gt 0 ]]
|
||||||
@@ -313,12 +305,8 @@ case $key in
|
|||||||
BACKEND=YES
|
BACKEND=YES
|
||||||
shift # past argument
|
shift # past argument
|
||||||
;;
|
;;
|
||||||
--enable-bsd|--enable-extra-platforms)
|
--enable-bsd)
|
||||||
ENABLE_EXTRA_PLATFORMS=YES
|
ENABLE_BSD=YES
|
||||||
shift # past argument
|
|
||||||
;;
|
|
||||||
--enable-extra-platforms-in-sdk)
|
|
||||||
ENABLE_EXTRA_PLATFORMS_IN_SDK=YES
|
|
||||||
shift # past argument
|
shift # past argument
|
||||||
;;
|
;;
|
||||||
-r|--runtime)
|
-r|--runtime)
|
||||||
@@ -362,17 +350,12 @@ esac
|
|||||||
done
|
done
|
||||||
set -- "${POSITIONAL[@]}" # restore positional parameters
|
set -- "${POSITIONAL[@]}" # restore positional parameters
|
||||||
|
|
||||||
if [ "$ENABLE_EXTRA_PLATFORMS_IN_SDK" = "YES" ];
|
|
||||||
then
|
|
||||||
EnableExtraPlatformsInSDK
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$BACKEND" = "YES" ];
|
if [ "$BACKEND" = "YES" ];
|
||||||
then
|
then
|
||||||
UpdateVersionNumber
|
UpdateVersionNumber
|
||||||
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
|
if [ "$ENABLE_BSD" = "YES" ];
|
||||||
then
|
then
|
||||||
EnableExtraPlatforms
|
EnableBsdSupport
|
||||||
fi
|
fi
|
||||||
Build
|
Build
|
||||||
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
|
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
|
||||||
@@ -382,10 +365,9 @@ then
|
|||||||
PackageTests "net6.0" "linux-x64"
|
PackageTests "net6.0" "linux-x64"
|
||||||
PackageTests "net6.0" "linux-musl-x64"
|
PackageTests "net6.0" "linux-musl-x64"
|
||||||
PackageTests "net6.0" "osx-x64"
|
PackageTests "net6.0" "osx-x64"
|
||||||
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
|
if [ "$ENABLE_BSD" = "YES" ];
|
||||||
then
|
then
|
||||||
PackageTests "net6.0" "freebsd-x64"
|
PackageTests "net6.0" "freebsd-x64"
|
||||||
PackageTests "net6.0" "linux-x86"
|
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
PackageTests "$FRAMEWORK" "$RID"
|
PackageTests "$FRAMEWORK" "$RID"
|
||||||
@@ -424,10 +406,9 @@ then
|
|||||||
Package "net6.0" "linux-musl-arm"
|
Package "net6.0" "linux-musl-arm"
|
||||||
Package "net6.0" "osx-x64"
|
Package "net6.0" "osx-x64"
|
||||||
Package "net6.0" "osx-arm64"
|
Package "net6.0" "osx-arm64"
|
||||||
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
|
if [ "$ENABLE_BSD" = "YES" ];
|
||||||
then
|
then
|
||||||
Package "net6.0" "freebsd-x64"
|
Package "net6.0" "freebsd-x64"
|
||||||
Package "net6.0" "linux-x86"
|
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
Package "$FRAMEWORK" "$RID"
|
Package "$FRAMEWORK" "$RID"
|
||||||
|
|||||||
@@ -78,7 +78,7 @@
|
|||||||
|
|
||||||
border: 1px solid var(--inputBorderColor);
|
border: 1px solid var(--inputBorderColor);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background-color: var(--inputBackgroundColor);
|
background-color: var(--white);
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading {
|
.loading {
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import FormInputHelpText from './FormInputHelpText';
|
|||||||
import IndexerFlagsSelectInputConnector from './IndexerFlagsSelectInputConnector';
|
import IndexerFlagsSelectInputConnector from './IndexerFlagsSelectInputConnector';
|
||||||
import InfoInput from './InfoInput';
|
import InfoInput from './InfoInput';
|
||||||
import KeyValueListInput from './KeyValueListInput';
|
import KeyValueListInput from './KeyValueListInput';
|
||||||
import NewznabCategorySelectInputConnector from './NewznabCategorySelectInputConnector';
|
|
||||||
import NumberInput from './NumberInput';
|
import NumberInput from './NumberInput';
|
||||||
import OAuthInputConnector from './OAuthInputConnector';
|
import OAuthInputConnector from './OAuthInputConnector';
|
||||||
import PasswordInput from './PasswordInput';
|
import PasswordInput from './PasswordInput';
|
||||||
@@ -69,9 +68,6 @@ function getComponent(type) {
|
|||||||
case inputTypes.PATH:
|
case inputTypes.PATH:
|
||||||
return PathInputConnector;
|
return PathInputConnector;
|
||||||
|
|
||||||
case inputTypes.CATEGORY_SELECT:
|
|
||||||
return NewznabCategorySelectInputConnector;
|
|
||||||
|
|
||||||
case inputTypes.INDEXER_FLAGS_SELECT:
|
case inputTypes.INDEXER_FLAGS_SELECT:
|
||||||
return IndexerFlagsSelectInputConnector;
|
return IndexerFlagsSelectInputConnector;
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ function createMapStateToProps() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
value: value || [],
|
value,
|
||||||
values
|
values
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ class TagInputInput extends Component {
|
|||||||
<div
|
<div
|
||||||
ref={forwardedRef}
|
ref={forwardedRef}
|
||||||
className={className}
|
className={className}
|
||||||
|
component="div"
|
||||||
onMouseDown={this.onMouseDown}
|
onMouseDown={this.onMouseDown}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -36,5 +36,5 @@
|
|||||||
/** Outline **/
|
/** Outline **/
|
||||||
|
|
||||||
.outline {
|
.outline {
|
||||||
background-color: var(--cardBackgroundColor);
|
background-color: var(--white);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,5 +108,5 @@
|
|||||||
/** Outline **/
|
/** Outline **/
|
||||||
|
|
||||||
.outline {
|
.outline {
|
||||||
background-color: var(--cardBackgroundColor);
|
background-color: var(--white);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ class PageHeader extends Component {
|
|||||||
<IconButton
|
<IconButton
|
||||||
className={styles.donate}
|
className={styles.donate}
|
||||||
name={icons.HEART}
|
name={icons.HEART}
|
||||||
to="https://prowlarr.com/donate"
|
to="https://opencollective.com/prowlarr"
|
||||||
size={14}
|
size={14}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #515253;
|
color: var(--toobarButtonHoverColor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import AppUpdatedModalConnector from 'App/AppUpdatedModalConnector';
|
|||||||
import ColorImpairedContext from 'App/ColorImpairedContext';
|
import ColorImpairedContext from 'App/ColorImpairedContext';
|
||||||
import ConnectionLostModalConnector from 'App/ConnectionLostModalConnector';
|
import ConnectionLostModalConnector from 'App/ConnectionLostModalConnector';
|
||||||
import SignalRConnector from 'Components/SignalRConnector';
|
import SignalRConnector from 'Components/SignalRConnector';
|
||||||
import AuthenticationRequiredModal from 'FirstRun/AuthenticationRequiredModal';
|
|
||||||
import locationShape from 'Helpers/Props/Shapes/locationShape';
|
import locationShape from 'Helpers/Props/Shapes/locationShape';
|
||||||
import PageHeader from './Header/PageHeader';
|
import PageHeader from './Header/PageHeader';
|
||||||
import PageSidebar from './Sidebar/PageSidebar';
|
import PageSidebar from './Sidebar/PageSidebar';
|
||||||
@@ -76,7 +75,6 @@ class Page extends Component {
|
|||||||
isSmallScreen,
|
isSmallScreen,
|
||||||
isSidebarVisible,
|
isSidebarVisible,
|
||||||
enableColorImpairedMode,
|
enableColorImpairedMode,
|
||||||
authenticationEnabled,
|
|
||||||
onSidebarToggle,
|
onSidebarToggle,
|
||||||
onSidebarVisibleChange
|
onSidebarVisibleChange
|
||||||
} = this.props;
|
} = this.props;
|
||||||
@@ -111,10 +109,6 @@ class Page extends Component {
|
|||||||
isOpen={this.state.isConnectionLostModalOpen}
|
isOpen={this.state.isConnectionLostModalOpen}
|
||||||
onModalClose={this.onConnectionLostModalClose}
|
onModalClose={this.onConnectionLostModalClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AuthenticationRequiredModal
|
|
||||||
isOpen={!authenticationEnabled}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</ColorImpairedContext.Provider>
|
</ColorImpairedContext.Provider>
|
||||||
);
|
);
|
||||||
@@ -130,7 +124,6 @@ Page.propTypes = {
|
|||||||
isUpdated: PropTypes.bool.isRequired,
|
isUpdated: PropTypes.bool.isRequired,
|
||||||
isDisconnected: PropTypes.bool.isRequired,
|
isDisconnected: PropTypes.bool.isRequired,
|
||||||
enableColorImpairedMode: PropTypes.bool.isRequired,
|
enableColorImpairedMode: PropTypes.bool.isRequired,
|
||||||
authenticationEnabled: PropTypes.bool.isRequired,
|
|
||||||
onResize: PropTypes.func.isRequired,
|
onResize: PropTypes.func.isRequired,
|
||||||
onSidebarToggle: PropTypes.func.isRequired,
|
onSidebarToggle: PropTypes.func.isRequired,
|
||||||
onSidebarVisibleChange: PropTypes.func.isRequired
|
onSidebarVisibleChange: PropTypes.func.isRequired
|
||||||
|
|||||||
@@ -7,11 +7,10 @@ import { saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
|
|||||||
import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
|
import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
|
||||||
import { fetchIndexers } from 'Store/Actions/indexerActions';
|
import { fetchIndexers } from 'Store/Actions/indexerActions';
|
||||||
import { fetchIndexerStatus } from 'Store/Actions/indexerStatusActions';
|
import { fetchIndexerStatus } from 'Store/Actions/indexerStatusActions';
|
||||||
import { fetchAppProfiles, fetchGeneralSettings, fetchIndexerCategories, fetchUISettings } from 'Store/Actions/settingsActions';
|
import { fetchAppProfiles, fetchGeneralSettings, fetchIndexerCategories, fetchLanguages, fetchUISettings } from 'Store/Actions/settingsActions';
|
||||||
import { fetchStatus } from 'Store/Actions/systemActions';
|
import { fetchStatus } from 'Store/Actions/systemActions';
|
||||||
import { fetchTags } from 'Store/Actions/tagActions';
|
import { fetchTags } from 'Store/Actions/tagActions';
|
||||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
|
||||||
import ErrorPage from './ErrorPage';
|
import ErrorPage from './ErrorPage';
|
||||||
import LoadingPage from './LoadingPage';
|
import LoadingPage from './LoadingPage';
|
||||||
import Page from './Page';
|
import Page from './Page';
|
||||||
@@ -49,6 +48,7 @@ const selectIsPopulated = createSelector(
|
|||||||
(state) => state.tags.isPopulated,
|
(state) => state.tags.isPopulated,
|
||||||
(state) => state.settings.ui.isPopulated,
|
(state) => state.settings.ui.isPopulated,
|
||||||
(state) => state.settings.general.isPopulated,
|
(state) => state.settings.general.isPopulated,
|
||||||
|
(state) => state.settings.languages.isPopulated,
|
||||||
(state) => state.settings.appProfiles.isPopulated,
|
(state) => state.settings.appProfiles.isPopulated,
|
||||||
(state) => state.indexers.isPopulated,
|
(state) => state.indexers.isPopulated,
|
||||||
(state) => state.indexerStatus.isPopulated,
|
(state) => state.indexerStatus.isPopulated,
|
||||||
@@ -59,6 +59,7 @@ const selectIsPopulated = createSelector(
|
|||||||
tagsIsPopulated,
|
tagsIsPopulated,
|
||||||
uiSettingsIsPopulated,
|
uiSettingsIsPopulated,
|
||||||
generalSettingsIsPopulated,
|
generalSettingsIsPopulated,
|
||||||
|
languagesIsPopulated,
|
||||||
appProfilesIsPopulated,
|
appProfilesIsPopulated,
|
||||||
indexersIsPopulated,
|
indexersIsPopulated,
|
||||||
indexerStatusIsPopulated,
|
indexerStatusIsPopulated,
|
||||||
@@ -70,6 +71,7 @@ const selectIsPopulated = createSelector(
|
|||||||
tagsIsPopulated &&
|
tagsIsPopulated &&
|
||||||
uiSettingsIsPopulated &&
|
uiSettingsIsPopulated &&
|
||||||
generalSettingsIsPopulated &&
|
generalSettingsIsPopulated &&
|
||||||
|
languagesIsPopulated &&
|
||||||
appProfilesIsPopulated &&
|
appProfilesIsPopulated &&
|
||||||
indexersIsPopulated &&
|
indexersIsPopulated &&
|
||||||
indexerStatusIsPopulated &&
|
indexerStatusIsPopulated &&
|
||||||
@@ -84,6 +86,7 @@ const selectErrors = createSelector(
|
|||||||
(state) => state.tags.error,
|
(state) => state.tags.error,
|
||||||
(state) => state.settings.ui.error,
|
(state) => state.settings.ui.error,
|
||||||
(state) => state.settings.general.error,
|
(state) => state.settings.general.error,
|
||||||
|
(state) => state.settings.languages.error,
|
||||||
(state) => state.settings.appProfiles.error,
|
(state) => state.settings.appProfiles.error,
|
||||||
(state) => state.indexers.error,
|
(state) => state.indexers.error,
|
||||||
(state) => state.indexerStatus.error,
|
(state) => state.indexerStatus.error,
|
||||||
@@ -94,6 +97,7 @@ const selectErrors = createSelector(
|
|||||||
tagsError,
|
tagsError,
|
||||||
uiSettingsError,
|
uiSettingsError,
|
||||||
generalSettingsError,
|
generalSettingsError,
|
||||||
|
languagesError,
|
||||||
appProfilesError,
|
appProfilesError,
|
||||||
indexersError,
|
indexersError,
|
||||||
indexerStatusError,
|
indexerStatusError,
|
||||||
@@ -105,6 +109,7 @@ const selectErrors = createSelector(
|
|||||||
tagsError ||
|
tagsError ||
|
||||||
uiSettingsError ||
|
uiSettingsError ||
|
||||||
generalSettingsError ||
|
generalSettingsError ||
|
||||||
|
languagesError ||
|
||||||
appProfilesError ||
|
appProfilesError ||
|
||||||
indexersError ||
|
indexersError ||
|
||||||
indexerStatusError ||
|
indexerStatusError ||
|
||||||
@@ -118,6 +123,7 @@ const selectErrors = createSelector(
|
|||||||
tagsError,
|
tagsError,
|
||||||
uiSettingsError,
|
uiSettingsError,
|
||||||
generalSettingsError,
|
generalSettingsError,
|
||||||
|
languagesError,
|
||||||
appProfilesError,
|
appProfilesError,
|
||||||
indexersError,
|
indexersError,
|
||||||
indexerStatusError,
|
indexerStatusError,
|
||||||
@@ -134,21 +140,18 @@ function createMapStateToProps() {
|
|||||||
selectErrors,
|
selectErrors,
|
||||||
selectAppProps,
|
selectAppProps,
|
||||||
createDimensionsSelector(),
|
createDimensionsSelector(),
|
||||||
createSystemStatusSelector(),
|
|
||||||
(
|
(
|
||||||
enableColorImpairedMode,
|
enableColorImpairedMode,
|
||||||
isPopulated,
|
isPopulated,
|
||||||
errors,
|
errors,
|
||||||
app,
|
app,
|
||||||
dimensions,
|
dimensions
|
||||||
systemStatus
|
|
||||||
) => {
|
) => {
|
||||||
return {
|
return {
|
||||||
...app,
|
...app,
|
||||||
...errors,
|
...errors,
|
||||||
isPopulated,
|
isPopulated,
|
||||||
isSmallScreen: dimensions.isSmallScreen,
|
isSmallScreen: dimensions.isSmallScreen,
|
||||||
authenticationEnabled: systemStatus.authentication !== 'none',
|
|
||||||
enableColorImpairedMode
|
enableColorImpairedMode
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -163,6 +166,9 @@ function createMapDispatchToProps(dispatch, props) {
|
|||||||
dispatchFetchTags() {
|
dispatchFetchTags() {
|
||||||
dispatch(fetchTags());
|
dispatch(fetchTags());
|
||||||
},
|
},
|
||||||
|
dispatchFetchLanguages() {
|
||||||
|
dispatch(fetchLanguages());
|
||||||
|
},
|
||||||
dispatchFetchIndexers() {
|
dispatchFetchIndexers() {
|
||||||
dispatch(fetchIndexers());
|
dispatch(fetchIndexers());
|
||||||
},
|
},
|
||||||
@@ -210,6 +216,7 @@ class PageConnector extends Component {
|
|||||||
if (!this.props.isPopulated) {
|
if (!this.props.isPopulated) {
|
||||||
this.props.dispatchFetchCustomFilters();
|
this.props.dispatchFetchCustomFilters();
|
||||||
this.props.dispatchFetchTags();
|
this.props.dispatchFetchTags();
|
||||||
|
this.props.dispatchFetchLanguages();
|
||||||
this.props.dispatchFetchAppProfiles();
|
this.props.dispatchFetchAppProfiles();
|
||||||
this.props.dispatchFetchIndexers();
|
this.props.dispatchFetchIndexers();
|
||||||
this.props.dispatchFetchIndexerStatus();
|
this.props.dispatchFetchIndexerStatus();
|
||||||
@@ -235,6 +242,7 @@ class PageConnector extends Component {
|
|||||||
isPopulated,
|
isPopulated,
|
||||||
hasError,
|
hasError,
|
||||||
dispatchFetchTags,
|
dispatchFetchTags,
|
||||||
|
dispatchFetchLanguages,
|
||||||
dispatchFetchAppProfiles,
|
dispatchFetchAppProfiles,
|
||||||
dispatchFetchIndexers,
|
dispatchFetchIndexers,
|
||||||
dispatchFetchIndexerStatus,
|
dispatchFetchIndexerStatus,
|
||||||
@@ -275,6 +283,7 @@ PageConnector.propTypes = {
|
|||||||
isSidebarVisible: PropTypes.bool.isRequired,
|
isSidebarVisible: PropTypes.bool.isRequired,
|
||||||
dispatchFetchCustomFilters: PropTypes.func.isRequired,
|
dispatchFetchCustomFilters: PropTypes.func.isRequired,
|
||||||
dispatchFetchTags: PropTypes.func.isRequired,
|
dispatchFetchTags: PropTypes.func.isRequired,
|
||||||
|
dispatchFetchLanguages: PropTypes.func.isRequired,
|
||||||
dispatchFetchAppProfiles: PropTypes.func.isRequired,
|
dispatchFetchAppProfiles: PropTypes.func.isRequired,
|
||||||
dispatchFetchIndexers: PropTypes.func.isRequired,
|
dispatchFetchIndexers: PropTypes.func.isRequired,
|
||||||
dispatchFetchIndexerStatus: PropTypes.func.isRequired,
|
dispatchFetchIndexerStatus: PropTypes.func.isRequired,
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ const SIDEBAR_WIDTH = parseInt(dimensions.sidebarWidth);
|
|||||||
const links = [
|
const links = [
|
||||||
{
|
{
|
||||||
iconName: icons.MOVIE_CONTINUING,
|
iconName: icons.MOVIE_CONTINUING,
|
||||||
title: translate('Indexers'),
|
title: 'Indexers',
|
||||||
to: '/',
|
to: '/',
|
||||||
alias: '/indexers',
|
alias: '/movies',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
title: translate('Stats'),
|
title: translate('Stats'),
|
||||||
@@ -33,13 +33,13 @@ const links = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
iconName: icons.SEARCH,
|
iconName: icons.SEARCH,
|
||||||
title: translate('Search'),
|
title: 'Search',
|
||||||
to: '/search'
|
to: '/search'
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
iconName: icons.ACTIVITY,
|
iconName: icons.ACTIVITY,
|
||||||
title: translate('History'),
|
title: 'History',
|
||||||
to: '/history'
|
to: '/history'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&.default {
|
&.default {
|
||||||
background-color: var(--popoverBodyBackgroundColor);
|
background-color: var(--white);
|
||||||
box-shadow: 0 5px 10px var(--popoverShadowColor);
|
box-shadow: 0 5px 10px var(--popoverShadowColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Modal from 'Components/Modal/Modal';
|
|
||||||
import { sizes } from 'Helpers/Props';
|
|
||||||
import AuthenticationRequiredModalContentConnector from './AuthenticationRequiredModalContentConnector';
|
|
||||||
|
|
||||||
function onModalClose() {
|
|
||||||
// No-op
|
|
||||||
}
|
|
||||||
|
|
||||||
function AuthenticationRequiredModal(props) {
|
|
||||||
const {
|
|
||||||
isOpen
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
size={sizes.MEDIUM}
|
|
||||||
isOpen={isOpen}
|
|
||||||
closeOnBackgroundClick={false}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
>
|
|
||||||
<AuthenticationRequiredModalContentConnector
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
AuthenticationRequiredModal.propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AuthenticationRequiredModal;
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
.authRequiredAlert {
|
|
||||||
composes: alert from '~Components/Alert.css';
|
|
||||||
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { useEffect, useRef } from 'react';
|
|
||||||
import Alert from 'Components/Alert';
|
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
|
||||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
|
||||||
import { inputTypes, kinds } from 'Helpers/Props';
|
|
||||||
import { authenticationMethodOptions, authenticationRequiredOptions, authenticationRequiredWarning } from 'Settings/General/SecuritySettings';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import styles from './AuthenticationRequiredModalContent.css';
|
|
||||||
|
|
||||||
function onModalClose() {
|
|
||||||
// No-op
|
|
||||||
}
|
|
||||||
|
|
||||||
function AuthenticationRequiredModalContent(props) {
|
|
||||||
const {
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
isSaving,
|
|
||||||
settings,
|
|
||||||
onInputChange,
|
|
||||||
onSavePress,
|
|
||||||
dispatchFetchStatus
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
authenticationMethod,
|
|
||||||
authenticationRequired,
|
|
||||||
username,
|
|
||||||
password
|
|
||||||
} = settings;
|
|
||||||
|
|
||||||
const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none';
|
|
||||||
|
|
||||||
const didMount = useRef(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isSaving && didMount.current) {
|
|
||||||
dispatchFetchStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
didMount.current = true;
|
|
||||||
}, [isSaving, dispatchFetchStatus]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalContent
|
|
||||||
showCloseButton={false}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
>
|
|
||||||
<ModalHeader>
|
|
||||||
{translate('AuthenticationRequired')}
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody>
|
|
||||||
<Alert
|
|
||||||
className={styles.authRequiredAlert}
|
|
||||||
kind={kinds.WARNING}
|
|
||||||
>
|
|
||||||
{authenticationRequiredWarning}
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
{
|
|
||||||
isPopulated && !error ?
|
|
||||||
<div>
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('Authentication')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.SELECT}
|
|
||||||
name="authenticationMethod"
|
|
||||||
values={authenticationMethodOptions}
|
|
||||||
helpText={translate('AuthenticationMethodHelpText')}
|
|
||||||
onChange={onInputChange}
|
|
||||||
{...authenticationMethod}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
{
|
|
||||||
authenticationEnabled ?
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('AuthenticationRequired')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.SELECT}
|
|
||||||
name="authenticationRequired"
|
|
||||||
values={authenticationRequiredOptions}
|
|
||||||
helpText={translate('AuthenticationRequiredHelpText')}
|
|
||||||
onChange={onInputChange}
|
|
||||||
{...authenticationRequired}
|
|
||||||
/>
|
|
||||||
</FormGroup> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
authenticationEnabled ?
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('Username')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.TEXT}
|
|
||||||
name="username"
|
|
||||||
onChange={onInputChange}
|
|
||||||
{...username}
|
|
||||||
/>
|
|
||||||
</FormGroup> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
authenticationEnabled ?
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('Password')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.PASSWORD}
|
|
||||||
name="password"
|
|
||||||
onChange={onInputChange}
|
|
||||||
{...password}
|
|
||||||
/>
|
|
||||||
</FormGroup> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</div> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!isPopulated && !error ? <LoadingIndicator /> : null
|
|
||||||
}
|
|
||||||
</ModalBody>
|
|
||||||
|
|
||||||
<ModalFooter>
|
|
||||||
<SpinnerButton
|
|
||||||
kind={kinds.PRIMARY}
|
|
||||||
isSpinning={isSaving}
|
|
||||||
isDisabled={!authenticationEnabled}
|
|
||||||
onPress={onSavePress}
|
|
||||||
>
|
|
||||||
{translate('Save')}
|
|
||||||
</SpinnerButton>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
AuthenticationRequiredModalContent.propTypes = {
|
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
|
||||||
error: PropTypes.object,
|
|
||||||
isSaving: PropTypes.bool.isRequired,
|
|
||||||
saveError: PropTypes.object,
|
|
||||||
settings: PropTypes.object.isRequired,
|
|
||||||
onInputChange: PropTypes.func.isRequired,
|
|
||||||
onSavePress: PropTypes.func.isRequired,
|
|
||||||
dispatchFetchStatus: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AuthenticationRequiredModalContent;
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
|
||||||
import { fetchGeneralSettings, saveGeneralSettings, setGeneralSettingsValue } from 'Store/Actions/settingsActions';
|
|
||||||
import { fetchStatus } from 'Store/Actions/systemActions';
|
|
||||||
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
|
|
||||||
import AuthenticationRequiredModalContent from './AuthenticationRequiredModalContent';
|
|
||||||
|
|
||||||
const SECTION = 'general';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
createSettingsSectionSelector(SECTION),
|
|
||||||
(sectionSettings) => {
|
|
||||||
return {
|
|
||||||
...sectionSettings
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
dispatchClearPendingChanges: clearPendingChanges,
|
|
||||||
dispatchSetGeneralSettingsValue: setGeneralSettingsValue,
|
|
||||||
dispatchSaveGeneralSettings: saveGeneralSettings,
|
|
||||||
dispatchFetchGeneralSettings: fetchGeneralSettings,
|
|
||||||
dispatchFetchStatus: fetchStatus
|
|
||||||
};
|
|
||||||
|
|
||||||
class AuthenticationRequiredModalContentConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.props.dispatchFetchGeneralSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.props.dispatchClearPendingChanges({ section: `settings.${SECTION}` });
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onInputChange = ({ name, value }) => {
|
|
||||||
this.props.dispatchSetGeneralSettingsValue({ name, value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onSavePress = () => {
|
|
||||||
this.props.dispatchSaveGeneralSettings();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
dispatchClearPendingChanges,
|
|
||||||
dispatchFetchGeneralSettings,
|
|
||||||
dispatchSetGeneralSettingsValue,
|
|
||||||
dispatchSaveGeneralSettings,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AuthenticationRequiredModalContent
|
|
||||||
{...otherProps}
|
|
||||||
onInputChange={this.onInputChange}
|
|
||||||
onSavePress={this.onSavePress}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AuthenticationRequiredModalContentConnector.propTypes = {
|
|
||||||
dispatchClearPendingChanges: PropTypes.func.isRequired,
|
|
||||||
dispatchFetchGeneralSettings: PropTypes.func.isRequired,
|
|
||||||
dispatchSetGeneralSettingsValue: PropTypes.func.isRequired,
|
|
||||||
dispatchSaveGeneralSettings: PropTypes.func.isRequired,
|
|
||||||
dispatchFetchStatus: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(AuthenticationRequiredModalContentConnector);
|
|
||||||
@@ -8,7 +8,6 @@ export const DEVICE = 'device';
|
|||||||
export const KEY_VALUE_LIST = 'keyValueList';
|
export const KEY_VALUE_LIST = 'keyValueList';
|
||||||
export const INFO = 'info';
|
export const INFO = 'info';
|
||||||
export const MOVIE_MONITORED_SELECT = 'movieMonitoredSelect';
|
export const MOVIE_MONITORED_SELECT = 'movieMonitoredSelect';
|
||||||
export const CATEGORY_SELECT = 'newznabCategorySelect';
|
|
||||||
export const NUMBER = 'number';
|
export const NUMBER = 'number';
|
||||||
export const OAUTH = 'oauth';
|
export const OAUTH = 'oauth';
|
||||||
export const PASSWORD = 'password';
|
export const PASSWORD = 'password';
|
||||||
@@ -33,7 +32,6 @@ export const all = [
|
|||||||
KEY_VALUE_LIST,
|
KEY_VALUE_LIST,
|
||||||
INFO,
|
INFO,
|
||||||
MOVIE_MONITORED_SELECT,
|
MOVIE_MONITORED_SELECT,
|
||||||
CATEGORY_SELECT,
|
|
||||||
NUMBER,
|
NUMBER,
|
||||||
OAUTH,
|
OAUTH,
|
||||||
PASSWORD,
|
PASSWORD,
|
||||||
|
|||||||
@@ -226,42 +226,6 @@ class HistoryRow extends Component {
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
|
||||||
data.label ?
|
|
||||||
<HistoryRowParameter
|
|
||||||
title='Label'
|
|
||||||
value={data.label}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
data.track ?
|
|
||||||
<HistoryRowParameter
|
|
||||||
title='Track'
|
|
||||||
value={data.track}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
data.year ?
|
|
||||||
<HistoryRowParameter
|
|
||||||
title='Year'
|
|
||||||
value={data.year}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
data.genre ?
|
|
||||||
<HistoryRowParameter
|
|
||||||
title='Genre'
|
|
||||||
value={data.genre}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
{
|
||||||
data.author ?
|
data.author ?
|
||||||
<HistoryRowParameter
|
<HistoryRowParameter
|
||||||
@@ -279,15 +243,6 @@ class HistoryRow extends Component {
|
|||||||
/> :
|
/> :
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
|
||||||
data.publisher ?
|
|
||||||
<HistoryRowParameter
|
|
||||||
title='Publisher'
|
|
||||||
value={data.publisher}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ class AddIndexerModalContent extends Component {
|
|||||||
const filteredIndexers = indexers.filter((indexer) => {
|
const filteredIndexers = indexers.filter((indexer) => {
|
||||||
const { filter, filterProtocols, filterLanguages, filterPrivacyLevels } = this.state;
|
const { filter, filterProtocols, filterLanguages, filterPrivacyLevels } = this.state;
|
||||||
|
|
||||||
if (!indexer.name.toLowerCase().includes(filter.toLocaleLowerCase()) && !indexer.description.toLowerCase().includes(filter.toLocaleLowerCase())) {
|
if (!indexer.name.toLowerCase().includes(filter.toLocaleLowerCase())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ class IndexerIndex extends Component {
|
|||||||
|
|
||||||
onKeyUp = (event) => {
|
onKeyUp = (event) => {
|
||||||
const jumpBarItems = this.state.jumpBarItems.order;
|
const jumpBarItems = this.state.jumpBarItems.order;
|
||||||
if (event.composedPath && event.composedPath().length === 4) {
|
if (event.path.length === 4) {
|
||||||
if (event.keyCode === keyCodes.HOME && event.ctrlKey) {
|
if (event.keyCode === keyCodes.HOME && event.ctrlKey) {
|
||||||
this.setState({ jumpToCharacter: jumpBarItems[0] });
|
this.setState({ jumpToCharacter: jumpBarItems[0] });
|
||||||
}
|
}
|
||||||
@@ -272,7 +272,6 @@ class IndexerIndex extends Component {
|
|||||||
saveError,
|
saveError,
|
||||||
isDeleting,
|
isDeleting,
|
||||||
isTestingAll,
|
isTestingAll,
|
||||||
isSyncingIndexers,
|
|
||||||
deleteError,
|
deleteError,
|
||||||
onScroll,
|
onScroll,
|
||||||
onSortSelect,
|
onSortSelect,
|
||||||
@@ -310,15 +309,6 @@ class IndexerIndex extends Component {
|
|||||||
onPress={this.onAddIndexerPress}
|
onPress={this.onAddIndexerPress}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PageToolbarSeparator />
|
|
||||||
|
|
||||||
<PageToolbarButton
|
|
||||||
label={translate('SyncAppIndexers')}
|
|
||||||
iconName={icons.REFRESH}
|
|
||||||
isSpinning={isSyncingIndexers}
|
|
||||||
onPress={this.props.onAppIndexerSyncPress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<PageToolbarButton
|
<PageToolbarButton
|
||||||
label={translate('TestAllIndexers')}
|
label={translate('TestAllIndexers')}
|
||||||
iconName={icons.TEST}
|
iconName={icons.TEST}
|
||||||
@@ -503,12 +493,10 @@ IndexerIndex.propTypes = {
|
|||||||
saveError: PropTypes.object,
|
saveError: PropTypes.object,
|
||||||
isDeleting: PropTypes.bool.isRequired,
|
isDeleting: PropTypes.bool.isRequired,
|
||||||
isTestingAll: PropTypes.bool.isRequired,
|
isTestingAll: PropTypes.bool.isRequired,
|
||||||
isSyncingIndexers: PropTypes.bool.isRequired,
|
|
||||||
deleteError: PropTypes.object,
|
deleteError: PropTypes.object,
|
||||||
onSortSelect: PropTypes.func.isRequired,
|
onSortSelect: PropTypes.func.isRequired,
|
||||||
onFilterSelect: PropTypes.func.isRequired,
|
onFilterSelect: PropTypes.func.isRequired,
|
||||||
onTestAllPress: PropTypes.func.isRequired,
|
onTestAllPress: PropTypes.func.isRequired,
|
||||||
onAppIndexerSyncPress: PropTypes.func.isRequired,
|
|
||||||
onScroll: PropTypes.func.isRequired,
|
onScroll: PropTypes.func.isRequired,
|
||||||
onSaveSelected: PropTypes.func.isRequired
|
onSaveSelected: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,13 +2,10 @@ import PropTypes from 'prop-types';
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import * as commandNames from 'Commands/commandNames';
|
|
||||||
import withScrollPosition from 'Components/withScrollPosition';
|
import withScrollPosition from 'Components/withScrollPosition';
|
||||||
import { executeCommand } from 'Store/Actions/commandActions';
|
|
||||||
import { testAllIndexers } from 'Store/Actions/indexerActions';
|
import { testAllIndexers } from 'Store/Actions/indexerActions';
|
||||||
import { saveIndexerEditor, setMovieFilter, setMovieSort, setMovieTableOption } from 'Store/Actions/indexerIndexActions';
|
import { saveIndexerEditor, setMovieFilter, setMovieSort, setMovieTableOption } from 'Store/Actions/indexerIndexActions';
|
||||||
import scrollPositions from 'Store/scrollPositions';
|
import scrollPositions from 'Store/scrollPositions';
|
||||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
|
||||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
import createIndexerClientSideCollectionItemsSelector from 'Store/Selectors/createIndexerClientSideCollectionItemsSelector';
|
import createIndexerClientSideCollectionItemsSelector from 'Store/Selectors/createIndexerClientSideCollectionItemsSelector';
|
||||||
import IndexerIndex from './IndexerIndex';
|
import IndexerIndex from './IndexerIndex';
|
||||||
@@ -16,16 +13,13 @@ import IndexerIndex from './IndexerIndex';
|
|||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
createIndexerClientSideCollectionItemsSelector('indexerIndex'),
|
createIndexerClientSideCollectionItemsSelector('indexerIndex'),
|
||||||
createCommandExecutingSelector(commandNames.APP_INDEXER_SYNC),
|
|
||||||
createDimensionsSelector(),
|
createDimensionsSelector(),
|
||||||
(
|
(
|
||||||
indexers,
|
indexers,
|
||||||
isSyncingIndexers,
|
|
||||||
dimensionsState
|
dimensionsState
|
||||||
) => {
|
) => {
|
||||||
return {
|
return {
|
||||||
...indexers,
|
...indexers,
|
||||||
isSyncingIndexers,
|
|
||||||
isSmallScreen: dimensionsState.isSmallScreen
|
isSmallScreen: dimensionsState.isSmallScreen
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -52,12 +46,6 @@ function createMapDispatchToProps(dispatch, props) {
|
|||||||
|
|
||||||
onTestAllPress() {
|
onTestAllPress() {
|
||||||
dispatch(testAllIndexers());
|
dispatch(testAllIndexers());
|
||||||
},
|
|
||||||
|
|
||||||
onAppIndexerSyncPress() {
|
|
||||||
dispatch(executeCommand({
|
|
||||||
name: commandNames.APP_INDEXER_SYNC
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,21 +35,21 @@ class IndexerIndexFooter extends PureComponent {
|
|||||||
<div className={styles.legendItem}>
|
<div className={styles.legendItem}>
|
||||||
<div className={styles.enabled} />
|
<div className={styles.enabled} />
|
||||||
<div>
|
<div>
|
||||||
{translate('Enabled')}
|
Enabled
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.legendItem}>
|
<div className={styles.legendItem}>
|
||||||
<div className={styles.redirected} />
|
<div className={styles.redirected} />
|
||||||
<div>
|
<div>
|
||||||
{translate('EnabledRedirected')}
|
Enabled, Redirected
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.legendItem}>
|
<div className={styles.legendItem}>
|
||||||
<div className={styles.disabled} />
|
<div className={styles.disabled} />
|
||||||
<div>
|
<div>
|
||||||
{translate('Disabled')}
|
Disabled
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ class IndexerIndexFooter extends PureComponent {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
{translate('Error')}
|
Error
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ function CapabilitiesLabel(props) {
|
|||||||
let filteredList = categories.filter((item) => item.id < 100000);
|
let filteredList = categories.filter((item) => item.id < 100000);
|
||||||
|
|
||||||
if (categoryFilter.length > 0) {
|
if (categoryFilter.length > 0) {
|
||||||
filteredList = filteredList.filter((item) => categoryFilter.includes(item.id) || (item.subCategories && item.subCategories.some((r) => categoryFilter.includes(r.id))));
|
filteredList = filteredList.filter((item) => categoryFilter.includes(item.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
const nameList = filteredList.map((item) => item.name).sort();
|
const nameList = filteredList.map((item) => item.name).sort();
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
$hoverScale: 1.05;
|
|
||||||
|
|
||||||
.content {
|
|
||||||
display: flex;
|
|
||||||
flex-grow: 0;
|
|
||||||
margin-left: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: var(--cardBackgroundColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
.info {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex-grow: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.titleRow {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
height: 38px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.indexerRow {
|
|
||||||
color: var(--disabledColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
.infoRow {
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
overflow: hidden;
|
|
||||||
width: 85%;
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 14px;
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
@@ -1,202 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import TextTruncate from 'react-text-truncate';
|
|
||||||
import Label from 'Components/Label';
|
|
||||||
import IconButton from 'Components/Link/IconButton';
|
|
||||||
import Link from 'Components/Link/Link';
|
|
||||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
|
||||||
import CategoryLabel from 'Search/Table/CategoryLabel';
|
|
||||||
import Peers from 'Search/Table/Peers';
|
|
||||||
import ProtocolLabel from 'Search/Table/ProtocolLabel';
|
|
||||||
import dimensions from 'Styles/Variables/dimensions';
|
|
||||||
import formatAge from 'Utilities/Number/formatAge';
|
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import styles from './SearchIndexOverview.css';
|
|
||||||
|
|
||||||
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
|
|
||||||
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
|
|
||||||
|
|
||||||
function getContentHeight(rowHeight, isSmallScreen) {
|
|
||||||
const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding;
|
|
||||||
|
|
||||||
return rowHeight - padding;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDownloadIcon(isGrabbing, isGrabbed, grabError) {
|
|
||||||
if (isGrabbing) {
|
|
||||||
return icons.SPINNER;
|
|
||||||
} else if (isGrabbed) {
|
|
||||||
return icons.DOWNLOADING;
|
|
||||||
} else if (grabError) {
|
|
||||||
return icons.DOWNLOADING;
|
|
||||||
}
|
|
||||||
|
|
||||||
return icons.DOWNLOAD;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDownloadTooltip(isGrabbing, isGrabbed, grabError) {
|
|
||||||
if (isGrabbing) {
|
|
||||||
return '';
|
|
||||||
} else if (isGrabbed) {
|
|
||||||
return translate('AddedToDownloadClient');
|
|
||||||
} else if (grabError) {
|
|
||||||
return grabError;
|
|
||||||
}
|
|
||||||
|
|
||||||
return translate('AddToDownloadClient');
|
|
||||||
}
|
|
||||||
|
|
||||||
class SearchIndexOverview extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onGrabPress = () => {
|
|
||||||
const {
|
|
||||||
guid,
|
|
||||||
indexerId,
|
|
||||||
onGrabPress
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
onGrabPress({
|
|
||||||
guid,
|
|
||||||
indexerId
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
title,
|
|
||||||
infoUrl,
|
|
||||||
protocol,
|
|
||||||
downloadUrl,
|
|
||||||
categories,
|
|
||||||
seeders,
|
|
||||||
leechers,
|
|
||||||
size,
|
|
||||||
age,
|
|
||||||
ageHours,
|
|
||||||
ageMinutes,
|
|
||||||
indexer,
|
|
||||||
rowHeight,
|
|
||||||
isSmallScreen,
|
|
||||||
isGrabbed,
|
|
||||||
isGrabbing,
|
|
||||||
grabError
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const contentHeight = getContentHeight(rowHeight, isSmallScreen);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.container}>
|
|
||||||
<div className={styles.content}>
|
|
||||||
<div className={styles.info} style={{ height: contentHeight }}>
|
|
||||||
<div className={styles.titleRow}>
|
|
||||||
<div className={styles.title}>
|
|
||||||
<Link
|
|
||||||
to={infoUrl}
|
|
||||||
title={title}
|
|
||||||
>
|
|
||||||
<TextTruncate
|
|
||||||
line={2}
|
|
||||||
text={title}
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.actions}>
|
|
||||||
<SpinnerIconButton
|
|
||||||
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
|
|
||||||
kind={grabError ? kinds.DANGER : kinds.DEFAULT}
|
|
||||||
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
|
|
||||||
isDisabled={isGrabbed}
|
|
||||||
isSpinning={isGrabbing}
|
|
||||||
onPress={this.onGrabPress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<IconButton
|
|
||||||
className={styles.downloadLink}
|
|
||||||
name={icons.SAVE}
|
|
||||||
title={translate('Save')}
|
|
||||||
to={downloadUrl}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={styles.indexerRow}>
|
|
||||||
{indexer}
|
|
||||||
</div>
|
|
||||||
<div className={styles.infoRow}>
|
|
||||||
<ProtocolLabel
|
|
||||||
protocol={protocol}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
protocol === 'torrent' &&
|
|
||||||
<Peers
|
|
||||||
seeders={seeders}
|
|
||||||
leechers={leechers}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
<Label>
|
|
||||||
{formatBytes(size)}
|
|
||||||
</Label>
|
|
||||||
|
|
||||||
<Label>
|
|
||||||
{formatAge(age, ageHours, ageMinutes)}
|
|
||||||
</Label>
|
|
||||||
|
|
||||||
<CategoryLabel
|
|
||||||
categories={categories}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SearchIndexOverview.propTypes = {
|
|
||||||
guid: PropTypes.string.isRequired,
|
|
||||||
categories: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
protocol: PropTypes.string.isRequired,
|
|
||||||
age: PropTypes.number.isRequired,
|
|
||||||
ageHours: PropTypes.number.isRequired,
|
|
||||||
ageMinutes: PropTypes.number.isRequired,
|
|
||||||
publishDate: PropTypes.string.isRequired,
|
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
infoUrl: PropTypes.string.isRequired,
|
|
||||||
downloadUrl: PropTypes.string.isRequired,
|
|
||||||
indexerId: PropTypes.number.isRequired,
|
|
||||||
indexer: PropTypes.string.isRequired,
|
|
||||||
size: PropTypes.number.isRequired,
|
|
||||||
files: PropTypes.number,
|
|
||||||
grabs: PropTypes.number,
|
|
||||||
seeders: PropTypes.number,
|
|
||||||
leechers: PropTypes.number,
|
|
||||||
indexerFlags: PropTypes.arrayOf(PropTypes.string).isRequired,
|
|
||||||
rowHeight: PropTypes.number.isRequired,
|
|
||||||
showRelativeDates: PropTypes.bool.isRequired,
|
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
|
||||||
longDateFormat: PropTypes.string.isRequired,
|
|
||||||
timeFormat: PropTypes.string.isRequired,
|
|
||||||
isSmallScreen: PropTypes.bool.isRequired,
|
|
||||||
onGrabPress: PropTypes.func.isRequired,
|
|
||||||
isGrabbing: PropTypes.bool.isRequired,
|
|
||||||
isGrabbed: PropTypes.bool.isRequired,
|
|
||||||
grabError: PropTypes.string
|
|
||||||
};
|
|
||||||
|
|
||||||
SearchIndexOverview.defaultProps = {
|
|
||||||
isGrabbing: false,
|
|
||||||
isGrabbed: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SearchIndexOverview;
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
.grid {
|
|
||||||
flex: 1 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
&:hover {
|
|
||||||
.content {
|
|
||||||
background-color: var(--tableRowHoverBackgroundColor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { Grid, WindowScroller } from 'react-virtualized';
|
|
||||||
import Measure from 'Components/Measure';
|
|
||||||
import SearchIndexItemConnector from 'Search/Table/SearchIndexItemConnector';
|
|
||||||
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
|
|
||||||
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
|
||||||
import SearchIndexOverview from './SearchIndexOverview';
|
|
||||||
import styles from './SearchIndexOverviews.css';
|
|
||||||
|
|
||||||
class SearchIndexOverviews extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
width: 0,
|
|
||||||
columnCount: 1,
|
|
||||||
rowHeight: 100,
|
|
||||||
scrollRestored: false
|
|
||||||
};
|
|
||||||
|
|
||||||
this._grid = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
|
||||||
const {
|
|
||||||
items,
|
|
||||||
sortKey,
|
|
||||||
jumpToCharacter,
|
|
||||||
scrollTop,
|
|
||||||
isSmallScreen
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
width,
|
|
||||||
rowHeight,
|
|
||||||
scrollRestored
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
if (prevProps.sortKey !== sortKey) {
|
|
||||||
this.calculateGrid(this.state.width, isSmallScreen);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
this._grid &&
|
|
||||||
(prevState.width !== width ||
|
|
||||||
prevState.rowHeight !== rowHeight ||
|
|
||||||
hasDifferentItemsOrOrder(prevProps.items, items, 'guid')
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
// recomputeGridSize also forces Grid to discard its cache of rendered cells
|
|
||||||
this._grid.recomputeGridSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._grid && scrollTop !== 0 && !scrollRestored) {
|
|
||||||
this.setState({ scrollRestored: true });
|
|
||||||
this._grid.scrollToPosition({ scrollTop });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) {
|
|
||||||
const index = getIndexOfFirstCharacter(items, jumpToCharacter);
|
|
||||||
|
|
||||||
if (this._grid && index != null) {
|
|
||||||
|
|
||||||
this._grid.scrollToCell({
|
|
||||||
rowIndex: index,
|
|
||||||
columnIndex: 0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
setGridRef = (ref) => {
|
|
||||||
this._grid = ref;
|
|
||||||
};
|
|
||||||
|
|
||||||
calculateGrid = (width = this.state.width, isSmallScreen) => {
|
|
||||||
const rowHeight = 100;
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
width,
|
|
||||||
rowHeight
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
cellRenderer = ({ key, rowIndex, style }) => {
|
|
||||||
const {
|
|
||||||
items,
|
|
||||||
showRelativeDates,
|
|
||||||
shortDateFormat,
|
|
||||||
longDateFormat,
|
|
||||||
timeFormat,
|
|
||||||
isSmallScreen,
|
|
||||||
onGrabPress
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
rowHeight
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const release = items[rowIndex];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={styles.container}
|
|
||||||
key={key}
|
|
||||||
style={style}
|
|
||||||
>
|
|
||||||
<SearchIndexItemConnector
|
|
||||||
key={release.guid}
|
|
||||||
component={SearchIndexOverview}
|
|
||||||
rowHeight={rowHeight}
|
|
||||||
showRelativeDates={showRelativeDates}
|
|
||||||
shortDateFormat={shortDateFormat}
|
|
||||||
longDateFormat={longDateFormat}
|
|
||||||
timeFormat={timeFormat}
|
|
||||||
isSmallScreen={isSmallScreen}
|
|
||||||
style={style}
|
|
||||||
guid={release.guid}
|
|
||||||
onGrabPress={onGrabPress}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onMeasure = ({ width }) => {
|
|
||||||
this.calculateGrid(width, this.props.isSmallScreen);
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
items
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
width,
|
|
||||||
rowHeight
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Measure
|
|
||||||
whitelist={['width']}
|
|
||||||
onMeasure={this.onMeasure}
|
|
||||||
>
|
|
||||||
<WindowScroller
|
|
||||||
scrollElement={undefined}
|
|
||||||
>
|
|
||||||
{({ height, registerChild, onChildScroll, scrollTop }) => {
|
|
||||||
if (!height) {
|
|
||||||
return <div />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={registerChild}>
|
|
||||||
<Grid
|
|
||||||
ref={this.setGridRef}
|
|
||||||
className={styles.grid}
|
|
||||||
autoHeight={true}
|
|
||||||
height={height}
|
|
||||||
columnCount={1}
|
|
||||||
columnWidth={width}
|
|
||||||
rowCount={items.length}
|
|
||||||
rowHeight={rowHeight}
|
|
||||||
width={width}
|
|
||||||
onScroll={onChildScroll}
|
|
||||||
scrollTop={scrollTop}
|
|
||||||
overscanRowCount={2}
|
|
||||||
cellRenderer={this.cellRenderer}
|
|
||||||
scrollToAlignment={'start'}
|
|
||||||
isScrollingOptOut={true}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</WindowScroller>
|
|
||||||
</Measure>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SearchIndexOverviews.propTypes = {
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
sortKey: PropTypes.string,
|
|
||||||
scrollTop: PropTypes.number.isRequired,
|
|
||||||
jumpToCharacter: PropTypes.string,
|
|
||||||
scroller: PropTypes.instanceOf(Element).isRequired,
|
|
||||||
showRelativeDates: PropTypes.bool.isRequired,
|
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
|
||||||
longDateFormat: PropTypes.string.isRequired,
|
|
||||||
isSmallScreen: PropTypes.bool.isRequired,
|
|
||||||
timeFormat: PropTypes.string.isRequired,
|
|
||||||
onGrabPress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SearchIndexOverviews;
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { grabRelease } from 'Store/Actions/releaseActions';
|
|
||||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
|
||||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
|
||||||
import SearchIndexOverviews from './SearchIndexOverviews';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
createUISettingsSelector(),
|
|
||||||
createDimensionsSelector(),
|
|
||||||
(uiSettings, dimensions) => {
|
|
||||||
return {
|
|
||||||
showRelativeDates: uiSettings.showRelativeDates,
|
|
||||||
shortDateFormat: uiSettings.shortDateFormat,
|
|
||||||
longDateFormat: uiSettings.longDateFormat,
|
|
||||||
timeFormat: uiSettings.timeFormat,
|
|
||||||
isSmallScreen: dimensions.isSmallScreen
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMapDispatchToProps(dispatch, props) {
|
|
||||||
return {
|
|
||||||
onGrabPress(payload) {
|
|
||||||
dispatch(grabRelease(payload));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, createMapDispatchToProps)(SearchIndexOverviews);
|
|
||||||
@@ -24,7 +24,6 @@ const searchOptions = [
|
|||||||
const seriesTokens = [
|
const seriesTokens = [
|
||||||
{ token: '{ImdbId:tt1234567}', example: 'tt12345' },
|
{ token: '{ImdbId:tt1234567}', example: 'tt12345' },
|
||||||
{ token: '{TvdbId:12345}', example: '12345' },
|
{ token: '{TvdbId:12345}', example: '12345' },
|
||||||
{ token: '{TmdbId:12345}', example: '12345' },
|
|
||||||
{ token: '{TvMazeId:12345}', example: '54321' },
|
{ token: '{TvMazeId:12345}', example: '54321' },
|
||||||
{ token: '{Season:00}', example: '01' },
|
{ token: '{Season:00}', example: '01' },
|
||||||
{ token: '{Episode:00}', example: '01' }
|
{ token: '{Episode:00}', example: '01' }
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
|||||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||||
import { align, icons, sortDirections } from 'Helpers/Props';
|
import { align, icons, sortDirections } from 'Helpers/Props';
|
||||||
|
import AddIndexerModal from 'Indexer/Add/AddIndexerModal';
|
||||||
|
import EditIndexerModalConnector from 'Indexer/Edit/EditIndexerModalConnector';
|
||||||
import NoIndexer from 'Indexer/NoIndexer';
|
import NoIndexer from 'Indexer/NoIndexer';
|
||||||
import * as keyCodes from 'Utilities/Constants/keyCodes';
|
import * as keyCodes from 'Utilities/Constants/keyCodes';
|
||||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||||
@@ -21,17 +23,12 @@ import selectAll from 'Utilities/Table/selectAll';
|
|||||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||||
import SearchIndexFilterMenu from './Menus/SearchIndexFilterMenu';
|
import SearchIndexFilterMenu from './Menus/SearchIndexFilterMenu';
|
||||||
import SearchIndexSortMenu from './Menus/SearchIndexSortMenu';
|
import SearchIndexSortMenu from './Menus/SearchIndexSortMenu';
|
||||||
import SearchIndexOverviewsConnector from './Mobile/SearchIndexOverviewsConnector';
|
|
||||||
import NoSearchResults from './NoSearchResults';
|
import NoSearchResults from './NoSearchResults';
|
||||||
import SearchFooterConnector from './SearchFooterConnector';
|
import SearchFooterConnector from './SearchFooterConnector';
|
||||||
import SearchIndexTableConnector from './Table/SearchIndexTableConnector';
|
import SearchIndexTableConnector from './Table/SearchIndexTableConnector';
|
||||||
import styles from './SearchIndex.css';
|
import styles from './SearchIndex.css';
|
||||||
|
|
||||||
function getViewComponent(isSmallScreen) {
|
function getViewComponent() {
|
||||||
if (isSmallScreen) {
|
|
||||||
return SearchIndexOverviewsConnector;
|
|
||||||
}
|
|
||||||
|
|
||||||
return SearchIndexTableConnector;
|
return SearchIndexTableConnector;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,6 +44,8 @@ class SearchIndex extends Component {
|
|||||||
scroller: null,
|
scroller: null,
|
||||||
jumpBarItems: { order: [] },
|
jumpBarItems: { order: [] },
|
||||||
jumpToCharacter: null,
|
jumpToCharacter: null,
|
||||||
|
isAddIndexerModalOpen: false,
|
||||||
|
isEditIndexerModalOpen: false,
|
||||||
searchType: null,
|
searchType: null,
|
||||||
lastToggled: null,
|
lastToggled: null,
|
||||||
allSelected: false,
|
allSelected: false,
|
||||||
@@ -178,6 +177,21 @@ class SearchIndex extends Component {
|
|||||||
//
|
//
|
||||||
// Listeners
|
// Listeners
|
||||||
|
|
||||||
|
onAddIndexerPress = () => {
|
||||||
|
this.setState({ isAddIndexerModalOpen: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onAddIndexerModalClose = ({ indexerSelected = false } = {}) => {
|
||||||
|
this.setState({
|
||||||
|
isAddIndexerModalOpen: false,
|
||||||
|
isEditIndexerModalOpen: indexerSelected
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onEditIndexerModalClose = () => {
|
||||||
|
this.setState({ isEditIndexerModalOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
onJumpBarItemPress = (jumpToCharacter) => {
|
onJumpBarItemPress = (jumpToCharacter) => {
|
||||||
this.setState({ jumpToCharacter });
|
this.setState({ jumpToCharacter });
|
||||||
};
|
};
|
||||||
@@ -239,7 +253,6 @@ class SearchIndex extends Component {
|
|||||||
onScroll,
|
onScroll,
|
||||||
onSortSelect,
|
onSortSelect,
|
||||||
onFilterSelect,
|
onFilterSelect,
|
||||||
isSmallScreen,
|
|
||||||
hasIndexers,
|
hasIndexers,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
@@ -247,6 +260,8 @@ class SearchIndex extends Component {
|
|||||||
const {
|
const {
|
||||||
scroller,
|
scroller,
|
||||||
jumpBarItems,
|
jumpBarItems,
|
||||||
|
isAddIndexerModalOpen,
|
||||||
|
isEditIndexerModalOpen,
|
||||||
jumpToCharacter,
|
jumpToCharacter,
|
||||||
selectedState,
|
selectedState,
|
||||||
allSelected,
|
allSelected,
|
||||||
@@ -255,7 +270,7 @@ class SearchIndex extends Component {
|
|||||||
|
|
||||||
const selectedIndexerIds = this.getSelectedIds();
|
const selectedIndexerIds = this.getSelectedIds();
|
||||||
|
|
||||||
const ViewComponent = getViewComponent(isSmallScreen);
|
const ViewComponent = getViewComponent();
|
||||||
const isLoaded = !!(!error && isPopulated && items.length && scroller);
|
const isLoaded = !!(!error && isPopulated && items.length && scroller);
|
||||||
const hasNoIndexer = !totalItems;
|
const hasNoIndexer = !totalItems;
|
||||||
|
|
||||||
@@ -369,6 +384,16 @@ class SearchIndex extends Component {
|
|||||||
onSearchPress={this.onSearchPress}
|
onSearchPress={this.onSearchPress}
|
||||||
onBulkGrabPress={this.onBulkGrabPress}
|
onBulkGrabPress={this.onBulkGrabPress}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<AddIndexerModal
|
||||||
|
isOpen={isAddIndexerModalOpen}
|
||||||
|
onModalClose={this.onAddIndexerModalClose}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EditIndexerModalConnector
|
||||||
|
isOpen={isEditIndexerModalOpen}
|
||||||
|
onModalClose={this.onEditIndexerModalClose}
|
||||||
|
/>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
.category {
|
.category {
|
||||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||||
|
|
||||||
flex: 0 0 130px;
|
flex: 0 0 110px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.age,
|
.age,
|
||||||
|
|||||||
@@ -18,20 +18,20 @@ function createMapStateToProps() {
|
|||||||
return createSelector(
|
return createSelector(
|
||||||
createReleaseSelector(),
|
createReleaseSelector(),
|
||||||
(
|
(
|
||||||
release
|
movie
|
||||||
) => {
|
) => {
|
||||||
|
|
||||||
// If a release is deleted this selector may fire before the parent
|
// If a movie is deleted this selector may fire before the parent
|
||||||
// selecors, which will result in an undefined release, if that happens
|
// selecors, which will result in an undefined movie, if that happens
|
||||||
// we want to return early here and again in the render function to avoid
|
// we want to return early here and again in the render function to avoid
|
||||||
// trying to show a release that has no information available.
|
// trying to show a movie that has no information available.
|
||||||
|
|
||||||
if (!release) {
|
if (!movie) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...release
|
...movie
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -41,7 +41,7 @@ const mapDispatchToProps = {
|
|||||||
dispatchExecuteCommand: executeCommand
|
dispatchExecuteCommand: executeCommand
|
||||||
};
|
};
|
||||||
|
|
||||||
class SearchIndexItemConnector extends Component {
|
class MovieIndexItemConnector extends Component {
|
||||||
|
|
||||||
//
|
//
|
||||||
// Render
|
// Render
|
||||||
@@ -66,9 +66,9 @@ class SearchIndexItemConnector extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SearchIndexItemConnector.propTypes = {
|
MovieIndexItemConnector.propTypes = {
|
||||||
guid: PropTypes.string,
|
guid: PropTypes.string,
|
||||||
component: PropTypes.elementType.isRequired
|
component: PropTypes.elementType.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(SearchIndexItemConnector);
|
export default connect(createMapStateToProps, mapDispatchToProps)(MovieIndexItemConnector);
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
.category {
|
.category {
|
||||||
composes: cell;
|
composes: cell;
|
||||||
|
|
||||||
flex: 0 0 130px;
|
flex: 0 0 110px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.age,
|
.age,
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Modal from 'Components/Modal/Modal';
|
|
||||||
import { sizes } from 'Helpers/Props';
|
|
||||||
import AddCategoryModalContentConnector from './AddCategoryModalContentConnector';
|
|
||||||
|
|
||||||
function AddCategoryModal({ isOpen, onModalClose, ...otherProps }) {
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
size={sizes.MEDIUM}
|
|
||||||
isOpen={isOpen}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
>
|
|
||||||
<AddCategoryModalContentConnector
|
|
||||||
{...otherProps}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
AddCategoryModal.propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AddCategoryModal;
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
|
||||||
import AddCategoryModal from './AddCategoryModal';
|
|
||||||
|
|
||||||
function createMapDispatchToProps(dispatch, props) {
|
|
||||||
const section = 'settings.downloadClientCategories';
|
|
||||||
|
|
||||||
return {
|
|
||||||
dispatchClearPendingChanges() {
|
|
||||||
dispatch(clearPendingChanges({ section }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
class AddCategoryModalConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onModalClose = () => {
|
|
||||||
this.props.dispatchClearPendingChanges();
|
|
||||||
this.props.onModalClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
dispatchClearPendingChanges,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AddCategoryModal
|
|
||||||
{...otherProps}
|
|
||||||
onModalClose={this.onModalClose}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AddCategoryModalConnector.propTypes = {
|
|
||||||
onModalClose: PropTypes.func.isRequired,
|
|
||||||
dispatchClearPendingChanges: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(null, createMapDispatchToProps)(AddCategoryModalConnector);
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
.deleteButton {
|
|
||||||
composes: button from '~Components/Link/Button.css';
|
|
||||||
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Form from 'Components/Form/Form';
|
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
|
||||||
import Button from 'Components/Link/Button';
|
|
||||||
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
|
||||||
import { inputTypes, kinds } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import styles from './AddCategoryModalContent.css';
|
|
||||||
|
|
||||||
function AddCategoryModalContent(props) {
|
|
||||||
const {
|
|
||||||
advancedSettings,
|
|
||||||
item,
|
|
||||||
onInputChange,
|
|
||||||
onFieldChange,
|
|
||||||
onCancelPress,
|
|
||||||
onSavePress,
|
|
||||||
onDeleteSpecificationPress,
|
|
||||||
...otherProps
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
clientCategory,
|
|
||||||
categories
|
|
||||||
} = item;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalContent onModalClose={onCancelPress}>
|
|
||||||
<ModalHeader>
|
|
||||||
{`${id ? 'Edit' : 'Add'} Category`}
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody>
|
|
||||||
<Form
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>
|
|
||||||
{translate('DownloadClientCategory')}
|
|
||||||
</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.TEXT}
|
|
||||||
name="clientCategory"
|
|
||||||
{...clientCategory}
|
|
||||||
onChange={onInputChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>
|
|
||||||
{translate('MappedCategories')}
|
|
||||||
</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CATEGORY_SELECT}
|
|
||||||
name="categories"
|
|
||||||
{...categories}
|
|
||||||
onChange={onInputChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
</Form>
|
|
||||||
</ModalBody>
|
|
||||||
<ModalFooter>
|
|
||||||
{
|
|
||||||
id &&
|
|
||||||
<Button
|
|
||||||
className={styles.deleteButton}
|
|
||||||
kind={kinds.DANGER}
|
|
||||||
onPress={onDeleteSpecificationPress}
|
|
||||||
>
|
|
||||||
{translate('Delete')}
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onPress={onCancelPress}
|
|
||||||
>
|
|
||||||
{translate('Cancel')}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<SpinnerErrorButton
|
|
||||||
isSpinning={false}
|
|
||||||
onPress={onSavePress}
|
|
||||||
>
|
|
||||||
{translate('Save')}
|
|
||||||
</SpinnerErrorButton>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
AddCategoryModalContent.propTypes = {
|
|
||||||
advancedSettings: PropTypes.bool.isRequired,
|
|
||||||
item: PropTypes.object.isRequired,
|
|
||||||
onInputChange: PropTypes.func.isRequired,
|
|
||||||
onFieldChange: PropTypes.func.isRequired,
|
|
||||||
onCancelPress: PropTypes.func.isRequired,
|
|
||||||
onSavePress: PropTypes.func.isRequired,
|
|
||||||
onDeleteSpecificationPress: PropTypes.func
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AddCategoryModalContent;
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { clearDownloadClientCategoryPending, saveDownloadClientCategory, setDownloadClientCategoryFieldValue, setDownloadClientCategoryValue } from 'Store/Actions/settingsActions';
|
|
||||||
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
|
||||||
import AddCategoryModalContent from './AddCategoryModalContent';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.settings.advancedSettings,
|
|
||||||
createProviderSettingsSelector('downloadClientCategories'),
|
|
||||||
(advancedSettings, specification) => {
|
|
||||||
return {
|
|
||||||
advancedSettings,
|
|
||||||
...specification
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
setDownloadClientCategoryValue,
|
|
||||||
setDownloadClientCategoryFieldValue,
|
|
||||||
saveDownloadClientCategory,
|
|
||||||
clearDownloadClientCategoryPending
|
|
||||||
};
|
|
||||||
|
|
||||||
class AddCategoryModalContentConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onInputChange = ({ name, value }) => {
|
|
||||||
this.props.setDownloadClientCategoryValue({ name, value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onFieldChange = ({ name, value }) => {
|
|
||||||
this.props.setDownloadClientCategoryFieldValue({ name, value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onCancelPress = () => {
|
|
||||||
this.props.clearDownloadClientCategoryPending();
|
|
||||||
this.props.onModalClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
onSavePress = () => {
|
|
||||||
this.props.saveDownloadClientCategory({ id: this.props.id });
|
|
||||||
this.props.onModalClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<AddCategoryModalContent
|
|
||||||
{...this.props}
|
|
||||||
onCancelPress={this.onCancelPress}
|
|
||||||
onSavePress={this.onSavePress}
|
|
||||||
onInputChange={this.onInputChange}
|
|
||||||
onFieldChange={this.onFieldChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AddCategoryModalContentConnector.propTypes = {
|
|
||||||
id: PropTypes.number,
|
|
||||||
item: PropTypes.object.isRequired,
|
|
||||||
setDownloadClientCategoryValue: PropTypes.func.isRequired,
|
|
||||||
setDownloadClientCategoryFieldValue: PropTypes.func.isRequired,
|
|
||||||
clearDownloadClientCategoryPending: PropTypes.func.isRequired,
|
|
||||||
saveDownloadClientCategory: PropTypes.func.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(AddCategoryModalContentConnector);
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
.customFormat {
|
|
||||||
composes: card from '~Components/Card.css';
|
|
||||||
|
|
||||||
width: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nameContainer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name {
|
|
||||||
@add-mixin truncate;
|
|
||||||
|
|
||||||
margin-bottom: 5px;
|
|
||||||
font-weight: 300;
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.labels {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin-top: 5px;
|
|
||||||
pointer-events: all;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltipLabel {
|
|
||||||
composes: label from '~Components/Label.css';
|
|
||||||
|
|
||||||
margin: 0;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Card from 'Components/Card';
|
|
||||||
import Label from 'Components/Label';
|
|
||||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
|
||||||
import { kinds } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import AddCategoryModalConnector from './AddCategoryModalConnector';
|
|
||||||
import styles from './Category.css';
|
|
||||||
|
|
||||||
class Category extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isEditSpecificationModalOpen: false,
|
|
||||||
isDeleteSpecificationModalOpen: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onEditSpecificationPress = () => {
|
|
||||||
this.setState({ isEditSpecificationModalOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onEditSpecificationModalClose = () => {
|
|
||||||
this.setState({ isEditSpecificationModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
onDeleteSpecificationPress = () => {
|
|
||||||
this.setState({
|
|
||||||
isEditSpecificationModalOpen: false,
|
|
||||||
isDeleteSpecificationModalOpen: true
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onDeleteSpecificationModalClose = () => {
|
|
||||||
this.setState({ isDeleteSpecificationModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
onConfirmDeleteSpecification = () => {
|
|
||||||
this.props.onConfirmDeleteSpecification(this.props.id);
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
clientCategory,
|
|
||||||
categories
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
className={styles.customFormat}
|
|
||||||
overlayContent={true}
|
|
||||||
onPress={this.onEditSpecificationPress}
|
|
||||||
>
|
|
||||||
<div className={styles.nameContainer}>
|
|
||||||
<div className={styles.name}>
|
|
||||||
{clientCategory}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Label kind={kinds.PRIMARY}>
|
|
||||||
{`${categories.length} ${categories.length > 1 ? translate('Categories') : translate('Category')}`}
|
|
||||||
</Label>
|
|
||||||
|
|
||||||
<AddCategoryModalConnector
|
|
||||||
id={id}
|
|
||||||
isOpen={this.state.isEditSpecificationModalOpen}
|
|
||||||
onModalClose={this.onEditSpecificationModalClose}
|
|
||||||
onDeleteSpecificationPress={this.onDeleteSpecificationPress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ConfirmModal
|
|
||||||
isOpen={this.state.isDeleteSpecificationModalOpen}
|
|
||||||
kind={kinds.DANGER}
|
|
||||||
title={translate('DeleteClientCategory')}
|
|
||||||
message={
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
{translate('AreYouSureYouWantToDeleteCategory', [name])}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
confirmLabel={translate('Delete')}
|
|
||||||
onConfirm={this.onConfirmDeleteSpecification}
|
|
||||||
onCancel={this.onDeleteSpecificationModalClose}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Category.propTypes = {
|
|
||||||
id: PropTypes.number.isRequired,
|
|
||||||
categories: PropTypes.arrayOf(PropTypes.number).isRequired,
|
|
||||||
clientCategory: PropTypes.string.isRequired,
|
|
||||||
onConfirmDeleteSpecification: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Category;
|
|
||||||
@@ -1,14 +1,11 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import Alert from 'Components/Alert';
|
import Alert from 'Components/Alert';
|
||||||
import Card from 'Components/Card';
|
|
||||||
import FieldSet from 'Components/FieldSet';
|
|
||||||
import Form from 'Components/Form/Form';
|
import Form from 'Components/Form/Form';
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
|
import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
@@ -16,33 +13,12 @@ import ModalBody from 'Components/Modal/ModalBody';
|
|||||||
import ModalContent from 'Components/Modal/ModalContent';
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import { icons, inputTypes, kinds } from 'Helpers/Props';
|
import { inputTypes, kinds } from 'Helpers/Props';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import AddCategoryModalConnector from './Categories/AddCategoryModalConnector';
|
|
||||||
import Category from './Categories/Category';
|
|
||||||
import styles from './EditDownloadClientModalContent.css';
|
import styles from './EditDownloadClientModalContent.css';
|
||||||
|
|
||||||
class EditDownloadClientModalContent extends Component {
|
class EditDownloadClientModalContent extends Component {
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isAddCategoryModalOpen: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
onAddCategoryPress = () => {
|
|
||||||
this.setState({ isAddCategoryModalOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onAddCategoryModalClose = () => {
|
|
||||||
this.setState({ isAddCategoryModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Render
|
// Render
|
||||||
|
|
||||||
@@ -51,7 +27,6 @@ class EditDownloadClientModalContent extends Component {
|
|||||||
advancedSettings,
|
advancedSettings,
|
||||||
isFetching,
|
isFetching,
|
||||||
error,
|
error,
|
||||||
categories,
|
|
||||||
isSaving,
|
isSaving,
|
||||||
isTesting,
|
isTesting,
|
||||||
saveError,
|
saveError,
|
||||||
@@ -62,21 +37,15 @@ class EditDownloadClientModalContent extends Component {
|
|||||||
onSavePress,
|
onSavePress,
|
||||||
onTestPress,
|
onTestPress,
|
||||||
onDeleteDownloadClientPress,
|
onDeleteDownloadClientPress,
|
||||||
onConfirmDeleteCategory,
|
|
||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const {
|
|
||||||
isAddCategoryModalOpen
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
implementationName,
|
implementationName,
|
||||||
name,
|
name,
|
||||||
enable,
|
enable,
|
||||||
priority,
|
priority,
|
||||||
supportsCategories,
|
|
||||||
fields,
|
fields,
|
||||||
message
|
message
|
||||||
} = item;
|
} = item;
|
||||||
@@ -167,43 +136,6 @@ class EditDownloadClientModalContent extends Component {
|
|||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
{
|
|
||||||
supportsCategories.value ?
|
|
||||||
<FieldSet legend={translate('MappedCategories')}>
|
|
||||||
<div className={styles.customFormats}>
|
|
||||||
{
|
|
||||||
categories.map((tag) => {
|
|
||||||
return (
|
|
||||||
<Category
|
|
||||||
key={tag.id}
|
|
||||||
{...tag}
|
|
||||||
onConfirmDeleteSpecification={onConfirmDeleteCategory}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
<Card
|
|
||||||
className={styles.addCategory}
|
|
||||||
onPress={this.onAddCategoryPress}
|
|
||||||
>
|
|
||||||
<div className={styles.center}>
|
|
||||||
<Icon
|
|
||||||
name={icons.ADD}
|
|
||||||
size={25}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</FieldSet> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
<AddCategoryModalConnector
|
|
||||||
isOpen={isAddCategoryModalOpen}
|
|
||||||
onModalClose={this.onAddCategoryModalClose}
|
|
||||||
/>
|
|
||||||
|
|
||||||
</Form>
|
</Form>
|
||||||
}
|
}
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
@@ -253,15 +185,13 @@ EditDownloadClientModalContent.propTypes = {
|
|||||||
isSaving: PropTypes.bool.isRequired,
|
isSaving: PropTypes.bool.isRequired,
|
||||||
saveError: PropTypes.object,
|
saveError: PropTypes.object,
|
||||||
isTesting: PropTypes.bool.isRequired,
|
isTesting: PropTypes.bool.isRequired,
|
||||||
categories: PropTypes.arrayOf(PropTypes.object),
|
|
||||||
item: PropTypes.object.isRequired,
|
item: PropTypes.object.isRequired,
|
||||||
onInputChange: PropTypes.func.isRequired,
|
onInputChange: PropTypes.func.isRequired,
|
||||||
onFieldChange: PropTypes.func.isRequired,
|
onFieldChange: PropTypes.func.isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired,
|
onModalClose: PropTypes.func.isRequired,
|
||||||
onSavePress: PropTypes.func.isRequired,
|
onSavePress: PropTypes.func.isRequired,
|
||||||
onTestPress: PropTypes.func.isRequired,
|
onTestPress: PropTypes.func.isRequired,
|
||||||
onDeleteDownloadClientPress: PropTypes.func,
|
onDeleteDownloadClientPress: PropTypes.func
|
||||||
onConfirmDeleteCategory: PropTypes.func.isRequired
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default EditDownloadClientModalContent;
|
export default EditDownloadClientModalContent;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { deleteDownloadClientCategory, fetchDownloadClientCategories, saveDownloadClient, setDownloadClientFieldValue, setDownloadClientValue, testDownloadClient } from 'Store/Actions/settingsActions';
|
import { saveDownloadClient, setDownloadClientFieldValue, setDownloadClientValue, testDownloadClient } from 'Store/Actions/settingsActions';
|
||||||
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
||||||
import EditDownloadClientModalContent from './EditDownloadClientModalContent';
|
import EditDownloadClientModalContent from './EditDownloadClientModalContent';
|
||||||
|
|
||||||
@@ -10,12 +10,10 @@ function createMapStateToProps() {
|
|||||||
return createSelector(
|
return createSelector(
|
||||||
(state) => state.settings.advancedSettings,
|
(state) => state.settings.advancedSettings,
|
||||||
createProviderSettingsSelector('downloadClients'),
|
createProviderSettingsSelector('downloadClients'),
|
||||||
(state) => state.settings.downloadClientCategories,
|
(advancedSettings, downloadClient) => {
|
||||||
(advancedSettings, downloadClient, categories) => {
|
|
||||||
return {
|
return {
|
||||||
advancedSettings,
|
advancedSettings,
|
||||||
...downloadClient,
|
...downloadClient
|
||||||
categories: categories.items
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -25,9 +23,7 @@ const mapDispatchToProps = {
|
|||||||
setDownloadClientValue,
|
setDownloadClientValue,
|
||||||
setDownloadClientFieldValue,
|
setDownloadClientFieldValue,
|
||||||
saveDownloadClient,
|
saveDownloadClient,
|
||||||
testDownloadClient,
|
testDownloadClient
|
||||||
fetchDownloadClientCategories,
|
|
||||||
deleteDownloadClientCategory
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class EditDownloadClientModalContentConnector extends Component {
|
class EditDownloadClientModalContentConnector extends Component {
|
||||||
@@ -35,14 +31,6 @@ class EditDownloadClientModalContentConnector extends Component {
|
|||||||
//
|
//
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
tagsFromId
|
|
||||||
} = this.props;
|
|
||||||
this.props.fetchDownloadClientCategories({ id: tagsFromId || id });
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
componentDidUpdate(prevProps, prevState) {
|
||||||
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
|
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
|
||||||
this.props.onModalClose();
|
this.props.onModalClose();
|
||||||
@@ -68,10 +56,6 @@ class EditDownloadClientModalContentConnector extends Component {
|
|||||||
this.props.testDownloadClient({ id: this.props.id });
|
this.props.testDownloadClient({ id: this.props.id });
|
||||||
};
|
};
|
||||||
|
|
||||||
onConfirmDeleteCategory = (id) => {
|
|
||||||
this.props.deleteDownloadClientCategory({ id });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Render
|
// Render
|
||||||
|
|
||||||
@@ -83,7 +67,6 @@ class EditDownloadClientModalContentConnector extends Component {
|
|||||||
onTestPress={this.onTestPress}
|
onTestPress={this.onTestPress}
|
||||||
onInputChange={this.onInputChange}
|
onInputChange={this.onInputChange}
|
||||||
onFieldChange={this.onFieldChange}
|
onFieldChange={this.onFieldChange}
|
||||||
onConfirmDeleteCategory={this.onConfirmDeleteCategory}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -91,13 +74,10 @@ class EditDownloadClientModalContentConnector extends Component {
|
|||||||
|
|
||||||
EditDownloadClientModalContentConnector.propTypes = {
|
EditDownloadClientModalContentConnector.propTypes = {
|
||||||
id: PropTypes.number,
|
id: PropTypes.number,
|
||||||
tagsFromId: PropTypes.number,
|
|
||||||
isFetching: PropTypes.bool.isRequired,
|
isFetching: PropTypes.bool.isRequired,
|
||||||
isSaving: PropTypes.bool.isRequired,
|
isSaving: PropTypes.bool.isRequired,
|
||||||
saveError: PropTypes.object,
|
saveError: PropTypes.object,
|
||||||
item: PropTypes.object.isRequired,
|
item: PropTypes.object.isRequired,
|
||||||
fetchDownloadClientCategories: PropTypes.func.isRequired,
|
|
||||||
deleteDownloadClientCategory: PropTypes.func.isRequired,
|
|
||||||
setDownloadClientValue: PropTypes.func.isRequired,
|
setDownloadClientValue: PropTypes.func.isRequired,
|
||||||
setDownloadClientFieldValue: PropTypes.func.isRequired,
|
setDownloadClientFieldValue: PropTypes.func.isRequired,
|
||||||
saveDownloadClient: PropTypes.func.isRequired,
|
saveDownloadClient: PropTypes.func.isRequired,
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const requiresRestartKeys = [
|
|||||||
'bindAddress',
|
'bindAddress',
|
||||||
'port',
|
'port',
|
||||||
'urlBase',
|
'urlBase',
|
||||||
|
'instanceName',
|
||||||
'enableSsl',
|
'enableSsl',
|
||||||
'sslPort',
|
'sslPort',
|
||||||
'sslCertPath',
|
'sslCertPath',
|
||||||
|
|||||||
@@ -11,20 +11,12 @@ import ConfirmModal from 'Components/Modal/ConfirmModal';
|
|||||||
import { icons, inputTypes, kinds } from 'Helpers/Props';
|
import { icons, inputTypes, kinds } from 'Helpers/Props';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
export const authenticationRequiredWarning = translate('AuthenticationRequiredWarning');
|
const authenticationMethodOptions = [
|
||||||
|
{ key: 'none', value: 'None' },
|
||||||
export const authenticationMethodOptions = [
|
|
||||||
{ key: 'none', value: 'None', isDisabled: true },
|
|
||||||
{ key: 'external', value: 'External', isHidden: true },
|
|
||||||
{ key: 'basic', value: 'Basic (Browser Popup)' },
|
{ key: 'basic', value: 'Basic (Browser Popup)' },
|
||||||
{ key: 'forms', value: 'Forms (Login Page)' }
|
{ key: 'forms', value: 'Forms (Login Page)' }
|
||||||
];
|
];
|
||||||
|
|
||||||
export const authenticationRequiredOptions = [
|
|
||||||
{ key: 'enabled', value: 'Enabled' },
|
|
||||||
{ key: 'disabledForLocalAddresses', value: 'Disabled for Local Addresses' }
|
|
||||||
];
|
|
||||||
|
|
||||||
const certificateValidationOptions = [
|
const certificateValidationOptions = [
|
||||||
{ key: 'enabled', value: 'Enabled' },
|
{ key: 'enabled', value: 'Enabled' },
|
||||||
{ key: 'disabledForLocalAddresses', value: 'Disabled for Local Addresses' },
|
{ key: 'disabledForLocalAddresses', value: 'Disabled for Local Addresses' },
|
||||||
@@ -76,7 +68,6 @@ class SecuritySettings extends Component {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
authenticationMethod,
|
authenticationMethod,
|
||||||
authenticationRequired,
|
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
apiKey,
|
apiKey,
|
||||||
@@ -95,31 +86,13 @@ class SecuritySettings extends Component {
|
|||||||
name="authenticationMethod"
|
name="authenticationMethod"
|
||||||
values={authenticationMethodOptions}
|
values={authenticationMethodOptions}
|
||||||
helpText={translate('AuthenticationMethodHelpText')}
|
helpText={translate('AuthenticationMethodHelpText')}
|
||||||
helpTextWarning={authenticationRequiredWarning}
|
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...authenticationMethod}
|
{...authenticationMethod}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
{
|
{
|
||||||
authenticationEnabled ?
|
authenticationEnabled &&
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('AuthenticationRequired')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.SELECT}
|
|
||||||
name="authenticationRequired"
|
|
||||||
values={authenticationRequiredOptions}
|
|
||||||
helpText={translate('AuthenticationRequiredHelpText')}
|
|
||||||
onChange={onInputChange}
|
|
||||||
{...authenticationRequired}
|
|
||||||
/>
|
|
||||||
</FormGroup> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
authenticationEnabled ?
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>{translate('Username')}</FormLabel>
|
<FormLabel>{translate('Username')}</FormLabel>
|
||||||
|
|
||||||
@@ -129,12 +102,11 @@ class SecuritySettings extends Component {
|
|||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...username}
|
{...username}
|
||||||
/>
|
/>
|
||||||
</FormGroup> :
|
</FormGroup>
|
||||||
null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
authenticationEnabled ?
|
authenticationEnabled &&
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>{translate('Password')}</FormLabel>
|
<FormLabel>{translate('Password')}</FormLabel>
|
||||||
|
|
||||||
@@ -144,8 +116,7 @@ class SecuritySettings extends Component {
|
|||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...password}
|
{...password}
|
||||||
/>
|
/>
|
||||||
</FormGroup> :
|
</FormGroup>
|
||||||
null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
|
|||||||
@@ -62,6 +62,8 @@ class UISettings extends Component {
|
|||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
const uiLanguages = languages.filter((item) => item.value !== 'Original');
|
||||||
|
|
||||||
const themeOptions = Object.keys(themes)
|
const themeOptions = Object.keys(themes)
|
||||||
.map((theme) => ({ key: theme, value: titleCase(theme) }));
|
.map((theme) => ({ key: theme, value: titleCase(theme) }));
|
||||||
|
|
||||||
@@ -170,7 +172,7 @@ class UISettings extends Component {
|
|||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.SELECT}
|
type={inputTypes.SELECT}
|
||||||
name="uiLanguage"
|
name="uiLanguage"
|
||||||
values={languages}
|
values={uiLanguages}
|
||||||
helpText={translate('UILanguageHelpText')}
|
helpText={translate('UILanguageHelpText')}
|
||||||
helpTextWarning={translate('UILanguageHelpTextWarning')}
|
helpTextWarning={translate('UILanguageHelpTextWarning')}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import React, { Component } from 'react';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||||
import { fetchLocalizationOptions } from 'Store/Actions/localizationActions';
|
|
||||||
import { fetchUISettings, saveUISettings, setUISettingsValue } from 'Store/Actions/settingsActions';
|
import { fetchUISettings, saveUISettings, setUISettingsValue } from 'Store/Actions/settingsActions';
|
||||||
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
|
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
|
||||||
import UISettings from './UISettings';
|
import UISettings from './UISettings';
|
||||||
@@ -12,17 +11,18 @@ const SECTION = 'ui';
|
|||||||
|
|
||||||
function createLanguagesSelector() {
|
function createLanguagesSelector() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state) => state.localization,
|
(state) => state.settings.languages,
|
||||||
(localization) => {
|
(languages) => {
|
||||||
const items = localization.items;
|
const items = languages.items;
|
||||||
|
const filterItems = ['Any', 'Unknown'];
|
||||||
|
|
||||||
if (!items) {
|
if (!items) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const newItems = items.filter((lang) => !items.includes(lang.name)).map((item) => {
|
const newItems = items.filter((lang) => !filterItems.includes(lang.name)).map((item) => {
|
||||||
return {
|
return {
|
||||||
key: item.value,
|
key: item.id,
|
||||||
value: item.name
|
value: item.name
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -51,7 +51,6 @@ const mapDispatchToProps = {
|
|||||||
setUISettingsValue,
|
setUISettingsValue,
|
||||||
saveUISettings,
|
saveUISettings,
|
||||||
fetchUISettings,
|
fetchUISettings,
|
||||||
fetchLocalizationOptions,
|
|
||||||
clearPendingChanges
|
clearPendingChanges
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -62,7 +61,6 @@ class UISettingsConnector extends Component {
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.fetchUISettings();
|
this.props.fetchUISettings();
|
||||||
this.props.fetchLocalizationOptions();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
@@ -98,7 +96,6 @@ UISettingsConnector.propTypes = {
|
|||||||
setUISettingsValue: PropTypes.func.isRequired,
|
setUISettingsValue: PropTypes.func.isRequired,
|
||||||
saveUISettings: PropTypes.func.isRequired,
|
saveUISettings: PropTypes.func.isRequired,
|
||||||
fetchUISettings: PropTypes.func.isRequired,
|
fetchUISettings: PropTypes.func.isRequired,
|
||||||
fetchLocalizationOptions: PropTypes.func.isRequired,
|
|
||||||
clearPendingChanges: PropTypes.func.isRequired
|
clearPendingChanges: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,169 +0,0 @@
|
|||||||
import { createAction } from 'redux-actions';
|
|
||||||
import { batchActions } from 'redux-batched-actions';
|
|
||||||
import createClearReducer from 'Store/Actions/Creators/Reducers/createClearReducer';
|
|
||||||
import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
|
|
||||||
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
|
||||||
import { createThunk } from 'Store/thunks';
|
|
||||||
import getNextId from 'Utilities/State/getNextId';
|
|
||||||
import getProviderState from 'Utilities/State/getProviderState';
|
|
||||||
import getSectionState from 'Utilities/State/getSectionState';
|
|
||||||
import selectProviderSchema from 'Utilities/State/selectProviderSchema';
|
|
||||||
import { removeItem, set, update, updateItem } from '../baseActions';
|
|
||||||
|
|
||||||
//
|
|
||||||
// Variables
|
|
||||||
|
|
||||||
const section = 'settings.downloadClientCategories';
|
|
||||||
|
|
||||||
//
|
|
||||||
// Actions Types
|
|
||||||
|
|
||||||
export const FETCH_DOWNLOAD_CLIENT_CATEGORIES = 'settings/downloadClientCategories/fetchDownloadClientCategories';
|
|
||||||
export const FETCH_DOWNLOAD_CLIENT_CATEGORY_SCHEMA = 'settings/downloadClientCategories/fetchDownloadClientCategorySchema';
|
|
||||||
export const SELECT_DOWNLOAD_CLIENT_CATEGORY_SCHEMA = 'settings/downloadClientCategories/selectDownloadClientCategorySchema';
|
|
||||||
export const SET_DOWNLOAD_CLIENT_CATEGORY_VALUE = 'settings/downloadClientCategories/setDownloadClientCategoryValue';
|
|
||||||
export const SET_DOWNLOAD_CLIENT_CATEGORY_FIELD_VALUE = 'settings/downloadClientCategories/setDownloadClientCategoryFieldValue';
|
|
||||||
export const SAVE_DOWNLOAD_CLIENT_CATEGORY = 'settings/downloadClientCategories/saveDownloadClientCategory';
|
|
||||||
export const DELETE_DOWNLOAD_CLIENT_CATEGORY = 'settings/downloadClientCategories/deleteDownloadClientCategory';
|
|
||||||
export const DELETE_ALL_DOWNLOAD_CLIENT_CATEGORY = 'settings/downloadClientCategories/deleteAllDownloadClientCategory';
|
|
||||||
export const CLEAR_DOWNLOAD_CLIENT_CATEGORIES = 'settings/downloadClientCategories/clearDownloadClientCategories';
|
|
||||||
export const CLEAR_DOWNLOAD_CLIENT_CATEGORY_PENDING = 'settings/downloadClientCategories/clearDownloadClientCategoryPending';
|
|
||||||
//
|
|
||||||
// Action Creators
|
|
||||||
|
|
||||||
export const fetchDownloadClientCategories = createThunk(FETCH_DOWNLOAD_CLIENT_CATEGORIES);
|
|
||||||
export const fetchDownloadClientCategorySchema = createThunk(FETCH_DOWNLOAD_CLIENT_CATEGORY_SCHEMA);
|
|
||||||
export const selectDownloadClientCategorySchema = createAction(SELECT_DOWNLOAD_CLIENT_CATEGORY_SCHEMA);
|
|
||||||
|
|
||||||
export const saveDownloadClientCategory = createThunk(SAVE_DOWNLOAD_CLIENT_CATEGORY);
|
|
||||||
export const deleteDownloadClientCategory = createThunk(DELETE_DOWNLOAD_CLIENT_CATEGORY);
|
|
||||||
export const deleteAllDownloadClientCategory = createThunk(DELETE_ALL_DOWNLOAD_CLIENT_CATEGORY);
|
|
||||||
|
|
||||||
export const setDownloadClientCategoryValue = createAction(SET_DOWNLOAD_CLIENT_CATEGORY_VALUE, (payload) => {
|
|
||||||
return {
|
|
||||||
section,
|
|
||||||
...payload
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
export const setDownloadClientCategoryFieldValue = createAction(SET_DOWNLOAD_CLIENT_CATEGORY_FIELD_VALUE, (payload) => {
|
|
||||||
return {
|
|
||||||
section,
|
|
||||||
...payload
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
export const clearDownloadClientCategory = createAction(CLEAR_DOWNLOAD_CLIENT_CATEGORIES);
|
|
||||||
|
|
||||||
export const clearDownloadClientCategoryPending = createThunk(CLEAR_DOWNLOAD_CLIENT_CATEGORY_PENDING);
|
|
||||||
|
|
||||||
//
|
|
||||||
// Details
|
|
||||||
|
|
||||||
export default {
|
|
||||||
|
|
||||||
//
|
|
||||||
// State
|
|
||||||
|
|
||||||
defaultState: {
|
|
||||||
isPopulated: false,
|
|
||||||
error: null,
|
|
||||||
isSchemaFetching: false,
|
|
||||||
isSchemaPopulated: false,
|
|
||||||
schemaError: null,
|
|
||||||
schema: [],
|
|
||||||
selectedSchema: {},
|
|
||||||
isSaving: false,
|
|
||||||
saveError: null,
|
|
||||||
items: [],
|
|
||||||
pendingChanges: {}
|
|
||||||
},
|
|
||||||
|
|
||||||
//
|
|
||||||
// Action Handlers
|
|
||||||
|
|
||||||
actionHandlers: {
|
|
||||||
[FETCH_DOWNLOAD_CLIENT_CATEGORIES]: (getState, payload, dispatch) => {
|
|
||||||
let tags = [];
|
|
||||||
if (payload.id) {
|
|
||||||
const cfState = getSectionState(getState(), 'settings.downloadClients', true);
|
|
||||||
const cf = cfState.items[cfState.itemMap[payload.id]];
|
|
||||||
tags = cf.categories.map((tag, i) => {
|
|
||||||
return {
|
|
||||||
id: i + 1,
|
|
||||||
...tag
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(batchActions([
|
|
||||||
update({ section, data: tags }),
|
|
||||||
set({
|
|
||||||
section,
|
|
||||||
isPopulated: true
|
|
||||||
})
|
|
||||||
]));
|
|
||||||
},
|
|
||||||
|
|
||||||
[SAVE_DOWNLOAD_CLIENT_CATEGORY]: (getState, payload, dispatch) => {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
...otherPayload
|
|
||||||
} = payload;
|
|
||||||
|
|
||||||
const saveData = getProviderState({ id, ...otherPayload }, getState, section, false);
|
|
||||||
|
|
||||||
// we have to set id since not actually posting to server yet
|
|
||||||
if (!saveData.id) {
|
|
||||||
saveData.id = getNextId(getState().settings.downloadClientCategories.items);
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(batchActions([
|
|
||||||
updateItem({ section, ...saveData }),
|
|
||||||
set({
|
|
||||||
section,
|
|
||||||
pendingChanges: {}
|
|
||||||
})
|
|
||||||
]));
|
|
||||||
},
|
|
||||||
|
|
||||||
[DELETE_DOWNLOAD_CLIENT_CATEGORY]: (getState, payload, dispatch) => {
|
|
||||||
const id = payload.id;
|
|
||||||
return dispatch(removeItem({ section, id }));
|
|
||||||
},
|
|
||||||
|
|
||||||
[DELETE_ALL_DOWNLOAD_CLIENT_CATEGORY]: (getState, payload, dispatch) => {
|
|
||||||
return dispatch(set({
|
|
||||||
section,
|
|
||||||
items: []
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
[CLEAR_DOWNLOAD_CLIENT_CATEGORY_PENDING]: (getState, payload, dispatch) => {
|
|
||||||
return dispatch(set({
|
|
||||||
section,
|
|
||||||
pendingChanges: {}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
//
|
|
||||||
// Reducers
|
|
||||||
|
|
||||||
reducers: {
|
|
||||||
[SET_DOWNLOAD_CLIENT_CATEGORY_VALUE]: createSetSettingValueReducer(section),
|
|
||||||
[SET_DOWNLOAD_CLIENT_CATEGORY_FIELD_VALUE]: createSetProviderFieldValueReducer(section),
|
|
||||||
|
|
||||||
[SELECT_DOWNLOAD_CLIENT_CATEGORY_SCHEMA]: (state, { payload }) => {
|
|
||||||
return selectProviderSchema(state, section, payload, (selectedSchema) => {
|
|
||||||
return selectedSchema;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
[CLEAR_DOWNLOAD_CLIENT_CATEGORIES]: createClearReducer(section, {
|
|
||||||
isPopulated: false,
|
|
||||||
error: null,
|
|
||||||
items: []
|
|
||||||
})
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -9,7 +9,6 @@ import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/
|
|||||||
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
||||||
import { createThunk } from 'Store/thunks';
|
import { createThunk } from 'Store/thunks';
|
||||||
import selectProviderSchema from 'Utilities/State/selectProviderSchema';
|
import selectProviderSchema from 'Utilities/State/selectProviderSchema';
|
||||||
import { set } from '../baseActions';
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Variables
|
// Variables
|
||||||
@@ -91,34 +90,10 @@ export default {
|
|||||||
[FETCH_DOWNLOAD_CLIENTS]: createFetchHandler(section, '/downloadclient'),
|
[FETCH_DOWNLOAD_CLIENTS]: createFetchHandler(section, '/downloadclient'),
|
||||||
[FETCH_DOWNLOAD_CLIENT_SCHEMA]: createFetchSchemaHandler(section, '/downloadclient/schema'),
|
[FETCH_DOWNLOAD_CLIENT_SCHEMA]: createFetchSchemaHandler(section, '/downloadclient/schema'),
|
||||||
|
|
||||||
[SAVE_DOWNLOAD_CLIENT]: (getState, payload, dispatch) => {
|
[SAVE_DOWNLOAD_CLIENT]: createSaveProviderHandler(section, '/downloadclient'),
|
||||||
// move the format tags in as a pending change
|
|
||||||
const state = getState();
|
|
||||||
const pendingChanges = state.settings.downloadClients.pendingChanges;
|
|
||||||
pendingChanges.categories = state.settings.downloadClientCategories.items;
|
|
||||||
dispatch(set({
|
|
||||||
section,
|
|
||||||
pendingChanges
|
|
||||||
}));
|
|
||||||
|
|
||||||
createSaveProviderHandler(section, '/downloadclient')(getState, payload, dispatch);
|
|
||||||
},
|
|
||||||
|
|
||||||
[CANCEL_SAVE_DOWNLOAD_CLIENT]: createCancelSaveProviderHandler(section),
|
[CANCEL_SAVE_DOWNLOAD_CLIENT]: createCancelSaveProviderHandler(section),
|
||||||
[DELETE_DOWNLOAD_CLIENT]: createRemoveItemHandler(section, '/downloadclient'),
|
[DELETE_DOWNLOAD_CLIENT]: createRemoveItemHandler(section, '/downloadclient'),
|
||||||
|
[TEST_DOWNLOAD_CLIENT]: createTestProviderHandler(section, '/downloadclient'),
|
||||||
[TEST_DOWNLOAD_CLIENT]: (getState, payload, dispatch) => {
|
|
||||||
const state = getState();
|
|
||||||
const pendingChanges = state.settings.downloadClients.pendingChanges;
|
|
||||||
pendingChanges.categories = state.settings.downloadClientCategories.items;
|
|
||||||
dispatch(set({
|
|
||||||
section,
|
|
||||||
pendingChanges
|
|
||||||
}));
|
|
||||||
|
|
||||||
createTestProviderHandler(section, '/downloadclient')(getState, payload, dispatch);
|
|
||||||
},
|
|
||||||
|
|
||||||
[CANCEL_TEST_DOWNLOAD_CLIENT]: createCancelTestProviderHandler(section),
|
[CANCEL_TEST_DOWNLOAD_CLIENT]: createCancelTestProviderHandler(section),
|
||||||
[TEST_ALL_DOWNLOAD_CLIENTS]: createTestAllProvidersHandler(section, '/downloadclient')
|
[TEST_ALL_DOWNLOAD_CLIENTS]: createTestAllProvidersHandler(section, '/downloadclient')
|
||||||
},
|
},
|
||||||
|
|||||||
48
frontend/src/Store/Actions/Settings/languages.js
Normal file
48
frontend/src/Store/Actions/Settings/languages.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
|
||||||
|
import { createThunk } from 'Store/thunks';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Variables
|
||||||
|
|
||||||
|
const section = 'settings.languages';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Actions Types
|
||||||
|
|
||||||
|
export const FETCH_LANGUAGES = 'settings/languages/fetchLanguages';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Action Creators
|
||||||
|
|
||||||
|
export const fetchLanguages = createThunk(FETCH_LANGUAGES);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Details
|
||||||
|
|
||||||
|
export default {
|
||||||
|
|
||||||
|
//
|
||||||
|
// State
|
||||||
|
|
||||||
|
defaultState: {
|
||||||
|
isFetching: false,
|
||||||
|
isPopulated: false,
|
||||||
|
error: null,
|
||||||
|
items: []
|
||||||
|
},
|
||||||
|
|
||||||
|
//
|
||||||
|
// Action Handlers
|
||||||
|
|
||||||
|
actionHandlers: {
|
||||||
|
[FETCH_LANGUAGES]: createFetchHandler(section, '/language')
|
||||||
|
},
|
||||||
|
|
||||||
|
//
|
||||||
|
// Reducers
|
||||||
|
|
||||||
|
reducers: {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
@@ -36,31 +36,31 @@ export const defaultState = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'indexer',
|
name: 'indexer',
|
||||||
label: translate('Indexer'),
|
label: 'Indexer',
|
||||||
isSortable: false,
|
isSortable: false,
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'query',
|
name: 'query',
|
||||||
label: translate('Query'),
|
label: 'Query',
|
||||||
isSortable: false,
|
isSortable: false,
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'parameters',
|
name: 'parameters',
|
||||||
label: translate('Parameters'),
|
label: 'Parameters',
|
||||||
isSortable: false,
|
isSortable: false,
|
||||||
isVisible: false
|
isVisible: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'grabTitle',
|
name: 'grabTitle',
|
||||||
label: translate('Grab Title'),
|
label: 'Grab Title',
|
||||||
isSortable: false,
|
isSortable: false,
|
||||||
isVisible: false
|
isVisible: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'categories',
|
name: 'categories',
|
||||||
label: translate('Categories'),
|
label: 'Categories',
|
||||||
isSortable: false,
|
isSortable: false,
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
@@ -72,13 +72,13 @@ export const defaultState = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'source',
|
name: 'source',
|
||||||
label: translate('Source'),
|
label: 'Source',
|
||||||
isSortable: false,
|
isSortable: false,
|
||||||
isVisible: false
|
isVisible: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'elapsedTime',
|
name: 'elapsedTime',
|
||||||
label: translate('Elapsed Time'),
|
label: 'Elapsed Time',
|
||||||
isSortable: false,
|
isSortable: false,
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import * as indexers from './indexerActions';
|
|||||||
import * as indexerIndex from './indexerIndexActions';
|
import * as indexerIndex from './indexerIndexActions';
|
||||||
import * as indexerStats from './indexerStatsActions';
|
import * as indexerStats from './indexerStatsActions';
|
||||||
import * as indexerStatus from './indexerStatusActions';
|
import * as indexerStatus from './indexerStatusActions';
|
||||||
import * as localization from './localizationActions';
|
|
||||||
import * as oAuth from './oAuthActions';
|
import * as oAuth from './oAuthActions';
|
||||||
import * as paths from './pathActions';
|
import * as paths from './pathActions';
|
||||||
import * as providerOptions from './providerOptionActions';
|
import * as providerOptions from './providerOptionActions';
|
||||||
@@ -26,7 +25,6 @@ export default [
|
|||||||
paths,
|
paths,
|
||||||
providerOptions,
|
providerOptions,
|
||||||
releases,
|
releases,
|
||||||
localization,
|
|
||||||
indexers,
|
indexers,
|
||||||
indexerIndex,
|
indexerIndex,
|
||||||
indexerStats,
|
indexerStats,
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export const defaultState = {
|
|||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
name: 'select',
|
name: 'select',
|
||||||
columnLabel: translate('Select'),
|
columnLabel: 'Select',
|
||||||
isSortable: false,
|
isSortable: false,
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
isModifiable: false,
|
isModifiable: false,
|
||||||
@@ -51,7 +51,7 @@ export const defaultState = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'sortName',
|
name: 'sortName',
|
||||||
label: translate('IndexerName'),
|
label: 'Indexer Name',
|
||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
isModifiable: false
|
isModifiable: false
|
||||||
@@ -88,7 +88,7 @@ export const defaultState = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'capabilities',
|
name: 'capabilities',
|
||||||
label: translate('Categories'),
|
label: 'Categories',
|
||||||
isSortable: false,
|
isSortable: false,
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
|
|
||||||
import { createThunk, handleThunks } from 'Store/thunks';
|
|
||||||
import createHandleActions from './Creators/createHandleActions';
|
|
||||||
|
|
||||||
//
|
|
||||||
// Variables
|
|
||||||
|
|
||||||
export const section = 'localization';
|
|
||||||
|
|
||||||
//
|
|
||||||
// State
|
|
||||||
|
|
||||||
export const defaultState = {
|
|
||||||
isFetching: false,
|
|
||||||
isPopulated: false,
|
|
||||||
error: null,
|
|
||||||
items: []
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Actions Types
|
|
||||||
|
|
||||||
export const FETCH_LOCALIZATION_OPTIONS = 'localization/fetchLocalizationOptions';
|
|
||||||
|
|
||||||
//
|
|
||||||
// Action Creators
|
|
||||||
|
|
||||||
export const fetchLocalizationOptions = createThunk(FETCH_LOCALIZATION_OPTIONS);
|
|
||||||
|
|
||||||
//
|
|
||||||
// Action Handlers
|
|
||||||
export const actionHandlers = handleThunks({
|
|
||||||
|
|
||||||
[FETCH_LOCALIZATION_OPTIONS]: createFetchHandler(section, '/localization/options')
|
|
||||||
});
|
|
||||||
|
|
||||||
//
|
|
||||||
// Reducers
|
|
||||||
export const reducers = createHandleActions({}, defaultState, section);
|
|
||||||
@@ -310,6 +310,8 @@ export const actionHandlers = handleThunks({
|
|||||||
isGrabbing: true
|
isGrabbing: true
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
console.log(payload);
|
||||||
|
|
||||||
const promise = createAjaxRequest({
|
const promise = createAjaxRequest({
|
||||||
url: '/search/bulk',
|
url: '/search/bulk',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|||||||
@@ -4,19 +4,19 @@ import createHandleActions from './Creators/createHandleActions';
|
|||||||
import applications from './Settings/applications';
|
import applications from './Settings/applications';
|
||||||
import appProfiles from './Settings/appProfiles';
|
import appProfiles from './Settings/appProfiles';
|
||||||
import development from './Settings/development';
|
import development from './Settings/development';
|
||||||
import downloadClientCategories from './Settings/downloadClientCategories';
|
|
||||||
import downloadClients from './Settings/downloadClients';
|
import downloadClients from './Settings/downloadClients';
|
||||||
import general from './Settings/general';
|
import general from './Settings/general';
|
||||||
import indexerCategories from './Settings/indexerCategories';
|
import indexerCategories from './Settings/indexerCategories';
|
||||||
import indexerProxies from './Settings/indexerProxies';
|
import indexerProxies from './Settings/indexerProxies';
|
||||||
|
import languages from './Settings/languages';
|
||||||
import notifications from './Settings/notifications';
|
import notifications from './Settings/notifications';
|
||||||
import ui from './Settings/ui';
|
import ui from './Settings/ui';
|
||||||
|
|
||||||
export * from './Settings/downloadClientCategories';
|
|
||||||
export * from './Settings/downloadClients';
|
export * from './Settings/downloadClients';
|
||||||
export * from './Settings/general';
|
export * from './Settings/general';
|
||||||
export * from './Settings/indexerCategories';
|
export * from './Settings/indexerCategories';
|
||||||
export * from './Settings/indexerProxies';
|
export * from './Settings/indexerProxies';
|
||||||
|
export * from './Settings/languages';
|
||||||
export * from './Settings/notifications';
|
export * from './Settings/notifications';
|
||||||
export * from './Settings/applications';
|
export * from './Settings/applications';
|
||||||
export * from './Settings/appProfiles';
|
export * from './Settings/appProfiles';
|
||||||
@@ -34,11 +34,11 @@ export const section = 'settings';
|
|||||||
export const defaultState = {
|
export const defaultState = {
|
||||||
advancedSettings: false,
|
advancedSettings: false,
|
||||||
|
|
||||||
downloadClientCategories: downloadClientCategories.defaultState,
|
|
||||||
downloadClients: downloadClients.defaultState,
|
downloadClients: downloadClients.defaultState,
|
||||||
general: general.defaultState,
|
general: general.defaultState,
|
||||||
indexerCategories: indexerCategories.defaultState,
|
indexerCategories: indexerCategories.defaultState,
|
||||||
indexerProxies: indexerProxies.defaultState,
|
indexerProxies: indexerProxies.defaultState,
|
||||||
|
languages: languages.defaultState,
|
||||||
notifications: notifications.defaultState,
|
notifications: notifications.defaultState,
|
||||||
applications: applications.defaultState,
|
applications: applications.defaultState,
|
||||||
appProfiles: appProfiles.defaultState,
|
appProfiles: appProfiles.defaultState,
|
||||||
@@ -64,11 +64,11 @@ export const toggleAdvancedSettings = createAction(TOGGLE_ADVANCED_SETTINGS);
|
|||||||
// Action Handlers
|
// Action Handlers
|
||||||
|
|
||||||
export const actionHandlers = handleThunks({
|
export const actionHandlers = handleThunks({
|
||||||
...downloadClientCategories.actionHandlers,
|
|
||||||
...downloadClients.actionHandlers,
|
...downloadClients.actionHandlers,
|
||||||
...general.actionHandlers,
|
...general.actionHandlers,
|
||||||
...indexerCategories.actionHandlers,
|
...indexerCategories.actionHandlers,
|
||||||
...indexerProxies.actionHandlers,
|
...indexerProxies.actionHandlers,
|
||||||
|
...languages.actionHandlers,
|
||||||
...notifications.actionHandlers,
|
...notifications.actionHandlers,
|
||||||
...applications.actionHandlers,
|
...applications.actionHandlers,
|
||||||
...appProfiles.actionHandlers,
|
...appProfiles.actionHandlers,
|
||||||
@@ -85,11 +85,11 @@ export const reducers = createHandleActions({
|
|||||||
return Object.assign({}, state, { advancedSettings: !state.advancedSettings });
|
return Object.assign({}, state, { advancedSettings: !state.advancedSettings });
|
||||||
},
|
},
|
||||||
|
|
||||||
...downloadClientCategories.reducers,
|
|
||||||
...downloadClients.reducers,
|
...downloadClients.reducers,
|
||||||
...general.reducers,
|
...general.reducers,
|
||||||
...indexerCategories.reducers,
|
...indexerCategories.reducers,
|
||||||
...indexerProxies.reducers,
|
...indexerProxies.reducers,
|
||||||
|
...languages.reducers,
|
||||||
...notifications.reducers,
|
...notifications.reducers,
|
||||||
...applications.reducers,
|
...applications.reducers,
|
||||||
...appProfiles.reducers,
|
...appProfiles.reducers,
|
||||||
|
|||||||
@@ -168,11 +168,10 @@ module.exports = {
|
|||||||
//
|
//
|
||||||
// Popover
|
// Popover
|
||||||
|
|
||||||
popoverTitleBackgroundColor: '#424242',
|
popoverTitleBackgroundColor: '#f7f7f7',
|
||||||
popoverTitleBorderColor: '#2a2a2a',
|
popoverTitleBorderColor: '#ebebeb',
|
||||||
popoverBodyBackgroundColor: '#2a2a2a',
|
|
||||||
popoverShadowColor: 'rgba(0, 0, 0, 0.2)',
|
popoverShadowColor: 'rgba(0, 0, 0, 0.2)',
|
||||||
popoverArrowBorderColor: '#2a2a2a',
|
popoverArrowBorderColor: '#fff',
|
||||||
|
|
||||||
popoverTitleBackgroundInverseColor: '#595959',
|
popoverTitleBackgroundInverseColor: '#595959',
|
||||||
popoverTitleBorderInverseColor: '#707070',
|
popoverTitleBorderInverseColor: '#707070',
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
import * as dark from './dark';
|
import * as dark from './dark';
|
||||||
import * as light from './light';
|
import * as light from './light';
|
||||||
|
|
||||||
const defaultDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
||||||
const auto = defaultDark ? { ...dark } : { ...light };
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
auto,
|
|
||||||
light,
|
light,
|
||||||
dark
|
dark
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -170,7 +170,6 @@ module.exports = {
|
|||||||
|
|
||||||
popoverTitleBackgroundColor: '#f7f7f7',
|
popoverTitleBackgroundColor: '#f7f7f7',
|
||||||
popoverTitleBorderColor: '#ebebeb',
|
popoverTitleBorderColor: '#ebebeb',
|
||||||
popoverBodyBackgroundColor: '#e9e9e9',
|
|
||||||
popoverShadowColor: 'rgba(0, 0, 0, 0.2)',
|
popoverShadowColor: 'rgba(0, 0, 0, 0.2)',
|
||||||
popoverArrowBorderColor: '#fff',
|
popoverArrowBorderColor: '#fff',
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class Donations extends Component {
|
|||||||
return (
|
return (
|
||||||
<FieldSet legend={translate('Donations')}>
|
<FieldSet legend={translate('Donations')}>
|
||||||
<div className={styles.logoContainer} title="Radarr">
|
<div className={styles.logoContainer} title="Radarr">
|
||||||
<Link to="https://radarr.video/donate">
|
<Link to="https://opencollective.com/radarr">
|
||||||
<img
|
<img
|
||||||
className={styles.logo}
|
className={styles.logo}
|
||||||
src={`${window.Prowlarr.urlBase}/Content/Images/Icons/logo-radarr.png`}
|
src={`${window.Prowlarr.urlBase}/Content/Images/Icons/logo-radarr.png`}
|
||||||
@@ -21,7 +21,7 @@ class Donations extends Component {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.logoContainer} title="Lidarr">
|
<div className={styles.logoContainer} title="Lidarr">
|
||||||
<Link to="https://lidarr.audio/donate">
|
<Link to="https://opencollective.com/lidarr">
|
||||||
<img
|
<img
|
||||||
className={styles.logo}
|
className={styles.logo}
|
||||||
src={`${window.Prowlarr.urlBase}/Content/Images/Icons/logo-lidarr.png`}
|
src={`${window.Prowlarr.urlBase}/Content/Images/Icons/logo-lidarr.png`}
|
||||||
@@ -29,7 +29,7 @@ class Donations extends Component {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.logoContainer} title="Readarr">
|
<div className={styles.logoContainer} title="Readarr">
|
||||||
<Link to="https://readarr.com/donate">
|
<Link to="https://opencollective.com/readarr">
|
||||||
<img
|
<img
|
||||||
className={styles.logo}
|
className={styles.logo}
|
||||||
src={`${window.Prowlarr.urlBase}/Content/Images/Icons/logo-readarr.png`}
|
src={`${window.Prowlarr.urlBase}/Content/Images/Icons/logo-readarr.png`}
|
||||||
@@ -37,7 +37,7 @@ class Donations extends Component {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.logoContainer} title="Prowlarr">
|
<div className={styles.logoContainer} title="Prowlarr">
|
||||||
<Link to="https://prowlarr.com/donate">
|
<Link to="https://opencollective.com/prowlarr">
|
||||||
<img
|
<img
|
||||||
className={styles.logo}
|
className={styles.logo}
|
||||||
src={`${window.Prowlarr.urlBase}/Content/Images/Icons/logo-prowlarr.png`}
|
src={`${window.Prowlarr.urlBase}/Content/Images/Icons/logo-prowlarr.png`}
|
||||||
|
|||||||
@@ -15,27 +15,27 @@ const columns = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'commandName',
|
name: 'commandName',
|
||||||
label: translate('Name'),
|
label: 'Name',
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'queued',
|
name: 'queued',
|
||||||
label: translate('Queued'),
|
label: 'Queued',
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'started',
|
name: 'started',
|
||||||
label: translate('Started'),
|
label: 'Started',
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'ended',
|
name: 'ended',
|
||||||
label: translate('Ended'),
|
label: 'Ended',
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'duration',
|
name: 'duration',
|
||||||
label: translate('Duration'),
|
label: 'Duration',
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,27 +10,27 @@ import ScheduledTaskRowConnector from './ScheduledTaskRowConnector';
|
|||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
name: 'name',
|
name: 'name',
|
||||||
label: translate('Name'),
|
label: 'Name',
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'interval',
|
name: 'interval',
|
||||||
label: translate('Interval'),
|
label: 'Interval',
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'lastExecution',
|
name: 'lastExecution',
|
||||||
label: translate('LastExecution'),
|
label: 'Last Execution',
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'lastDuration',
|
name: 'lastDuration',
|
||||||
label: translate('LastDuration'),
|
label: 'Last Duration',
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'nextExecution',
|
name: 'nextExecution',
|
||||||
label: translate('NextExecution'),
|
label: 'Next Execution',
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { filesize } from 'filesize';
|
import filesize from 'filesize';
|
||||||
|
|
||||||
function formatBytes(input) {
|
function formatBytes(input) {
|
||||||
const size = Number(input);
|
const size = Number(input);
|
||||||
|
|||||||
111
package.json
111
package.json
@@ -25,108 +25,107 @@
|
|||||||
"not chrome < 60"
|
"not chrome < 60"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "6.2.1",
|
"@fortawesome/fontawesome-free": "6.1.1",
|
||||||
"@fortawesome/fontawesome-svg-core": "6.2.1",
|
"@fortawesome/fontawesome-svg-core": "6.1.1",
|
||||||
"@fortawesome/free-regular-svg-icons": "6.2.1",
|
"@fortawesome/free-regular-svg-icons": "6.1.1",
|
||||||
"@fortawesome/free-solid-svg-icons": "6.2.1",
|
"@fortawesome/free-solid-svg-icons": "6.1.1",
|
||||||
"@fortawesome/react-fontawesome": "0.2.0",
|
"@fortawesome/react-fontawesome": "0.1.18",
|
||||||
"@microsoft/signalr": "6.0.11",
|
"@microsoft/signalr": "6.0.3",
|
||||||
"@sentry/browser": "7.28.0",
|
"@sentry/browser": "6.19.2",
|
||||||
"@sentry/integrations": "7.28.0",
|
"@sentry/integrations": "6.19.2",
|
||||||
"chart.js": "4.1.1",
|
"chart.js": "3.7.1",
|
||||||
"classnames": "2.3.2",
|
"classnames": "2.3.1",
|
||||||
"clipboard": "2.0.11",
|
"clipboard": "2.0.10",
|
||||||
"connected-react-router": "6.9.3",
|
"connected-react-router": "6.9.1",
|
||||||
"element-class": "0.2.2",
|
"element-class": "0.2.2",
|
||||||
"filesize": "10.0.6",
|
"filesize": "6.3.0",
|
||||||
"history": "4.10.1",
|
"history": "4.10.1",
|
||||||
"https-browserify": "1.0.0",
|
"https-browserify": "1.0.0",
|
||||||
"jdu": "1.0.0",
|
"jdu": "1.0.0",
|
||||||
"jquery": "3.6.2",
|
"jquery": "3.6.0",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"mobile-detect": "1.4.5",
|
"mobile-detect": "1.4.5",
|
||||||
"moment": "2.29.4",
|
"moment": "2.29.2",
|
||||||
"mousetrap": "1.6.5",
|
"mousetrap": "1.6.5",
|
||||||
"normalize.css": "8.0.1",
|
"normalize.css": "8.0.1",
|
||||||
"prop-types": "15.8.1",
|
"prop-types": "15.8.1",
|
||||||
"qs": "6.11.0",
|
"qs": "6.10.3",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-addons-shallow-compare": "15.6.3",
|
"react-addons-shallow-compare": "15.6.3",
|
||||||
"react-async-script": "1.2.0",
|
"react-async-script": "1.2.0",
|
||||||
"react-autosuggest": "10.1.0",
|
"react-autosuggest": "10.1.0",
|
||||||
"react-custom-scrollbars-2": "4.5.0",
|
"react-custom-scrollbars-2": "4.4.0",
|
||||||
"react-dnd": "14.0.4",
|
"react-dnd": "14.0.4",
|
||||||
"react-dnd-html5-backend": "14.0.2",
|
"react-dnd-html5-backend": "14.0.2",
|
||||||
"react-dnd-multi-backend": "6.0.2",
|
"react-dnd-multi-backend": "6.0.2",
|
||||||
"react-dnd-touch-backend": "14.1.1",
|
"react-dnd-touch-backend": "14.1.1",
|
||||||
"react-document-title": "2.0.3",
|
"react-document-title": "2.0.3",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
"react-focus-lock": "2.9.2",
|
"react-focus-lock": "2.5.0",
|
||||||
"react-google-recaptcha": "2.1.0",
|
"react-google-recaptcha": "2.1.0",
|
||||||
"react-lazyload": "3.2.0",
|
"react-lazyload": "3.2.0",
|
||||||
"react-measure": "1.4.7",
|
"react-measure": "1.4.7",
|
||||||
"react-popper": "1.3.7",
|
"react-popper": "1.3.7",
|
||||||
"react-redux": "8.0.5",
|
"react-redux": "7.2.4",
|
||||||
"react-router": "5.2.0",
|
"react-router": "5.2.0",
|
||||||
"react-router-dom": "5.2.0",
|
"react-router-dom": "5.2.0",
|
||||||
"react-text-truncate": "0.19.0",
|
|
||||||
"react-virtualized": "9.21.1",
|
"react-virtualized": "9.21.1",
|
||||||
"redux": "4.2.0",
|
"redux": "4.1.0",
|
||||||
"redux-actions": "2.6.5",
|
"redux-actions": "2.6.5",
|
||||||
"redux-batched-actions": "0.5.0",
|
"redux-batched-actions": "0.5.0",
|
||||||
"redux-localstorage": "0.4.1",
|
"redux-localstorage": "0.4.1",
|
||||||
"redux-thunk": "2.4.2",
|
"redux-thunk": "2.3.0",
|
||||||
"reselect": "4.1.7"
|
"reselect": "4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.20.5",
|
"@babel/core": "7.17.8",
|
||||||
"@babel/eslint-parser": "7.19.1",
|
"@babel/eslint-parser": "7.17.0",
|
||||||
"@babel/plugin-proposal-class-properties": "7.18.6",
|
"@babel/plugin-proposal-class-properties": "7.16.7",
|
||||||
"@babel/plugin-proposal-decorators": "7.20.5",
|
"@babel/plugin-proposal-decorators": "7.17.8",
|
||||||
"@babel/plugin-proposal-export-default-from": "7.18.10",
|
"@babel/plugin-proposal-export-default-from": "7.16.7",
|
||||||
"@babel/plugin-proposal-export-namespace-from": "7.18.9",
|
"@babel/plugin-proposal-export-namespace-from": "7.16.7",
|
||||||
"@babel/plugin-proposal-function-sent": "7.18.6",
|
"@babel/plugin-proposal-function-sent": "7.16.7",
|
||||||
"@babel/plugin-proposal-nullish-coalescing-operator": "7.18.6",
|
"@babel/plugin-proposal-nullish-coalescing-operator": "7.16.7",
|
||||||
"@babel/plugin-proposal-numeric-separator": "7.18.6",
|
"@babel/plugin-proposal-numeric-separator": "7.16.7",
|
||||||
"@babel/plugin-proposal-optional-chaining": "7.18.9",
|
"@babel/plugin-proposal-optional-chaining": "7.16.7",
|
||||||
"@babel/plugin-proposal-throw-expressions": "7.18.6",
|
"@babel/plugin-proposal-throw-expressions": "7.16.7",
|
||||||
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
||||||
"@babel/preset-env": "7.20.2",
|
"@babel/preset-env": "7.16.11",
|
||||||
"@babel/preset-react": "7.18.6",
|
"@babel/preset-react": "7.16.7",
|
||||||
"autoprefixer": "10.4.13",
|
"autoprefixer": "10.4.4",
|
||||||
"babel-loader": "9.1.0",
|
"babel-loader": "8.2.4",
|
||||||
"babel-plugin-inline-classnames": "2.0.1",
|
"babel-plugin-inline-classnames": "2.0.1",
|
||||||
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
|
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
|
||||||
"core-js": "3.26.1",
|
"core-js": "3.21.1",
|
||||||
"css-loader": "6.7.3",
|
"css-loader": "6.7.1",
|
||||||
"eslint": "8.30.0",
|
"eslint": "8.11.0",
|
||||||
"eslint-plugin-filenames": "1.3.2",
|
"eslint-plugin-filenames": "1.3.2",
|
||||||
"eslint-plugin-import": "2.26.0",
|
"eslint-plugin-import": "2.25.4",
|
||||||
"eslint-plugin-react": "7.31.11",
|
"eslint-plugin-react": "7.29.4",
|
||||||
"eslint-plugin-simple-import-sort": "8.0.0",
|
"eslint-plugin-simple-import-sort": "7.0.0",
|
||||||
"esprint": "3.6.0",
|
"esprint": "3.3.0",
|
||||||
"file-loader": "6.2.0",
|
"file-loader": "6.2.0",
|
||||||
"filemanager-webpack-plugin": "8.0.0",
|
"filemanager-webpack-plugin": "6.1.7",
|
||||||
"html-webpack-plugin": "5.5.0",
|
"html-webpack-plugin": "5.5.0",
|
||||||
"loader-utils": "^3.2.1",
|
"loader-utils": "^3.0.0",
|
||||||
"mini-css-extract-plugin": "2.7.2",
|
"mini-css-extract-plugin": "2.6.0",
|
||||||
"postcss": "8.4.20",
|
"postcss": "8.4.12",
|
||||||
"postcss-color-function": "4.1.0",
|
"postcss-color-function": "4.1.0",
|
||||||
"postcss-loader": "7.0.2",
|
"postcss-loader": "6.2.1",
|
||||||
"postcss-mixins": "9.0.4",
|
"postcss-mixins": "9.0.2",
|
||||||
"postcss-nested": "6.0.0",
|
"postcss-nested": "5.0.6",
|
||||||
"postcss-simple-vars": "7.0.1",
|
"postcss-simple-vars": "6.0.3",
|
||||||
"postcss-url": "10.1.3",
|
"postcss-url": "10.1.3",
|
||||||
"require-nocache": "1.0.0",
|
"require-nocache": "1.0.0",
|
||||||
"rimraf": "3.0.2",
|
"rimraf": "3.0.2",
|
||||||
"run-sequence": "2.2.1",
|
"run-sequence": "2.2.1",
|
||||||
"streamqueue": "1.1.2",
|
"streamqueue": "1.1.2",
|
||||||
"style-loader": "3.3.1",
|
"style-loader": "3.3.1",
|
||||||
"stylelint": "14.16.0",
|
"stylelint": "14.6.0",
|
||||||
"stylelint-order": "5.0.0",
|
"stylelint-order": "5.0.0",
|
||||||
"url-loader": "4.1.1",
|
"url-loader": "4.1.1",
|
||||||
"webpack": "5.75.0",
|
"webpack": "5.70.0",
|
||||||
"webpack-cli": "5.0.1",
|
"webpack-cli": "4.9.2",
|
||||||
"webpack-livereload-plugin": "3.0.2"
|
"webpack-livereload-plugin": "3.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,7 +94,7 @@
|
|||||||
|
|
||||||
<!-- Standard testing packages -->
|
<!-- Standard testing packages -->
|
||||||
<ItemGroup Condition="'$(TestProject)'=='true'">
|
<ItemGroup Condition="'$(TestProject)'=='true'">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
|
||||||
<PackageReference Include="NunitXml.TestLogger" Version="3.0.117" />
|
<PackageReference Include="NunitXml.TestLogger" Version="3.0.117" />
|
||||||
|
|||||||
@@ -44,9 +44,9 @@ namespace NzbDrone.Automation.Test
|
|||||||
|
|
||||||
driver.Manage().Window.Size = new System.Drawing.Size(1920, 1080);
|
driver.Manage().Window.Size = new System.Drawing.Size(1920, 1080);
|
||||||
|
|
||||||
_runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger(), null);
|
_runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger());
|
||||||
_runner.KillAll();
|
_runner.KillAll();
|
||||||
_runner.Start(true);
|
_runner.Start();
|
||||||
|
|
||||||
driver.Url = "http://localhost:9696";
|
driver.Url = "http://localhost:9696";
|
||||||
|
|
||||||
|
|||||||
@@ -10,16 +10,6 @@ namespace NzbDrone.Common.Test.DiskTests
|
|||||||
public abstract class DiskProviderFixtureBase<TSubject> : TestBase<TSubject>
|
public abstract class DiskProviderFixtureBase<TSubject> : TestBase<TSubject>
|
||||||
where TSubject : class, IDiskProvider
|
where TSubject : class, IDiskProvider
|
||||||
{
|
{
|
||||||
[Test]
|
|
||||||
public void writealltext_should_truncate_existing()
|
|
||||||
{
|
|
||||||
var file = GetTempFilePath();
|
|
||||||
|
|
||||||
Subject.WriteAllText(file, "A pretty long string");
|
|
||||||
Subject.WriteAllText(file, "A short string");
|
|
||||||
Subject.ReadAllText(file).Should().Be("A short string");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
[Retry(5)]
|
[Retry(5)]
|
||||||
public void directory_exist_should_be_able_to_find_existing_folder()
|
public void directory_exist_should_be_able_to_find_existing_folder()
|
||||||
|
|||||||
@@ -402,40 +402,6 @@ namespace NzbDrone.Common.Test.DiskTests
|
|||||||
VerifyCopyFolder(source.FullName, destination.FullName);
|
VerifyCopyFolder(source.FullName, destination.FullName);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void CopyFolder_should_detect_caseinsensitive_parents()
|
|
||||||
{
|
|
||||||
WindowsOnly();
|
|
||||||
|
|
||||||
WithRealDiskProvider();
|
|
||||||
|
|
||||||
var original = GetFilledTempFolder();
|
|
||||||
var root = new DirectoryInfo(GetTempFilePath());
|
|
||||||
var source = new DirectoryInfo(root.FullName + "A/series");
|
|
||||||
var destination = new DirectoryInfo(root.FullName + "a/series");
|
|
||||||
|
|
||||||
Subject.TransferFolder(original.FullName, source.FullName, TransferMode.Copy);
|
|
||||||
|
|
||||||
Assert.Throws<IOException>(() => Subject.TransferFolder(source.FullName, destination.FullName, TransferMode.Copy));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void CopyFolder_should_detect_caseinsensitive_folder()
|
|
||||||
{
|
|
||||||
WindowsOnly();
|
|
||||||
|
|
||||||
WithRealDiskProvider();
|
|
||||||
|
|
||||||
var original = GetFilledTempFolder();
|
|
||||||
var root = new DirectoryInfo(GetTempFilePath());
|
|
||||||
var source = new DirectoryInfo(root.FullName + "A/series");
|
|
||||||
var destination = new DirectoryInfo(root.FullName + "A/Series");
|
|
||||||
|
|
||||||
Subject.TransferFolder(original.FullName, source.FullName, TransferMode.Copy);
|
|
||||||
|
|
||||||
Assert.Throws<IOException>(() => Subject.TransferFolder(source.FullName, destination.FullName, TransferMode.Copy));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void CopyFolder_should_ignore_nfs_temp_file()
|
public void CopyFolder_should_ignore_nfs_temp_file()
|
||||||
{
|
{
|
||||||
@@ -485,42 +451,6 @@ namespace NzbDrone.Common.Test.DiskTests
|
|||||||
VerifyMoveFolder(original.FullName, source.FullName, destination.FullName);
|
VerifyMoveFolder(original.FullName, source.FullName, destination.FullName);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void MoveFolder_should_detect_caseinsensitive_parents()
|
|
||||||
{
|
|
||||||
WindowsOnly();
|
|
||||||
|
|
||||||
WithRealDiskProvider();
|
|
||||||
|
|
||||||
var original = GetFilledTempFolder();
|
|
||||||
var root = new DirectoryInfo(GetTempFilePath());
|
|
||||||
var source = new DirectoryInfo(root.FullName + "A/series");
|
|
||||||
var destination = new DirectoryInfo(root.FullName + "a/series");
|
|
||||||
|
|
||||||
Subject.TransferFolder(original.FullName, source.FullName, TransferMode.Copy);
|
|
||||||
|
|
||||||
Assert.Throws<IOException>(() => Subject.TransferFolder(source.FullName, destination.FullName, TransferMode.Move));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void MoveFolder_should_rename_caseinsensitive_folder()
|
|
||||||
{
|
|
||||||
WindowsOnly();
|
|
||||||
|
|
||||||
WithRealDiskProvider();
|
|
||||||
|
|
||||||
var original = GetFilledTempFolder();
|
|
||||||
var root = new DirectoryInfo(GetTempFilePath());
|
|
||||||
var source = new DirectoryInfo(root.FullName + "A/series");
|
|
||||||
var destination = new DirectoryInfo(root.FullName + "A/Series");
|
|
||||||
|
|
||||||
Subject.TransferFolder(original.FullName, source.FullName, TransferMode.Copy);
|
|
||||||
|
|
||||||
Subject.TransferFolder(source.FullName, destination.FullName, TransferMode.Move);
|
|
||||||
|
|
||||||
source.FullName.GetActualCasing().Should().Be(destination.FullName);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void should_throw_if_destination_is_readonly()
|
public void should_throw_if_destination_is_readonly()
|
||||||
{
|
{
|
||||||
@@ -623,23 +553,6 @@ namespace NzbDrone.Common.Test.DiskTests
|
|||||||
VerifyCopyFolder(original.FullName, destination.FullName);
|
VerifyCopyFolder(original.FullName, destination.FullName);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void MirrorFolder_should_handle_trailing_slash()
|
|
||||||
{
|
|
||||||
WithRealDiskProvider();
|
|
||||||
|
|
||||||
var original = GetFilledTempFolder();
|
|
||||||
var source = new DirectoryInfo(GetTempFilePath());
|
|
||||||
var destination = new DirectoryInfo(GetTempFilePath());
|
|
||||||
|
|
||||||
Subject.TransferFolder(original.FullName, source.FullName, TransferMode.Copy);
|
|
||||||
|
|
||||||
var count = Subject.MirrorFolder(source.FullName + Path.DirectorySeparatorChar, destination.FullName);
|
|
||||||
|
|
||||||
count.Should().Equals(3);
|
|
||||||
VerifyCopyFolder(original.FullName, destination.FullName);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TransferFolder_should_use_movefolder_if_on_same_mount()
|
public void TransferFolder_should_use_movefolder_if_on_same_mount()
|
||||||
{
|
{
|
||||||
@@ -839,10 +752,6 @@ namespace NzbDrone.Common.Test.DiskTests
|
|||||||
.Setup(v => v.CreateFolder(It.IsAny<string>()))
|
.Setup(v => v.CreateFolder(It.IsAny<string>()))
|
||||||
.Callback<string>(v => Directory.CreateDirectory(v));
|
.Callback<string>(v => Directory.CreateDirectory(v));
|
||||||
|
|
||||||
Mocker.GetMock<IDiskProvider>()
|
|
||||||
.Setup(v => v.MoveFolder(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<bool>()))
|
|
||||||
.Callback<string, string, bool>((v, r, b) => Directory.Move(v, r));
|
|
||||||
|
|
||||||
Mocker.GetMock<IDiskProvider>()
|
Mocker.GetMock<IDiskProvider>()
|
||||||
.Setup(v => v.DeleteFolder(It.IsAny<string>(), It.IsAny<bool>()))
|
.Setup(v => v.DeleteFolder(It.IsAny<string>(), It.IsAny<bool>()))
|
||||||
.Callback<string, bool>((v, r) => Directory.Delete(v, r));
|
.Callback<string, bool>((v, r) => Directory.Delete(v, r));
|
||||||
|
|||||||
@@ -28,6 +28,14 @@ namespace NzbDrone.Common.Test.DiskTests
|
|||||||
Subject.GetAvailableSpace(Path.Combine(path, "invalidFolder")).Should().NotBe(0);
|
Subject.GetAvailableSpace(Path.Combine(path, "invalidFolder")).Should().NotBe(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Ignore("Docker")]
|
||||||
|
[Test]
|
||||||
|
public void should_be_able_to_check_space_on_ramdrive()
|
||||||
|
{
|
||||||
|
PosixOnly();
|
||||||
|
Subject.GetAvailableSpace("/run/").Should().NotBe(0);
|
||||||
|
}
|
||||||
|
|
||||||
[Ignore("Docker")]
|
[Ignore("Docker")]
|
||||||
[Test]
|
[Test]
|
||||||
public void should_return_free_disk_space()
|
public void should_return_free_disk_space()
|
||||||
@@ -36,6 +44,35 @@ namespace NzbDrone.Common.Test.DiskTests
|
|||||||
result.Should().BeGreaterThan(0);
|
result.Should().BeGreaterThan(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_be_able_to_get_space_on_unc()
|
||||||
|
{
|
||||||
|
WindowsOnly();
|
||||||
|
|
||||||
|
var result = Subject.GetAvailableSpace(@"\\localhost\c$\Windows");
|
||||||
|
result.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_throw_if_drive_doesnt_exist()
|
||||||
|
{
|
||||||
|
WindowsOnly();
|
||||||
|
|
||||||
|
// Find a drive that doesn't exist.
|
||||||
|
for (char driveletter = 'Z'; driveletter > 'D'; driveletter--)
|
||||||
|
{
|
||||||
|
if (new DriveInfo(driveletter.ToString()).IsReady)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Throws<DirectoryNotFoundException>(() => Subject.GetAvailableSpace(driveletter + @":\NOT_A_REAL_PATH\DOES_NOT_EXIST".AsOsAgnostic()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Inconclusive("No drive available for testing.");
|
||||||
|
}
|
||||||
|
|
||||||
[Ignore("Docker")]
|
[Ignore("Docker")]
|
||||||
[Test]
|
[Test]
|
||||||
public void should_be_able_to_get_space_on_folder_that_doesnt_exist()
|
public void should_be_able_to_get_space_on_folder_that_doesnt_exist()
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
using System.Globalization;
|
|
||||||
using FluentAssertions;
|
|
||||||
using NUnit.Framework;
|
|
||||||
using NzbDrone.Common.Extensions;
|
|
||||||
|
|
||||||
namespace NzbDrone.Common.Test.ExtensionTests.StringExtensionTests
|
|
||||||
{
|
|
||||||
[TestFixture]
|
|
||||||
public class IsValidIPAddressFixture
|
|
||||||
{
|
|
||||||
[TestCase("192.168.0.1")]
|
|
||||||
[TestCase("::1")]
|
|
||||||
[TestCase("2001:db8:4006:812::200e")]
|
|
||||||
public void should_validate_ip_address(string input)
|
|
||||||
{
|
|
||||||
input.IsValidIpAddress().Should().BeTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase("sonarr.tv")]
|
|
||||||
public void should_not_parse_non_ip_address(string input)
|
|
||||||
{
|
|
||||||
input.IsValidIpAddress().Should().BeFalse();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,6 @@ using System.Globalization;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Moq;
|
using Moq;
|
||||||
@@ -16,11 +15,8 @@ using NzbDrone.Common.Http;
|
|||||||
using NzbDrone.Common.Http.Dispatchers;
|
using NzbDrone.Common.Http.Dispatchers;
|
||||||
using NzbDrone.Common.Http.Proxy;
|
using NzbDrone.Common.Http.Proxy;
|
||||||
using NzbDrone.Common.TPL;
|
using NzbDrone.Common.TPL;
|
||||||
using NzbDrone.Core.Configuration;
|
|
||||||
using NzbDrone.Core.Security;
|
|
||||||
using NzbDrone.Test.Common;
|
using NzbDrone.Test.Common;
|
||||||
using NzbDrone.Test.Common.Categories;
|
using NzbDrone.Test.Common.Categories;
|
||||||
using HttpClient = NzbDrone.Common.Http.HttpClient;
|
|
||||||
|
|
||||||
namespace NzbDrone.Common.Test.Http
|
namespace NzbDrone.Common.Test.Http
|
||||||
{
|
{
|
||||||
@@ -35,8 +31,6 @@ namespace NzbDrone.Common.Test.Http
|
|||||||
private string _httpBinHost;
|
private string _httpBinHost;
|
||||||
private string _httpBinHost2;
|
private string _httpBinHost2;
|
||||||
|
|
||||||
private System.Net.Http.HttpClient _httpClient = new ();
|
|
||||||
|
|
||||||
[OneTimeSetUp]
|
[OneTimeSetUp]
|
||||||
public void FixtureSetUp()
|
public void FixtureSetUp()
|
||||||
{
|
{
|
||||||
@@ -44,7 +38,7 @@ namespace NzbDrone.Common.Test.Http
|
|||||||
var mainHost = "httpbin.servarr.com";
|
var mainHost = "httpbin.servarr.com";
|
||||||
|
|
||||||
// Use mirrors for tests that use two hosts
|
// Use mirrors for tests that use two hosts
|
||||||
var candidates = new[] { "httpbin1.servarr.com" };
|
var candidates = new[] { "eu.httpbin.org", /* "httpbin.org", */ "www.httpbin.org" };
|
||||||
|
|
||||||
// httpbin.org is broken right now, occassionally redirecting to https if it's unavailable.
|
// httpbin.org is broken right now, occassionally redirecting to https if it's unavailable.
|
||||||
_httpBinHost = mainHost;
|
_httpBinHost = mainHost;
|
||||||
@@ -52,22 +46,31 @@ namespace NzbDrone.Common.Test.Http
|
|||||||
|
|
||||||
TestLogger.Info($"{candidates.Length} TestSites available.");
|
TestLogger.Info($"{candidates.Length} TestSites available.");
|
||||||
|
|
||||||
_httpBinSleep = 10;
|
_httpBinSleep = _httpBinHosts.Length < 2 ? 100 : 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsTestSiteAvailable(string site)
|
private bool IsTestSiteAvailable(string site)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var res = _httpClient.GetAsync($"https://{site}/get").GetAwaiter().GetResult();
|
var req = WebRequest.Create($"https://{site}/get") as HttpWebRequest;
|
||||||
|
var res = req.GetResponse() as HttpWebResponse;
|
||||||
if (res.StatusCode != HttpStatusCode.OK)
|
if (res.StatusCode != HttpStatusCode.OK)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
res = _httpClient.GetAsync($"https://{site}/status/429").GetAwaiter().GetResult();
|
try
|
||||||
|
{
|
||||||
|
req = WebRequest.Create($"https://{site}/status/429") as HttpWebRequest;
|
||||||
|
res = req.GetResponse() as HttpWebResponse;
|
||||||
|
}
|
||||||
|
catch (WebException ex)
|
||||||
|
{
|
||||||
|
res = ex.Response as HttpWebResponse;
|
||||||
|
}
|
||||||
|
|
||||||
if (res == null || res.StatusCode != HttpStatusCode.TooManyRequests)
|
if (res == null || res.StatusCode != (HttpStatusCode)429)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -92,13 +95,10 @@ namespace NzbDrone.Common.Test.Http
|
|||||||
Mocker.GetMock<IOsInfo>().Setup(c => c.Name).Returns("TestOS");
|
Mocker.GetMock<IOsInfo>().Setup(c => c.Name).Returns("TestOS");
|
||||||
Mocker.GetMock<IOsInfo>().Setup(c => c.Version).Returns("9.0.0");
|
Mocker.GetMock<IOsInfo>().Setup(c => c.Version).Returns("9.0.0");
|
||||||
|
|
||||||
Mocker.GetMock<IConfigService>().SetupGet(x => x.CertificateValidation).Returns(CertificateValidationType.Enabled);
|
|
||||||
|
|
||||||
Mocker.SetConstant<IUserAgentBuilder>(Mocker.Resolve<UserAgentBuilder>());
|
Mocker.SetConstant<IUserAgentBuilder>(Mocker.Resolve<UserAgentBuilder>());
|
||||||
|
|
||||||
Mocker.SetConstant<ICacheManager>(Mocker.Resolve<CacheManager>());
|
Mocker.SetConstant<ICacheManager>(Mocker.Resolve<CacheManager>());
|
||||||
Mocker.SetConstant<ICreateManagedWebProxy>(Mocker.Resolve<ManagedWebProxyFactory>());
|
Mocker.SetConstant<ICreateManagedWebProxy>(Mocker.Resolve<ManagedWebProxyFactory>());
|
||||||
Mocker.SetConstant<ICertificateValidationService>(new X509CertificateValidationService(Mocker.GetMock<IConfigService>().Object, TestLogger));
|
|
||||||
Mocker.SetConstant<IRateLimitService>(Mocker.Resolve<RateLimitService>());
|
Mocker.SetConstant<IRateLimitService>(Mocker.Resolve<RateLimitService>());
|
||||||
Mocker.SetConstant<IEnumerable<IHttpRequestInterceptor>>(Array.Empty<IHttpRequestInterceptor>());
|
Mocker.SetConstant<IEnumerable<IHttpRequestInterceptor>>(Array.Empty<IHttpRequestInterceptor>());
|
||||||
Mocker.SetConstant<IHttpDispatcher>(Mocker.Resolve<TDispatcher>());
|
Mocker.SetConstant<IHttpDispatcher>(Mocker.Resolve<TDispatcher>());
|
||||||
@@ -138,28 +138,6 @@ namespace NzbDrone.Common.Test.Http
|
|||||||
response.Content.Should().NotBeNullOrWhiteSpace();
|
response.Content.Should().NotBeNullOrWhiteSpace();
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase(CertificateValidationType.Enabled)]
|
|
||||||
[TestCase(CertificateValidationType.DisabledForLocalAddresses)]
|
|
||||||
public void bad_ssl_should_fail_when_remote_validation_enabled(CertificateValidationType validationType)
|
|
||||||
{
|
|
||||||
Mocker.GetMock<IConfigService>().SetupGet(x => x.CertificateValidation).Returns(validationType);
|
|
||||||
var request = new HttpRequest($"https://expired.badssl.com");
|
|
||||||
|
|
||||||
Assert.Throws<HttpRequestException>(() => Subject.Execute(request));
|
|
||||||
ExceptionVerification.ExpectedErrors(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void bad_ssl_should_pass_if_remote_validation_disabled()
|
|
||||||
{
|
|
||||||
Mocker.GetMock<IConfigService>().SetupGet(x => x.CertificateValidation).Returns(CertificateValidationType.Disabled);
|
|
||||||
|
|
||||||
var request = new HttpRequest($"https://expired.badssl.com");
|
|
||||||
|
|
||||||
Subject.Execute(request);
|
|
||||||
ExceptionVerification.ExpectedErrors(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void should_execute_typed_get()
|
public void should_execute_typed_get()
|
||||||
{
|
{
|
||||||
@@ -184,45 +162,15 @@ namespace NzbDrone.Common.Test.Http
|
|||||||
response.Resource.Data.Should().Be(message);
|
response.Resource.Data.Should().Be(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[TestCase("gzip")]
|
||||||
public void should_execute_post_with_content_type()
|
public void should_execute_get_using_gzip(string compression)
|
||||||
{
|
{
|
||||||
var message = "{ my: 1 }";
|
var request = new HttpRequest($"https://{_httpBinHost}/{compression}");
|
||||||
|
|
||||||
var request = new HttpRequest($"https://{_httpBinHost}/post");
|
|
||||||
request.SetContent(message);
|
|
||||||
request.Headers.ContentType = "application/json";
|
|
||||||
|
|
||||||
var response = Subject.Post<HttpBinResource>(request);
|
|
||||||
|
|
||||||
response.Resource.Data.Should().Be(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void should_execute_get_using_gzip()
|
|
||||||
{
|
|
||||||
var request = new HttpRequest($"https://{_httpBinHost}/gzip");
|
|
||||||
|
|
||||||
var response = Subject.Get<HttpBinResource>(request);
|
var response = Subject.Get<HttpBinResource>(request);
|
||||||
|
|
||||||
response.Resource.Headers["Accept-Encoding"].ToString().Should().Contain("gzip");
|
response.Resource.Headers["Accept-Encoding"].ToString().Should().Be(compression);
|
||||||
|
|
||||||
response.Resource.Gzipped.Should().BeTrue();
|
response.Resource.Gzipped.Should().BeTrue();
|
||||||
response.Resource.Brotli.Should().BeFalse();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
[Platform(Exclude = "MacOsX", Reason = "Azure agent update prevents brotli on OSX")]
|
|
||||||
public void should_execute_get_using_brotli()
|
|
||||||
{
|
|
||||||
var request = new HttpRequest($"https://{_httpBinHost}/brotli");
|
|
||||||
|
|
||||||
var response = Subject.Get<HttpBinResource>(request);
|
|
||||||
|
|
||||||
response.Resource.Headers["Accept-Encoding"].ToString().Should().Contain("br");
|
|
||||||
|
|
||||||
response.Resource.Gzipped.Should().BeFalse();
|
|
||||||
response.Resource.Brotli.Should().BeTrue();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase(HttpStatusCode.Unauthorized)]
|
[TestCase(HttpStatusCode.Unauthorized)]
|
||||||
@@ -242,28 +190,6 @@ namespace NzbDrone.Common.Test.Http
|
|||||||
ExceptionVerification.IgnoreWarns();
|
ExceptionVerification.IgnoreWarns();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void should_not_throw_on_suppressed_status_codes()
|
|
||||||
{
|
|
||||||
var request = new HttpRequest($"https://{_httpBinHost}/status/{HttpStatusCode.NotFound}");
|
|
||||||
request.SuppressHttpErrorStatusCodes = new[] { HttpStatusCode.NotFound };
|
|
||||||
|
|
||||||
Assert.Throws<HttpException>(() => Subject.Get<HttpBinResource>(request));
|
|
||||||
|
|
||||||
ExceptionVerification.IgnoreWarns();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void should_not_log_unsuccessful_status_codes()
|
|
||||||
{
|
|
||||||
var request = new HttpRequest($"https://{_httpBinHost}/status/{HttpStatusCode.NotFound}");
|
|
||||||
request.LogHttpError = false;
|
|
||||||
|
|
||||||
Assert.Throws<HttpException>(() => Subject.Get<HttpBinResource>(request));
|
|
||||||
|
|
||||||
ExceptionVerification.ExpectedWarns(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void should_not_follow_redirects_when_not_in_production()
|
public void should_not_follow_redirects_when_not_in_production()
|
||||||
{
|
{
|
||||||
@@ -389,38 +315,13 @@ namespace NzbDrone.Common.Test.Http
|
|||||||
{
|
{
|
||||||
var file = GetTempFilePath();
|
var file = GetTempFilePath();
|
||||||
|
|
||||||
Assert.Throws<HttpException>(() => Subject.DownloadFile("https://download.sonarr.tv/wrongpath", file));
|
Assert.Throws<WebException>(() => Subject.DownloadFile("https://download.sonarr.tv/wrongpath", file));
|
||||||
|
|
||||||
File.Exists(file).Should().BeFalse();
|
File.Exists(file).Should().BeFalse();
|
||||||
|
|
||||||
ExceptionVerification.ExpectedWarns(1);
|
ExceptionVerification.ExpectedWarns(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void should_not_write_redirect_content_to_stream()
|
|
||||||
{
|
|
||||||
var file = GetTempFilePath();
|
|
||||||
|
|
||||||
using (var fileStream = new FileStream(file, FileMode.Create))
|
|
||||||
{
|
|
||||||
var request = new HttpRequest($"http://{_httpBinHost}/redirect/1");
|
|
||||||
request.AllowAutoRedirect = false;
|
|
||||||
request.ResponseStream = fileStream;
|
|
||||||
|
|
||||||
var response = Subject.Get(request);
|
|
||||||
|
|
||||||
response.StatusCode.Should().Be(HttpStatusCode.Moved);
|
|
||||||
}
|
|
||||||
|
|
||||||
ExceptionVerification.ExpectedErrors(1);
|
|
||||||
|
|
||||||
File.Exists(file).Should().BeTrue();
|
|
||||||
|
|
||||||
var fileInfo = new FileInfo(file);
|
|
||||||
|
|
||||||
fileInfo.Length.Should().Be(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void should_send_cookie()
|
public void should_send_cookie()
|
||||||
{
|
{
|
||||||
@@ -852,7 +753,6 @@ namespace NzbDrone.Common.Test.Http
|
|||||||
public string Url { get; set; }
|
public string Url { get; set; }
|
||||||
public string Data { get; set; }
|
public string Data { get; set; }
|
||||||
public bool Gzipped { get; set; }
|
public bool Gzipped { get; set; }
|
||||||
public bool Brotli { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class HttpCookieResource
|
public class HttpCookieResource
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NzbDrone.Common.Http;
|
using NzbDrone.Common.Http;
|
||||||
using NzbDrone.Test.Common;
|
using NzbDrone.Test.Common;
|
||||||
@@ -10,7 +10,6 @@ namespace NzbDrone.Common.Test.Http
|
|||||||
[TestCase("abc://my_host.com:8080/root/api/")]
|
[TestCase("abc://my_host.com:8080/root/api/")]
|
||||||
[TestCase("abc://my_host.com:8080//root/api/")]
|
[TestCase("abc://my_host.com:8080//root/api/")]
|
||||||
[TestCase("abc://my_host.com:8080/root//api/")]
|
[TestCase("abc://my_host.com:8080/root//api/")]
|
||||||
[TestCase("abc://[::1]:8080/root//api/")]
|
|
||||||
public void should_parse(string uri)
|
public void should_parse(string uri)
|
||||||
{
|
{
|
||||||
var newUri = new HttpUri(uri);
|
var newUri = new HttpUri(uri);
|
||||||
|
|||||||
@@ -28,12 +28,9 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
|||||||
// Indexer and Download Client Responses
|
// Indexer and Download Client Responses
|
||||||
|
|
||||||
// avistaz response
|
// avistaz response
|
||||||
[TestCase(@"""download"":""https://avistaz.to/rss/download/2b51db35e1910123321025a12b9933d2/tb51db35e1910123321025a12b9933d2.torrent"",")]
|
[TestCase(@"""download"":""https:\/\/avistaz.to\/rss\/download\/2b51db35e1910123321025a12b9933d2\/tb51db35e1910123321025a12b9933d2.torrent"",")]
|
||||||
[TestCase(@",""info_hash"":""2b51db35e1910123321025a12b9933d2"",")]
|
[TestCase(@",""info_hash"":""2b51db35e1910123321025a12b9933d2"",")]
|
||||||
|
|
||||||
// animebytes response
|
|
||||||
[TestCase(@"""Link"":""https://animebytes.tv/torrent/994064/download/tb51db35e1910123321025a12b9933d2"",")]
|
|
||||||
|
|
||||||
// danish bytes response
|
// danish bytes response
|
||||||
[TestCase(@",""rsskey"":""2b51db35e1910123321025a12b9933d2"",")]
|
[TestCase(@",""rsskey"":""2b51db35e1910123321025a12b9933d2"",")]
|
||||||
[TestCase(@",""passkey"":""2b51db35e1910123321025a12b9933d2"",")]
|
[TestCase(@",""passkey"":""2b51db35e1910123321025a12b9933d2"",")]
|
||||||
@@ -80,48 +77,29 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
|||||||
// Download Station
|
// Download Station
|
||||||
[TestCase(@"webapi/entry.cgi?api=(removed)&version=2&method=login&account=01233210&passwd=mySecret&format=sid&session=DownloadStation")]
|
[TestCase(@"webapi/entry.cgi?api=(removed)&version=2&method=login&account=01233210&passwd=mySecret&format=sid&session=DownloadStation")]
|
||||||
|
|
||||||
// Tracker Responses
|
|
||||||
[TestCase(@"tracker"":""http://xxx.yyy/announce.php?passkey=9pr04sg601233210imaveql2tyu8xyui"",""info"":""http://xxx.yyy/info?a=b""")]
|
|
||||||
|
|
||||||
// BroadcastheNet
|
// BroadcastheNet
|
||||||
[TestCase(@"method: ""getTorrents"", ""params"": [ ""mySecret"",")]
|
[TestCase(@"method: ""getTorrents"", ""params"": [ ""mySecret"",")]
|
||||||
[TestCase(@"getTorrents(""mySecret"", [asdfasdf], 100, 0)")]
|
[TestCase(@"getTorrents(""mySecret"", [asdfasdf], 100, 0)")]
|
||||||
[TestCase(@"""DownloadURL"":""https://broadcasthe.net/torrents.php?action=download&id=123&authkey=mySecret&torrent_pass=mySecret""")]
|
[TestCase(@"""DownloadURL"":""https:\/\/broadcasthe.net\/torrents.php?action=download&id=123&authkey=mySecret&torrent_pass=mySecret""")]
|
||||||
|
|
||||||
// Webhooks - Notifiarr
|
// Notifiarr
|
||||||
[TestCase(@"https://xxx.yyy/api/v1/notification/prowlarr/9pr04sg6-0123-3210-imav-eql2tyu8xyui")]
|
|
||||||
[TestCase("https://notifiarr.com/notifier.php: api=1234530f-422f-4aac-b6b3-01233210aaaa&radarr_health_issue_message=Download")]
|
[TestCase("https://notifiarr.com/notifier.php: api=1234530f-422f-4aac-b6b3-01233210aaaa&radarr_health_issue_message=Download")]
|
||||||
|
[TestCase("/readarr/signalr/messages/negotiate?access_token=1234530f422f4aacb6b301233210aaaa&negotiateVersion=1")]
|
||||||
|
|
||||||
// RSS
|
// RSS
|
||||||
[TestCase(@"<atom:link href = ""https://api.nzb.su/api?t=search&extended=1&cat=3030&apikey=mySecret&q=Diggers"" rel=""self"" type=""application/rss+xml"" />")]
|
[TestCase(@"<atom:link href = ""https://api.nzb.su/api?t=search&extended=1&cat=3030&apikey=mySecret&q=Diggers"" rel=""self"" type=""application/rss+xml"" />")]
|
||||||
|
|
||||||
// Internal
|
// Internal
|
||||||
[TestCase(@"[Info] MigrationController: *** Migrating Database=prowlarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;Enlist=False ***")]
|
[TestCase(@"[Info] MigrationController: *** Migrating Database=prowlarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;Enlist=False ***")]
|
||||||
[TestCase("/readarr/signalr/messages/negotiate?access_token=1234530f422f4aacb6b301233210aaaa&negotiateVersion=1")]
|
|
||||||
[TestCase(@"[Info] MigrationController: *** Migrating Database=prowlarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;token=mySecret;Enlist=False&username=mySecret;mypassword=mySecret;mypass=shouldkeep1;test_token=mySecret;password=123%@%_@!#^#@;use_password=mySecret;get_token=shouldkeep2;usetoken=shouldkeep3;passwrd=mySecret;")]
|
|
||||||
public void should_clean_message(string message)
|
public void should_clean_message(string message)
|
||||||
{
|
{
|
||||||
var cleansedMessage = CleanseLogMessage.Cleanse(message);
|
var cleansedMessage = CleanseLogMessage.Cleanse(message);
|
||||||
|
|
||||||
cleansedMessage.Should().NotContain("mySecret");
|
cleansedMessage.Should().NotContain("mySecret");
|
||||||
cleansedMessage.Should().NotContain("123%@%_@!#^#@");
|
|
||||||
cleansedMessage.Should().NotContain("01233210");
|
cleansedMessage.Should().NotContain("01233210");
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase(@"[Info] MigrationController: *** Migrating Database=radarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;token=mySecret;Enlist=False&username=mySecret;mypassword=mySecret;mypass=shouldkeep1;test_token=mySecret;password=123%@%_@!#^#@;use_password=mySecret;get_token=shouldkeep2;usetoken=shouldkeep3;passwrd=mySecret;")]
|
|
||||||
public void should_keep_message(string message)
|
|
||||||
{
|
|
||||||
var cleansedMessage = CleanseLogMessage.Cleanse(message);
|
|
||||||
|
|
||||||
cleansedMessage.Should().NotContain("mySecret");
|
|
||||||
cleansedMessage.Should().NotContain("123%@%_@!#^#@");
|
|
||||||
cleansedMessage.Should().NotContain("01233210");
|
|
||||||
|
|
||||||
cleansedMessage.Should().Contain("shouldkeep1");
|
|
||||||
cleansedMessage.Should().Contain("shouldkeep2");
|
|
||||||
cleansedMessage.Should().Contain("shouldkeep3");
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase(@"Some message (from 32.2.3.5 user agent)")]
|
[TestCase(@"Some message (from 32.2.3.5 user agent)")]
|
||||||
[TestCase(@"Auth-Invalidated ip 32.2.3.5")]
|
[TestCase(@"Auth-Invalidated ip 32.2.3.5")]
|
||||||
[TestCase(@"Auth-Success ip 32.2.3.5")]
|
[TestCase(@"Auth-Success ip 32.2.3.5")]
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ namespace NzbDrone.Common.Test
|
|||||||
var processStarted = new ManualResetEventSlim();
|
var processStarted = new ManualResetEventSlim();
|
||||||
|
|
||||||
string suffix;
|
string suffix;
|
||||||
if (OsInfo.IsWindows)
|
if (OsInfo.IsWindows || PlatformInfo.IsMono)
|
||||||
{
|
{
|
||||||
suffix = ".exe";
|
suffix = ".exe";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,11 @@ using DryIoc.Microsoft.DependencyInjection;
|
|||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using Moq;
|
using Moq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NzbDrone.Common.Composition.Extensions;
|
using NzbDrone.Common.Composition.Extensions;
|
||||||
using NzbDrone.Common.EnvironmentInfo;
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
using NzbDrone.Common.Instrumentation.Extensions;
|
using NzbDrone.Common.Instrumentation.Extensions;
|
||||||
using NzbDrone.Core.Datastore;
|
|
||||||
using NzbDrone.Core.Datastore.Extensions;
|
using NzbDrone.Core.Datastore.Extensions;
|
||||||
using NzbDrone.Core.Lifecycle;
|
using NzbDrone.Core.Lifecycle;
|
||||||
using NzbDrone.Core.Messaging.Events;
|
using NzbDrone.Core.Messaging.Events;
|
||||||
@@ -31,8 +29,7 @@ namespace NzbDrone.Common.Test
|
|||||||
.AddDummyDatabase()
|
.AddDummyDatabase()
|
||||||
.AddStartupContext(new StartupContext("first", "second"));
|
.AddStartupContext(new StartupContext("first", "second"));
|
||||||
|
|
||||||
container.RegisterInstance(new Mock<IHostLifetime>().Object);
|
container.RegisterInstance<IHostLifetime>(new Mock<IHostLifetime>().Object);
|
||||||
container.RegisterInstance(new Mock<IOptions<PostgresOptions>>().Object);
|
|
||||||
|
|
||||||
var serviceProvider = container.GetServiceProvider();
|
var serviceProvider = container.GetServiceProvider();
|
||||||
|
|
||||||
|
|||||||
@@ -30,8 +30,7 @@ namespace NzbDrone.Common.Disk
|
|||||||
public abstract long? GetAvailableSpace(string path);
|
public abstract long? GetAvailableSpace(string path);
|
||||||
public abstract void InheritFolderPermissions(string filename);
|
public abstract void InheritFolderPermissions(string filename);
|
||||||
public abstract void SetEveryonePermissions(string filename);
|
public abstract void SetEveryonePermissions(string filename);
|
||||||
public abstract void SetFilePermissions(string path, string mask, string group);
|
public abstract void SetPermissions(string path, string mask);
|
||||||
public abstract void SetPermissions(string path, string mask, string group);
|
|
||||||
public abstract void CopyPermissions(string sourcePath, string targetPath);
|
public abstract void CopyPermissions(string sourcePath, string targetPath);
|
||||||
public abstract long? GetTotalSize(string path);
|
public abstract long? GetTotalSize(string path);
|
||||||
|
|
||||||
@@ -131,7 +130,7 @@ namespace NzbDrone.Common.Disk
|
|||||||
{
|
{
|
||||||
var testPath = Path.Combine(path, "prowlarr_write_test.txt");
|
var testPath = Path.Combine(path, "prowlarr_write_test.txt");
|
||||||
var testContent = string.Format("This file was created to verify if '{0}' is writable. It should've been automatically deleted. Feel free to delete it.", path);
|
var testContent = string.Format("This file was created to verify if '{0}' is writable. It should've been automatically deleted. Feel free to delete it.", path);
|
||||||
WriteAllText(testPath, testContent);
|
File.WriteAllText(testPath, testContent);
|
||||||
File.Delete(testPath);
|
File.Delete(testPath);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -259,6 +258,17 @@ namespace NzbDrone.Common.Disk
|
|||||||
Ensure.That(source, () => source).IsValidPath();
|
Ensure.That(source, () => source).IsValidPath();
|
||||||
Ensure.That(destination, () => destination).IsValidPath();
|
Ensure.That(destination, () => destination).IsValidPath();
|
||||||
|
|
||||||
|
if (source.PathEquals(destination))
|
||||||
|
{
|
||||||
|
throw new IOException(string.Format("Source and destination can't be the same {0}", source));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (FolderExists(destination) && overwrite)
|
||||||
|
{
|
||||||
|
DeleteFolder(destination, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
RemoveReadOnlyFolder(source);
|
||||||
Directory.Move(source, destination);
|
Directory.Move(source, destination);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,16 +310,7 @@ namespace NzbDrone.Common.Disk
|
|||||||
{
|
{
|
||||||
Ensure.That(filename, () => filename).IsValidPath();
|
Ensure.That(filename, () => filename).IsValidPath();
|
||||||
RemoveReadOnly(filename);
|
RemoveReadOnly(filename);
|
||||||
|
File.WriteAllText(filename, contents);
|
||||||
// File.WriteAllText is broken on net core when writing to some CIFS mounts
|
|
||||||
// This workaround from https://github.com/dotnet/runtime/issues/42790#issuecomment-700362617
|
|
||||||
using (var fs = new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.None))
|
|
||||||
{
|
|
||||||
using (var writer = new StreamWriter(fs))
|
|
||||||
{
|
|
||||||
writer.Write(contents);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void FolderSetLastWriteTime(string path, DateTime dateTime)
|
public void FolderSetLastWriteTime(string path, DateTime dateTime)
|
||||||
@@ -549,7 +550,7 @@ namespace NzbDrone.Common.Disk
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual bool IsValidFolderPermissionMask(string mask)
|
public virtual bool IsValidFilePermissionMask(string mask)
|
||||||
{
|
{
|
||||||
throw new NotSupportedException();
|
throw new NotSupportedException();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using System.Linq;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common.EnsureThat;
|
using NzbDrone.Common.EnsureThat;
|
||||||
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
|
|
||||||
namespace NzbDrone.Common.Disk
|
namespace NzbDrone.Common.Disk
|
||||||
@@ -26,56 +27,11 @@ namespace NzbDrone.Common.Disk
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string ResolveRealParentPath(string path)
|
|
||||||
{
|
|
||||||
var parentPath = path.GetParentPath();
|
|
||||||
if (!_diskProvider.FolderExists(parentPath))
|
|
||||||
{
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
var realParentPath = parentPath.GetActualCasing();
|
|
||||||
|
|
||||||
var partialChildPath = path.Substring(parentPath.Length);
|
|
||||||
|
|
||||||
return realParentPath + partialChildPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
public TransferMode TransferFolder(string sourcePath, string targetPath, TransferMode mode)
|
public TransferMode TransferFolder(string sourcePath, string targetPath, TransferMode mode)
|
||||||
{
|
{
|
||||||
Ensure.That(sourcePath, () => sourcePath).IsValidPath();
|
Ensure.That(sourcePath, () => sourcePath).IsValidPath();
|
||||||
Ensure.That(targetPath, () => targetPath).IsValidPath();
|
Ensure.That(targetPath, () => targetPath).IsValidPath();
|
||||||
|
|
||||||
sourcePath = ResolveRealParentPath(sourcePath);
|
|
||||||
targetPath = ResolveRealParentPath(targetPath);
|
|
||||||
|
|
||||||
_logger.Debug("{0} Directory [{1}] > [{2}]", mode, sourcePath, targetPath);
|
|
||||||
|
|
||||||
if (sourcePath == targetPath)
|
|
||||||
{
|
|
||||||
throw new IOException(string.Format("Source and destination can't be the same {0}", sourcePath));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mode == TransferMode.Move && sourcePath.PathEquals(targetPath, StringComparison.InvariantCultureIgnoreCase) && _diskProvider.FolderExists(targetPath))
|
|
||||||
{
|
|
||||||
// Move folder out of the way to allow case-insensitive renames
|
|
||||||
var tempPath = sourcePath + ".backup~";
|
|
||||||
_logger.Trace("Rename Intermediate Directory [{0}] > [{1}]", sourcePath, tempPath);
|
|
||||||
_diskProvider.MoveFolder(sourcePath, tempPath);
|
|
||||||
|
|
||||||
if (!_diskProvider.FolderExists(targetPath))
|
|
||||||
{
|
|
||||||
_logger.Trace("Rename Intermediate Directory [{0}] > [{1}]", tempPath, targetPath);
|
|
||||||
_logger.Debug("Rename Directory [{0}] > [{1}]", sourcePath, targetPath);
|
|
||||||
_diskProvider.MoveFolder(tempPath, targetPath);
|
|
||||||
return mode;
|
|
||||||
}
|
|
||||||
|
|
||||||
// There were two separate folders, revert the intermediate rename and let the recursion deal with it
|
|
||||||
_logger.Trace("Rename Intermediate Directory [{0}] > [{1}]", tempPath, sourcePath);
|
|
||||||
_diskProvider.MoveFolder(tempPath, sourcePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mode == TransferMode.Move && !_diskProvider.FolderExists(targetPath))
|
if (mode == TransferMode.Move && !_diskProvider.FolderExists(targetPath))
|
||||||
{
|
{
|
||||||
var sourceMount = _diskProvider.GetMount(sourcePath);
|
var sourceMount = _diskProvider.GetMount(sourcePath);
|
||||||
@@ -84,7 +40,7 @@ namespace NzbDrone.Common.Disk
|
|||||||
// If we're on the same mount, do a simple folder move.
|
// If we're on the same mount, do a simple folder move.
|
||||||
if (sourceMount != null && targetMount != null && sourceMount.RootDirectory == targetMount.RootDirectory)
|
if (sourceMount != null && targetMount != null && sourceMount.RootDirectory == targetMount.RootDirectory)
|
||||||
{
|
{
|
||||||
_logger.Debug("Rename Directory [{0}] > [{1}]", sourcePath, targetPath);
|
_logger.Debug("Move Directory [{0}] > [{1}]", sourcePath, targetPath);
|
||||||
_diskProvider.MoveFolder(sourcePath, targetPath);
|
_diskProvider.MoveFolder(sourcePath, targetPath);
|
||||||
return mode;
|
return mode;
|
||||||
}
|
}
|
||||||
@@ -123,13 +79,6 @@ namespace NzbDrone.Common.Disk
|
|||||||
|
|
||||||
if (mode.HasFlag(TransferMode.Move))
|
if (mode.HasFlag(TransferMode.Move))
|
||||||
{
|
{
|
||||||
var totalSize = _diskProvider.GetFileInfos(sourcePath).Sum(v => v.Length);
|
|
||||||
|
|
||||||
if (totalSize > (100 * 1024L * 1024L))
|
|
||||||
{
|
|
||||||
throw new IOException($"Large files still exist in {sourcePath} after folder move, not deleting source folder");
|
|
||||||
}
|
|
||||||
|
|
||||||
_diskProvider.DeleteFolder(sourcePath, true);
|
_diskProvider.DeleteFolder(sourcePath, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,10 +92,7 @@ namespace NzbDrone.Common.Disk
|
|||||||
Ensure.That(sourcePath, () => sourcePath).IsValidPath();
|
Ensure.That(sourcePath, () => sourcePath).IsValidPath();
|
||||||
Ensure.That(targetPath, () => targetPath).IsValidPath();
|
Ensure.That(targetPath, () => targetPath).IsValidPath();
|
||||||
|
|
||||||
sourcePath = ResolveRealParentPath(sourcePath);
|
_logger.Debug("Mirror [{0}] > [{1}]", sourcePath, targetPath);
|
||||||
targetPath = ResolveRealParentPath(targetPath);
|
|
||||||
|
|
||||||
_logger.Debug("Mirror Folder [{0}] > [{1}]", sourcePath, targetPath);
|
|
||||||
|
|
||||||
if (!_diskProvider.FolderExists(targetPath))
|
if (!_diskProvider.FolderExists(targetPath))
|
||||||
{
|
{
|
||||||
@@ -258,9 +204,6 @@ namespace NzbDrone.Common.Disk
|
|||||||
Ensure.That(sourcePath, () => sourcePath).IsValidPath();
|
Ensure.That(sourcePath, () => sourcePath).IsValidPath();
|
||||||
Ensure.That(targetPath, () => targetPath).IsValidPath();
|
Ensure.That(targetPath, () => targetPath).IsValidPath();
|
||||||
|
|
||||||
sourcePath = ResolveRealParentPath(sourcePath);
|
|
||||||
targetPath = ResolveRealParentPath(targetPath);
|
|
||||||
|
|
||||||
_logger.Debug("{0} [{1}] > [{2}]", mode, sourcePath, targetPath);
|
_logger.Debug("{0} [{1}] > [{2}]", mode, sourcePath, targetPath);
|
||||||
|
|
||||||
var originalSize = _diskProvider.GetFileSize(sourcePath);
|
var originalSize = _diskProvider.GetFileSize(sourcePath);
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ namespace NzbDrone.Common.Disk
|
|||||||
long? GetAvailableSpace(string path);
|
long? GetAvailableSpace(string path);
|
||||||
void InheritFolderPermissions(string filename);
|
void InheritFolderPermissions(string filename);
|
||||||
void SetEveryonePermissions(string filename);
|
void SetEveryonePermissions(string filename);
|
||||||
void SetFilePermissions(string path, string mask, string group);
|
void SetPermissions(string path, string mask);
|
||||||
void SetPermissions(string path, string mask, string group);
|
|
||||||
void CopyPermissions(string sourcePath, string targetPath);
|
void CopyPermissions(string sourcePath, string targetPath);
|
||||||
long? GetTotalSize(string path);
|
long? GetTotalSize(string path);
|
||||||
DateTime FolderGetCreationTime(string path);
|
DateTime FolderGetCreationTime(string path);
|
||||||
@@ -57,6 +56,6 @@ namespace NzbDrone.Common.Disk
|
|||||||
List<FileInfo> GetFileInfos(string path, SearchOption searchOption = SearchOption.TopDirectoryOnly);
|
List<FileInfo> GetFileInfos(string path, SearchOption searchOption = SearchOption.TopDirectoryOnly);
|
||||||
void RemoveEmptySubfolders(string path);
|
void RemoveEmptySubfolders(string path);
|
||||||
void SaveStream(Stream stream, string path);
|
void SaveStream(Stream stream, string path);
|
||||||
bool IsValidFolderPermissionMask(string mask);
|
bool IsValidFilePermissionMask(string mask);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,79 +1,15 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
|
||||||
using NzbDrone.Common.EnvironmentInfo;
|
|
||||||
|
|
||||||
namespace NzbDrone.Common.Disk
|
namespace NzbDrone.Common.Disk
|
||||||
{
|
{
|
||||||
public static class LongPathSupport
|
public static class LongPathSupport
|
||||||
{
|
{
|
||||||
private static int MAX_PATH;
|
|
||||||
private static int MAX_NAME;
|
|
||||||
|
|
||||||
public static void Enable()
|
public static void Enable()
|
||||||
{
|
{
|
||||||
// Mono has an issue with enabling long path support via app.config.
|
// Mono has an issue with enabling long path support via app.config.
|
||||||
// This works for both mono and .net on Windows.
|
// This works for both mono and .net on Windows.
|
||||||
AppContext.SetSwitch("Switch.System.IO.UseLegacyPathHandling", false);
|
AppContext.SetSwitch("Switch.System.IO.UseLegacyPathHandling", false);
|
||||||
AppContext.SetSwitch("Switch.System.IO.BlockLongPaths", false);
|
AppContext.SetSwitch("Switch.System.IO.BlockLongPaths", false);
|
||||||
|
|
||||||
DetectLongPathLimits();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void DetectLongPathLimits()
|
|
||||||
{
|
|
||||||
if (!int.TryParse(Environment.GetEnvironmentVariable("MAX_PATH"), out MAX_PATH))
|
|
||||||
{
|
|
||||||
if (OsInfo.IsLinux)
|
|
||||||
{
|
|
||||||
MAX_PATH = 4096;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Windows paths can be up to 32,767 characters long, but each component of the path must be less than 255.
|
|
||||||
// If the OS does not have Long Path enabled, then the following will throw an exception
|
|
||||||
// ref: https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation
|
|
||||||
Path.GetDirectoryName($@"C:\{new string('a', 254)}\{new string('a', 254)}");
|
|
||||||
MAX_PATH = 4096;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
MAX_PATH = 260 - 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!int.TryParse(Environment.GetEnvironmentVariable("MAX_NAME"), out MAX_NAME))
|
|
||||||
{
|
|
||||||
MAX_NAME = 255;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int MaxFilePathLength
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (MAX_PATH == 0)
|
|
||||||
{
|
|
||||||
DetectLongPathLimits();
|
|
||||||
}
|
|
||||||
|
|
||||||
return MAX_PATH;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int MaxFileNameLength
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (MAX_NAME == 0)
|
|
||||||
{
|
|
||||||
DetectLongPathLimits();
|
|
||||||
}
|
|
||||||
|
|
||||||
return MAX_NAME;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,50 +7,34 @@ namespace NzbDrone.Common.Extensions
|
|||||||
{
|
{
|
||||||
public static bool IsLocalAddress(this IPAddress ipAddress)
|
public static bool IsLocalAddress(this IPAddress ipAddress)
|
||||||
{
|
{
|
||||||
// Map back to IPv4 if mapped to IPv6, for example "::ffff:1.2.3.4" to "1.2.3.4".
|
if (ipAddress.IsIPv6LinkLocal)
|
||||||
if (ipAddress.IsIPv4MappedToIPv6)
|
|
||||||
{
|
{
|
||||||
ipAddress = ipAddress.MapToIPv4();
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checks loopback ranges for both IPv4 and IPv6.
|
|
||||||
if (IPAddress.IsLoopback(ipAddress))
|
if (IPAddress.IsLoopback(ipAddress))
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// IPv4
|
|
||||||
if (ipAddress.AddressFamily == AddressFamily.InterNetwork)
|
if (ipAddress.AddressFamily == AddressFamily.InterNetwork)
|
||||||
{
|
{
|
||||||
return IsLocalIPv4(ipAddress.GetAddressBytes());
|
byte[] bytes = ipAddress.GetAddressBytes();
|
||||||
}
|
switch (bytes[0])
|
||||||
|
{
|
||||||
// IPv6
|
case 10:
|
||||||
if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6)
|
case 127:
|
||||||
{
|
return true;
|
||||||
return ipAddress.IsIPv6LinkLocal ||
|
case 172:
|
||||||
ipAddress.IsIPv6UniqueLocal ||
|
return bytes[1] < 32 && bytes[1] >= 16;
|
||||||
ipAddress.IsIPv6SiteLocal;
|
case 192:
|
||||||
|
return bytes[1] == 168;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsLocalIPv4(byte[] ipv4Bytes)
|
|
||||||
{
|
|
||||||
// Link local (no IP assigned by DHCP): 169.254.0.0 to 169.254.255.255 (169.254.0.0/16)
|
|
||||||
bool IsLinkLocal() => ipv4Bytes[0] == 169 && ipv4Bytes[1] == 254;
|
|
||||||
|
|
||||||
// Class A private range: 10.0.0.0 – 10.255.255.255 (10.0.0.0/8)
|
|
||||||
bool IsClassA() => ipv4Bytes[0] == 10;
|
|
||||||
|
|
||||||
// Class B private range: 172.16.0.0 – 172.31.255.255 (172.16.0.0/12)
|
|
||||||
bool IsClassB() => ipv4Bytes[0] == 172 && ipv4Bytes[1] >= 16 && ipv4Bytes[1] <= 31;
|
|
||||||
|
|
||||||
// Class C private range: 192.168.0.0 – 192.168.255.255 (192.168.0.0/16)
|
|
||||||
bool IsClassC() => ipv4Bytes[0] == 192 && ipv4Bytes[1] == 168;
|
|
||||||
|
|
||||||
return IsLinkLocal() || IsClassA() || IsClassC() || IsClassB();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ using System.Collections.Generic;
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Sockets;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
@@ -211,51 +210,5 @@ namespace NzbDrone.Common.Extensions
|
|||||||
|
|
||||||
return result.TrimStart(' ', '.').TrimEnd(' ');
|
return result.TrimStart(' ', '.').TrimEnd(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string EncodeRFC3986(this string value)
|
|
||||||
{
|
|
||||||
// From Twitterizer http://www.twitterizer.net/
|
|
||||||
if (string.IsNullOrEmpty(value))
|
|
||||||
{
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
var encoded = Uri.EscapeDataString(value);
|
|
||||||
|
|
||||||
return Regex
|
|
||||||
.Replace(encoded, "(%[0-9a-f][0-9a-f])", c => c.Value.ToUpper())
|
|
||||||
.Replace("(", "%28")
|
|
||||||
.Replace(")", "%29")
|
|
||||||
.Replace("$", "%24")
|
|
||||||
.Replace("!", "%21")
|
|
||||||
.Replace("*", "%2A")
|
|
||||||
.Replace("'", "%27")
|
|
||||||
.Replace("%7E", "~");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool IsValidIpAddress(this string value)
|
|
||||||
{
|
|
||||||
if (!IPAddress.TryParse(value, out var parsedAddress))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parsedAddress.Equals(IPAddress.Parse("255.255.255.255")))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parsedAddress.IsIPv6Multicast)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return parsedAddress.AddressFamily == AddressFamily.InterNetwork || parsedAddress.AddressFamily == AddressFamily.InterNetworkV6;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string ToUrlHost(this string input)
|
|
||||||
{
|
|
||||||
return input.Contains(":") ? $"[{input}]" : input;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
using System.Net;
|
|
||||||
|
|
||||||
namespace NzbDrone.Common.Http
|
|
||||||
{
|
|
||||||
public class BasicNetworkCredential : NetworkCredential
|
|
||||||
{
|
|
||||||
public BasicNetworkCredential(string user, string pass)
|
|
||||||
: base(user, pass)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,7 +10,6 @@ namespace NzbDrone.Common.Http
|
|||||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
|
||||||
// NOTE: we are not checking non-ascii characters and we should
|
// NOTE: we are not checking non-ascii characters and we should
|
||||||
private static readonly Regex _CookieRegex = new Regex(@"([^\(\)<>@,;:\\""/\[\]\?=\{\}\s]+)=([^,;\\""\s]+)");
|
private static readonly Regex _CookieRegex = new Regex(@"([^\(\)<>@,;:\\""/\[\]\?=\{\}\s]+)=([^,;\\""\s]+)");
|
||||||
private static readonly string[] FilterProps = { "COMMENT", "COMMENTURL", "DISCORD", "DOMAIN", "EXPIRES", "MAX-AGE", "PATH", "PORT", "SECURE", "VERSION", "HTTPONLY", "SAMESITE" };
|
|
||||||
private static readonly char[] InvalidKeyChars = { '(', ')', '<', '>', '@', ',', ';', ':', '\\', '"', '/', '[', ']', '?', '=', '{', '}', ' ', '\t', '\n' };
|
private static readonly char[] InvalidKeyChars = { '(', ')', '<', '>', '@', ',', ';', ':', '\\', '"', '/', '[', ']', '?', '=', '{', '}', ' ', '\t', '\n' };
|
||||||
private static readonly char[] InvalidValueChars = { '"', ',', ';', '\\', ' ', '\t', '\n' };
|
private static readonly char[] InvalidValueChars = { '"', ',', ';', '\\', ' ', '\t', '\n' };
|
||||||
|
|
||||||
@@ -25,7 +24,7 @@ namespace NzbDrone.Common.Http
|
|||||||
var matches = _CookieRegex.Match(cookieHeader);
|
var matches = _CookieRegex.Match(cookieHeader);
|
||||||
while (matches.Success)
|
while (matches.Success)
|
||||||
{
|
{
|
||||||
if (matches.Groups.Count > 2 && !FilterProps.Contains(matches.Groups[1].Value.ToUpperInvariant()))
|
if (matches.Groups.Count > 2)
|
||||||
{
|
{
|
||||||
cookieDictionary[matches.Groups[1].Value] = matches.Groups[2].Value;
|
cookieDictionary[matches.Groups[1].Value] = matches.Groups[2].Value;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
using System.Net.Http;
|
|
||||||
using System.Net.Security;
|
|
||||||
using System.Security.Cryptography.X509Certificates;
|
|
||||||
|
|
||||||
namespace NzbDrone.Common.Http.Dispatchers
|
|
||||||
{
|
|
||||||
public interface ICertificateValidationService
|
|
||||||
{
|
|
||||||
bool ShouldByPassValidationError(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,5 +6,6 @@ namespace NzbDrone.Common.Http.Dispatchers
|
|||||||
public interface IHttpDispatcher
|
public interface IHttpDispatcher
|
||||||
{
|
{
|
||||||
Task<HttpResponse> GetResponseAsync(HttpRequest request, CookieContainer cookies);
|
Task<HttpResponse> GetResponseAsync(HttpRequest request, CookieContainer cookies);
|
||||||
|
Task DownloadFileAsync(string url, string fileName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.IO.Compression;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Reflection;
|
||||||
using System.Net.Security;
|
|
||||||
using System.Net.Sockets;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common.Cache;
|
using NLog.Fluent;
|
||||||
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Http.Proxy;
|
using NzbDrone.Common.Http.Proxy;
|
||||||
|
|
||||||
@@ -18,225 +15,221 @@ namespace NzbDrone.Common.Http.Dispatchers
|
|||||||
{
|
{
|
||||||
public class ManagedHttpDispatcher : IHttpDispatcher
|
public class ManagedHttpDispatcher : IHttpDispatcher
|
||||||
{
|
{
|
||||||
private const string NO_PROXY_KEY = "no-proxy";
|
|
||||||
|
|
||||||
private const int connection_establish_timeout = 2000;
|
|
||||||
private static bool useIPv6 = Socket.OSSupportsIPv6;
|
|
||||||
private static bool hasResolvedIPv6Availability;
|
|
||||||
|
|
||||||
private readonly IHttpProxySettingsProvider _proxySettingsProvider;
|
private readonly IHttpProxySettingsProvider _proxySettingsProvider;
|
||||||
private readonly ICreateManagedWebProxy _createManagedWebProxy;
|
private readonly ICreateManagedWebProxy _createManagedWebProxy;
|
||||||
private readonly ICertificateValidationService _certificateValidationService;
|
|
||||||
private readonly IUserAgentBuilder _userAgentBuilder;
|
private readonly IUserAgentBuilder _userAgentBuilder;
|
||||||
private readonly ICached<System.Net.Http.HttpClient> _httpClientCache;
|
private readonly IPlatformInfo _platformInfo;
|
||||||
private readonly ICached<CredentialCache> _credentialCache;
|
private readonly Logger _logger;
|
||||||
|
|
||||||
public ManagedHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider,
|
public ManagedHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider, ICreateManagedWebProxy createManagedWebProxy, IUserAgentBuilder userAgentBuilder, IPlatformInfo platformInfo, Logger logger)
|
||||||
ICreateManagedWebProxy createManagedWebProxy,
|
|
||||||
ICertificateValidationService certificateValidationService,
|
|
||||||
IUserAgentBuilder userAgentBuilder,
|
|
||||||
ICacheManager cacheManager)
|
|
||||||
{
|
{
|
||||||
_proxySettingsProvider = proxySettingsProvider;
|
_proxySettingsProvider = proxySettingsProvider;
|
||||||
_createManagedWebProxy = createManagedWebProxy;
|
_createManagedWebProxy = createManagedWebProxy;
|
||||||
_certificateValidationService = certificateValidationService;
|
|
||||||
_userAgentBuilder = userAgentBuilder;
|
_userAgentBuilder = userAgentBuilder;
|
||||||
|
_platformInfo = platformInfo;
|
||||||
_httpClientCache = cacheManager.GetCache<System.Net.Http.HttpClient>(typeof(ManagedHttpDispatcher));
|
_logger = logger;
|
||||||
_credentialCache = cacheManager.GetCache<CredentialCache>(typeof(ManagedHttpDispatcher), "credentialcache");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<HttpResponse> GetResponseAsync(HttpRequest request, CookieContainer cookies)
|
public async Task<HttpResponse> GetResponseAsync(HttpRequest request, CookieContainer cookies)
|
||||||
{
|
{
|
||||||
var requestMessage = new HttpRequestMessage(request.Method, (Uri)request.Url);
|
var webRequest = (HttpWebRequest)WebRequest.Create((Uri)request.Url);
|
||||||
requestMessage.Headers.UserAgent.ParseAdd(_userAgentBuilder.GetUserAgent(request.UseSimplifiedUserAgent));
|
|
||||||
requestMessage.Headers.ConnectionClose = !request.ConnectionKeepAlive;
|
|
||||||
|
|
||||||
var cookieHeader = cookies.GetCookieHeader((Uri)request.Url);
|
// Deflate is not a standard and could break depending on implementation.
|
||||||
if (cookieHeader.IsNotNullOrWhiteSpace())
|
// we should just stick with the more compatible Gzip
|
||||||
{
|
//http://stackoverflow.com/questions/8490718/how-to-decompress-stream-deflated-with-java-util-zip-deflater-in-net
|
||||||
requestMessage.Headers.Add("Cookie", cookieHeader);
|
webRequest.AutomaticDecompression = DecompressionMethods.GZip;
|
||||||
}
|
|
||||||
|
webRequest.Method = request.Method.ToString();
|
||||||
|
webRequest.UserAgent = _userAgentBuilder.GetUserAgent(request.UseSimplifiedUserAgent);
|
||||||
|
webRequest.KeepAlive = request.ConnectionKeepAlive;
|
||||||
|
webRequest.AllowAutoRedirect = false;
|
||||||
|
webRequest.CookieContainer = cookies;
|
||||||
|
|
||||||
using var cts = new CancellationTokenSource();
|
|
||||||
if (request.RequestTimeout != TimeSpan.Zero)
|
if (request.RequestTimeout != TimeSpan.Zero)
|
||||||
{
|
{
|
||||||
cts.CancelAfter(request.RequestTimeout);
|
webRequest.Timeout = (int)Math.Ceiling(request.RequestTimeout.TotalMilliseconds);
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// The default for System.Net.Http.HttpClient
|
|
||||||
cts.CancelAfter(TimeSpan.FromSeconds(100));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.Credentials != null)
|
webRequest.Proxy = request.Proxy ?? GetProxy(request.Url);
|
||||||
{
|
|
||||||
if (request.Credentials is BasicNetworkCredential bc)
|
|
||||||
{
|
|
||||||
// Manually set header to avoid initial challenge response
|
|
||||||
var authInfo = bc.UserName + ":" + bc.Password;
|
|
||||||
authInfo = Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes(authInfo));
|
|
||||||
requestMessage.Headers.Add("Authorization", "Basic " + authInfo);
|
|
||||||
}
|
|
||||||
else if (request.Credentials is NetworkCredential nc)
|
|
||||||
{
|
|
||||||
var creds = GetCredentialCache();
|
|
||||||
foreach (var authtype in new[] { "Basic", "Digest" })
|
|
||||||
{
|
|
||||||
creds.Remove((Uri)request.Url, authtype);
|
|
||||||
creds.Add((Uri)request.Url, authtype, nc);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.ContentData != null)
|
|
||||||
{
|
|
||||||
requestMessage.Content = new ByteArrayContent(request.ContentData);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.Headers != null)
|
if (request.Headers != null)
|
||||||
{
|
{
|
||||||
AddRequestHeaders(requestMessage, request.Headers);
|
AddRequestHeaders(webRequest, request.Headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
var httpClient = GetClient(request.Url, request.ProxySettings);
|
HttpWebResponse httpWebResponse;
|
||||||
|
|
||||||
var sw = new Stopwatch();
|
var sw = new Stopwatch();
|
||||||
|
|
||||||
sw.Start();
|
sw.Start();
|
||||||
|
|
||||||
using var responseMessage = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cts.Token);
|
try
|
||||||
{
|
{
|
||||||
byte[] data = null;
|
if (request.ContentData != null)
|
||||||
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
if (request.ResponseStream != null && responseMessage.StatusCode == HttpStatusCode.OK)
|
webRequest.ContentLength = request.ContentData.Length;
|
||||||
|
using (var writeStream = webRequest.GetRequestStream())
|
||||||
{
|
{
|
||||||
responseMessage.Content.CopyTo(request.ResponseStream, null, cts.Token);
|
writeStream.Write(request.ContentData, 0, request.ContentData.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
httpWebResponse = (HttpWebResponse)await webRequest.GetResponseAsync();
|
||||||
|
}
|
||||||
|
catch (WebException e)
|
||||||
|
{
|
||||||
|
httpWebResponse = (HttpWebResponse)e.Response;
|
||||||
|
|
||||||
|
if (httpWebResponse == null)
|
||||||
|
{
|
||||||
|
// The default messages for WebException on mono are pretty horrible.
|
||||||
|
if (e.Status == WebExceptionStatus.NameResolutionFailure)
|
||||||
|
{
|
||||||
|
throw new WebException($"DNS Name Resolution Failure: '{webRequest.RequestUri.Host}'", e.Status);
|
||||||
|
}
|
||||||
|
else if (e.ToString().Contains("TLS Support not"))
|
||||||
|
{
|
||||||
|
throw new TlsFailureException(webRequest, e);
|
||||||
|
}
|
||||||
|
else if (e.ToString().Contains("The authentication or decryption has failed."))
|
||||||
|
{
|
||||||
|
throw new TlsFailureException(webRequest, e);
|
||||||
|
}
|
||||||
|
else if (OsInfo.IsNotWindows)
|
||||||
|
{
|
||||||
|
throw new WebException($"{e.Message}: '{webRequest.RequestUri}'", e, e.Status, e.Response);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
data = responseMessage.Content.ReadAsByteArrayAsync(cts.Token).GetAwaiter().GetResult();
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
}
|
||||||
|
|
||||||
|
byte[] data = null;
|
||||||
|
|
||||||
|
using (var responseStream = httpWebResponse.GetResponseStream())
|
||||||
|
{
|
||||||
|
if (responseStream != null && responseStream != Stream.Null)
|
||||||
{
|
{
|
||||||
throw new WebException("Failed to read complete http response", ex, WebExceptionStatus.ReceiveFailure, null);
|
try
|
||||||
}
|
|
||||||
|
|
||||||
var headers = responseMessage.Headers.ToNameValueCollection();
|
|
||||||
|
|
||||||
headers.Add(responseMessage.Content.Headers.ToNameValueCollection());
|
|
||||||
|
|
||||||
CookieContainer responseCookies = new CookieContainer();
|
|
||||||
|
|
||||||
if (responseMessage.Headers.TryGetValues("Set-Cookie", out var cookieHeaders))
|
|
||||||
{
|
|
||||||
foreach (var responseCookieHeader in cookieHeaders)
|
|
||||||
{
|
{
|
||||||
try
|
data = await responseStream.ToBytes();
|
||||||
{
|
}
|
||||||
cookies.SetCookies(responseMessage.RequestMessage.RequestUri, responseCookieHeader);
|
catch (Exception ex)
|
||||||
}
|
{
|
||||||
catch
|
throw new WebException("Failed to read complete http response", ex, WebExceptionStatus.ReceiveFailure, httpWebResponse);
|
||||||
{
|
|
||||||
// Ignore invalid cookies
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var cookieCollection = cookies.GetCookies(responseMessage.RequestMessage.RequestUri);
|
sw.Stop();
|
||||||
|
|
||||||
sw.Stop();
|
return new HttpResponse(request, new HttpHeader(httpWebResponse.Headers), httpWebResponse.Cookies, data, sw.ElapsedMilliseconds, httpWebResponse.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
return new HttpResponse(request, new HttpHeader(headers), cookieCollection, data, sw.ElapsedMilliseconds, responseMessage.StatusCode);
|
public async Task DownloadFileAsync(string url, string fileName)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var fileInfo = new FileInfo(fileName);
|
||||||
|
if (fileInfo.Directory != null && !fileInfo.Directory.Exists)
|
||||||
|
{
|
||||||
|
fileInfo.Directory.Create();
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.Debug("Downloading [{0}] to [{1}]", url, fileName);
|
||||||
|
|
||||||
|
var stopWatch = Stopwatch.StartNew();
|
||||||
|
var uri = new HttpUri(url);
|
||||||
|
|
||||||
|
using (var webClient = new GZipWebClient())
|
||||||
|
{
|
||||||
|
webClient.Headers.Add(HttpRequestHeader.UserAgent, _userAgentBuilder.GetUserAgent());
|
||||||
|
webClient.Proxy = GetProxy(uri);
|
||||||
|
await webClient.DownloadFileTaskAsync(url, fileName);
|
||||||
|
stopWatch.Stop();
|
||||||
|
_logger.Debug("Downloading Completed. took {0:0}s", stopWatch.Elapsed.Seconds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (WebException e)
|
||||||
|
{
|
||||||
|
_logger.Warn("Failed to get response from: {0} {1}", url, e.Message);
|
||||||
|
|
||||||
|
if (File.Exists(fileName))
|
||||||
|
{
|
||||||
|
File.Delete(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.Warn(e, "Failed to get response from: " + url);
|
||||||
|
|
||||||
|
if (File.Exists(fileName))
|
||||||
|
{
|
||||||
|
File.Delete(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual System.Net.Http.HttpClient GetClient(HttpUri uri, HttpProxySettings requestProxy)
|
protected virtual IWebProxy GetProxy(HttpUri uri)
|
||||||
{
|
{
|
||||||
var proxySettings = requestProxy ?? _proxySettingsProvider.GetProxySettings(uri);
|
IWebProxy proxy = null;
|
||||||
|
|
||||||
var key = proxySettings?.Key ?? NO_PROXY_KEY;
|
var proxySettings = _proxySettingsProvider.GetProxySettings(uri);
|
||||||
|
|
||||||
return _httpClientCache.Get(key, () => CreateHttpClient(proxySettings));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual System.Net.Http.HttpClient CreateHttpClient(HttpProxySettings proxySettings)
|
|
||||||
{
|
|
||||||
var handler = new SocketsHttpHandler()
|
|
||||||
{
|
|
||||||
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Brotli,
|
|
||||||
UseCookies = false, // sic - we don't want to use a shared cookie container
|
|
||||||
AllowAutoRedirect = false,
|
|
||||||
Credentials = GetCredentialCache(),
|
|
||||||
PreAuthenticate = true,
|
|
||||||
MaxConnectionsPerServer = 12,
|
|
||||||
ConnectCallback = onConnect,
|
|
||||||
PooledConnectionLifetime = TimeSpan.FromMinutes(10),
|
|
||||||
SslOptions = new SslClientAuthenticationOptions
|
|
||||||
{
|
|
||||||
RemoteCertificateValidationCallback = _certificateValidationService.ShouldByPassValidationError
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (proxySettings != null)
|
if (proxySettings != null)
|
||||||
{
|
{
|
||||||
handler.Proxy = _createManagedWebProxy.GetWebProxy(proxySettings);
|
proxy = _createManagedWebProxy.GetWebProxy(proxySettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
var client = new System.Net.Http.HttpClient(handler)
|
return proxy;
|
||||||
{
|
|
||||||
Timeout = Timeout.InfiniteTimeSpan
|
|
||||||
};
|
|
||||||
|
|
||||||
return client;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual void AddRequestHeaders(HttpRequestMessage webRequest, HttpHeader headers)
|
protected virtual void AddRequestHeaders(HttpWebRequest webRequest, HttpHeader headers)
|
||||||
{
|
{
|
||||||
foreach (var header in headers)
|
foreach (var header in headers)
|
||||||
{
|
{
|
||||||
switch (header.Key)
|
switch (header.Key)
|
||||||
{
|
{
|
||||||
case "Accept":
|
case "Accept":
|
||||||
webRequest.Headers.Accept.ParseAdd(header.Value);
|
webRequest.Accept = header.Value;
|
||||||
break;
|
break;
|
||||||
case "Connection":
|
case "Connection":
|
||||||
webRequest.Headers.Connection.Clear();
|
webRequest.Connection = header.Value;
|
||||||
webRequest.Headers.Connection.Add(header.Value);
|
|
||||||
break;
|
break;
|
||||||
case "Content-Length":
|
case "Content-Length":
|
||||||
AddContentHeader(webRequest, "Content-Length", header.Value);
|
webRequest.ContentLength = Convert.ToInt64(header.Value);
|
||||||
break;
|
break;
|
||||||
case "Content-Type":
|
case "Content-Type":
|
||||||
AddContentHeader(webRequest, "Content-Type", header.Value);
|
webRequest.ContentType = header.Value;
|
||||||
break;
|
break;
|
||||||
case "Date":
|
case "Date":
|
||||||
webRequest.Headers.Remove("Date");
|
webRequest.Date = HttpHeader.ParseDateTime(header.Value);
|
||||||
webRequest.Headers.Date = HttpHeader.ParseDateTime(header.Value);
|
|
||||||
break;
|
break;
|
||||||
case "Expect":
|
case "Expect":
|
||||||
webRequest.Headers.Expect.ParseAdd(header.Value);
|
webRequest.Expect = header.Value;
|
||||||
break;
|
break;
|
||||||
case "Host":
|
case "Host":
|
||||||
webRequest.Headers.Host = header.Value;
|
webRequest.Host = header.Value;
|
||||||
break;
|
break;
|
||||||
case "If-Modified-Since":
|
case "If-Modified-Since":
|
||||||
webRequest.Headers.IfModifiedSince = HttpHeader.ParseDateTime(header.Value);
|
webRequest.IfModifiedSince = HttpHeader.ParseDateTime(header.Value);
|
||||||
break;
|
break;
|
||||||
case "Range":
|
case "Range":
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
case "Referer":
|
case "Referer":
|
||||||
webRequest.Headers.Add("Referer", header.Value);
|
webRequest.Referer = header.Value;
|
||||||
break;
|
break;
|
||||||
case "Transfer-Encoding":
|
case "Transfer-Encoding":
|
||||||
webRequest.Headers.TransferEncoding.ParseAdd(header.Value);
|
webRequest.TransferEncoding = header.Value;
|
||||||
break;
|
break;
|
||||||
case "User-Agent":
|
case "User-Agent":
|
||||||
webRequest.Headers.UserAgent.Clear();
|
webRequest.UserAgent = header.Value;
|
||||||
webRequest.Headers.UserAgent.ParseAdd(header.Value);
|
|
||||||
break;
|
break;
|
||||||
case "Proxy-Connection":
|
case "Proxy-Connection":
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
@@ -246,84 +239,5 @@ namespace NzbDrone.Common.Http.Dispatchers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddContentHeader(HttpRequestMessage request, string header, string value)
|
|
||||||
{
|
|
||||||
var headers = request.Content?.Headers;
|
|
||||||
if (headers == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
headers.Remove(header);
|
|
||||||
headers.Add(header, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private CredentialCache GetCredentialCache()
|
|
||||||
{
|
|
||||||
return _credentialCache.Get("credentialCache", () => new CredentialCache());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async ValueTask<Stream> onConnect(SocketsHttpConnectionContext context, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
// Until .NET supports an implementation of Happy Eyeballs (https://tools.ietf.org/html/rfc8305#section-2), let's make IPv4 fallback work in a simple way.
|
|
||||||
// This issue is being tracked at https://github.com/dotnet/runtime/issues/26177 and expected to be fixed in .NET 6.
|
|
||||||
if (useIPv6)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var localToken = cancellationToken;
|
|
||||||
|
|
||||||
if (!hasResolvedIPv6Availability)
|
|
||||||
{
|
|
||||||
// to make things move fast, use a very low timeout for the initial ipv6 attempt.
|
|
||||||
var quickFailCts = new CancellationTokenSource(connection_establish_timeout);
|
|
||||||
var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, quickFailCts.Token);
|
|
||||||
|
|
||||||
localToken = linkedTokenSource.Token;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await attemptConnection(AddressFamily.InterNetworkV6, context, localToken);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// very naively fallback to ipv4 permanently for this execution based on the response of the first connection attempt.
|
|
||||||
// note that this may cause users to eventually get switched to ipv4 (on a random failure when they are switching networks, for instance)
|
|
||||||
// but in the interest of keeping this implementation simple, this is acceptable.
|
|
||||||
useIPv6 = false;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
hasResolvedIPv6Availability = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// fallback to IPv4.
|
|
||||||
return await attemptConnection(AddressFamily.InterNetwork, context, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async ValueTask<Stream> attemptConnection(AddressFamily addressFamily, SocketsHttpConnectionContext context, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
// The following socket constructor will create a dual-mode socket on systems where IPV6 is available.
|
|
||||||
var socket = new Socket(addressFamily, SocketType.Stream, ProtocolType.Tcp)
|
|
||||||
{
|
|
||||||
// Turn off Nagle's algorithm since it degrades performance in most HttpClient scenarios.
|
|
||||||
NoDelay = true
|
|
||||||
};
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await socket.ConnectAsync(context.DnsEndPoint, cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
// The stream should take the ownership of the underlying socket,
|
|
||||||
// closing it when it's disposed.
|
|
||||||
return new NetworkStream(socket, ownsSocket: true);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
socket.Dispose();
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
15
src/NzbDrone.Common/Http/GZipWebClient.cs
Normal file
15
src/NzbDrone.Common/Http/GZipWebClient.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
using System.Net;
|
||||||
|
|
||||||
|
namespace NzbDrone.Common.Http
|
||||||
|
{
|
||||||
|
public class GZipWebClient : WebClient
|
||||||
|
{
|
||||||
|
protected override WebRequest GetWebRequest(Uri address)
|
||||||
|
{
|
||||||
|
var request = (HttpWebRequest)base.GetWebRequest(address);
|
||||||
|
request.AutomaticDecompression = DecompressionMethods.GZip;
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
@@ -87,21 +86,13 @@ namespace NzbDrone.Common.Http
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 302 or 303 should default to GET on redirect even if POST on original
|
// 302 or 303 should default to GET on redirect even if POST on original
|
||||||
if (RequestRequiresForceGet(response.StatusCode, response.Request.Method))
|
if (response.StatusCode == HttpStatusCode.Redirect || response.StatusCode == HttpStatusCode.RedirectMethod)
|
||||||
{
|
{
|
||||||
request.Method = HttpMethod.Get;
|
request.Method = HttpMethod.Get;
|
||||||
request.ContentData = null;
|
request.ContentData = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save to add to final response
|
response = await ExecuteRequestAsync(request, cookieContainer);
|
||||||
var responseCookies = response.Cookies;
|
|
||||||
|
|
||||||
// Update cookiecontainer for next request with any cookies recieved on last request
|
|
||||||
var responseContainer = HandleRedirectCookies(request, response);
|
|
||||||
|
|
||||||
response = await ExecuteRequestAsync(request, responseContainer);
|
|
||||||
|
|
||||||
response.Cookies.Add(responseCookies);
|
|
||||||
}
|
}
|
||||||
while (response.HasHttpRedirect);
|
while (response.HasHttpRedirect);
|
||||||
}
|
}
|
||||||
@@ -111,14 +102,11 @@ namespace NzbDrone.Common.Http
|
|||||||
_logger.Error("Server requested a redirect to [{0}] while in developer mode. Update the request URL to avoid this redirect.", response.Headers["Location"]);
|
_logger.Error("Server requested a redirect to [{0}] while in developer mode. Update the request URL to avoid this redirect.", response.Headers["Location"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!request.SuppressHttpError && response.HasHttpError && (request.SuppressHttpErrorStatusCodes == null || !request.SuppressHttpErrorStatusCodes.Contains(response.StatusCode)))
|
if (!request.SuppressHttpError && response.HasHttpError)
|
||||||
{
|
{
|
||||||
if (request.LogHttpError)
|
_logger.Warn("HTTP Error - {0}", response);
|
||||||
{
|
|
||||||
_logger.Warn("HTTP Error - {0}", response);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.StatusCode == HttpStatusCode.TooManyRequests)
|
if ((int)response.StatusCode == 429)
|
||||||
{
|
{
|
||||||
throw new TooManyRequestsException(request, response);
|
throw new TooManyRequestsException(request, response);
|
||||||
}
|
}
|
||||||
@@ -136,21 +124,6 @@ namespace NzbDrone.Common.Http
|
|||||||
return ExecuteAsync(request).GetAwaiter().GetResult();
|
return ExecuteAsync(request).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool RequestRequiresForceGet(HttpStatusCode statusCode, HttpMethod requestMethod)
|
|
||||||
{
|
|
||||||
switch (statusCode)
|
|
||||||
{
|
|
||||||
case HttpStatusCode.Moved:
|
|
||||||
case HttpStatusCode.Found:
|
|
||||||
case HttpStatusCode.MultipleChoices:
|
|
||||||
return requestMethod == HttpMethod.Post;
|
|
||||||
case HttpStatusCode.SeeOther:
|
|
||||||
return requestMethod != HttpMethod.Get && requestMethod != HttpMethod.Head;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<HttpResponse> ExecuteRequestAsync(HttpRequest request, CookieContainer cookieContainer)
|
private async Task<HttpResponse> ExecuteRequestAsync(HttpRequest request, CookieContainer cookieContainer)
|
||||||
{
|
{
|
||||||
foreach (var interceptor in _requestInterceptors)
|
foreach (var interceptor in _requestInterceptors)
|
||||||
@@ -167,6 +140,8 @@ namespace NzbDrone.Common.Http
|
|||||||
|
|
||||||
var stopWatch = Stopwatch.StartNew();
|
var stopWatch = Stopwatch.StartNew();
|
||||||
|
|
||||||
|
PrepareRequestCookies(request, cookieContainer);
|
||||||
|
|
||||||
var response = await _httpDispatcher.GetResponseAsync(request, cookieContainer);
|
var response = await _httpDispatcher.GetResponseAsync(request, cookieContainer);
|
||||||
|
|
||||||
HandleResponseCookies(response, cookieContainer);
|
HandleResponseCookies(response, cookieContainer);
|
||||||
@@ -233,125 +208,52 @@ namespace NzbDrone.Common.Http
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private CookieContainer HandleRedirectCookies(HttpRequest request, HttpResponse response)
|
private void PrepareRequestCookies(HttpRequest request, CookieContainer cookieContainer)
|
||||||
{
|
{
|
||||||
var sourceContainer = new CookieContainer();
|
// Don't collect persistnet cookies for intermediate/redirected urls.
|
||||||
var responseCookies = response.GetCookies();
|
/*lock (_cookieContainerCache)
|
||||||
if (responseCookies.Count != 0)
|
|
||||||
{
|
{
|
||||||
foreach (var pair in responseCookies)
|
var presistentContainer = _cookieContainerCache.Get("container", () => new CookieContainer());
|
||||||
{
|
var persistentCookies = presistentContainer.GetCookies((Uri)request.Url);
|
||||||
Cookie cookie;
|
var existingCookies = cookieContainer.GetCookies((Uri)request.Url);
|
||||||
if (pair.Value == null)
|
|
||||||
{
|
|
||||||
cookie = new Cookie(pair.Key, "", "/")
|
|
||||||
{
|
|
||||||
Expires = DateTime.Now.AddDays(-1)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
cookie = new Cookie(pair.Key, pair.Value, "/")
|
|
||||||
{
|
|
||||||
// Use Now rather than UtcNow to work around Mono cookie expiry bug.
|
|
||||||
// See https://gist.github.com/ta264/7822b1424f72e5b4c961
|
|
||||||
Expires = DateTime.Now.AddHours(1)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceContainer.Add((Uri)request.Url, cookie);
|
cookieContainer.Add(persistentCookies);
|
||||||
}
|
cookieContainer.Add(existingCookies);
|
||||||
}
|
}*/
|
||||||
|
|
||||||
return sourceContainer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleResponseCookies(HttpResponse response, CookieContainer container)
|
private void HandleResponseCookies(HttpResponse response, CookieContainer cookieContainer)
|
||||||
{
|
{
|
||||||
foreach (Cookie cookie in container.GetAllCookies())
|
|
||||||
{
|
|
||||||
cookie.Expired = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var cookieHeaders = response.GetCookieHeaders();
|
var cookieHeaders = response.GetCookieHeaders();
|
||||||
if (cookieHeaders.Empty())
|
if (cookieHeaders.Empty())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
AddCookiesToContainer(response.Request.Url, cookieHeaders, container);
|
|
||||||
|
|
||||||
if (response.Request.StoreResponseCookie)
|
if (response.Request.StoreResponseCookie)
|
||||||
{
|
{
|
||||||
lock (_cookieContainerCache)
|
lock (_cookieContainerCache)
|
||||||
{
|
{
|
||||||
var persistentCookieContainer = _cookieContainerCache.Get("container", () => new CookieContainer());
|
var persistentCookieContainer = _cookieContainerCache.Get("container", () => new CookieContainer());
|
||||||
|
|
||||||
AddCookiesToContainer(response.Request.Url, cookieHeaders, persistentCookieContainer);
|
foreach (var cookieHeader in cookieHeaders)
|
||||||
}
|
{
|
||||||
}
|
try
|
||||||
}
|
{
|
||||||
|
persistentCookieContainer.SetCookies((Uri)response.Request.Url, cookieHeader);
|
||||||
private void AddCookiesToContainer(HttpUri url, string[] cookieHeaders, CookieContainer container)
|
}
|
||||||
{
|
catch (Exception ex)
|
||||||
foreach (var cookieHeader in cookieHeaders)
|
{
|
||||||
{
|
_logger.Debug(ex, "Invalid cookie in {0}", response.Request.Url);
|
||||||
try
|
}
|
||||||
{
|
}
|
||||||
container.SetCookies((Uri)url, cookieHeader);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.Debug(ex, "Invalid cookie in {0}", url);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DownloadFileAsync(string url, string fileName)
|
public async Task DownloadFileAsync(string url, string fileName)
|
||||||
{
|
{
|
||||||
var fileNamePart = fileName + ".part";
|
await _httpDispatcher.DownloadFileAsync(url, fileName);
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var fileInfo = new FileInfo(fileName);
|
|
||||||
if (fileInfo.Directory != null && !fileInfo.Directory.Exists)
|
|
||||||
{
|
|
||||||
fileInfo.Directory.Create();
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.Debug("Downloading [{0}] to [{1}]", url, fileName);
|
|
||||||
|
|
||||||
var stopWatch = Stopwatch.StartNew();
|
|
||||||
using (var fileStream = new FileStream(fileNamePart, FileMode.Create, FileAccess.ReadWrite))
|
|
||||||
{
|
|
||||||
var request = new HttpRequest(url);
|
|
||||||
request.AllowAutoRedirect = true;
|
|
||||||
request.ResponseStream = fileStream;
|
|
||||||
var response = await GetAsync(request);
|
|
||||||
|
|
||||||
if (response.Headers.ContentType != null && response.Headers.ContentType.Contains("text/html"))
|
|
||||||
{
|
|
||||||
throw new HttpException(request, response, "Site responded with html content.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stopWatch.Stop();
|
|
||||||
|
|
||||||
if (File.Exists(fileName))
|
|
||||||
{
|
|
||||||
File.Delete(fileName);
|
|
||||||
}
|
|
||||||
|
|
||||||
File.Move(fileNamePart, fileName);
|
|
||||||
_logger.Debug("Downloading Completed. took {0:0}s", stopWatch.Elapsed.Seconds);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (File.Exists(fileNamePart))
|
|
||||||
{
|
|
||||||
File.Delete(fileNamePart);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DownloadFile(string url, string fileName)
|
public void DownloadFile(string url, string fileName)
|
||||||
|
|||||||
@@ -4,27 +4,11 @@ using System.Collections.Generic;
|
|||||||
using System.Collections.Specialized;
|
using System.Collections.Specialized;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
|
||||||
using System.Net.Http.Headers;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
|
|
||||||
namespace NzbDrone.Common.Http
|
namespace NzbDrone.Common.Http
|
||||||
{
|
{
|
||||||
public static class WebHeaderCollectionExtensions
|
|
||||||
{
|
|
||||||
public static NameValueCollection ToNameValueCollection(this HttpHeaders headers)
|
|
||||||
{
|
|
||||||
var result = new NameValueCollection();
|
|
||||||
foreach (var header in headers)
|
|
||||||
{
|
|
||||||
result.Add(header.Key, header.Value.ConcatToString(";"));
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class HttpHeader : NameValueCollection, IEnumerable<KeyValuePair<string, string>>, IEnumerable
|
public class HttpHeader : NameValueCollection, IEnumerable<KeyValuePair<string, string>>, IEnumerable
|
||||||
{
|
{
|
||||||
public HttpHeader(NameValueCollection headers)
|
public HttpHeader(NameValueCollection headers)
|
||||||
@@ -32,11 +16,6 @@ namespace NzbDrone.Common.Http
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public HttpHeader(HttpHeaders headers)
|
|
||||||
: base(headers.ToNameValueCollection())
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public HttpHeader()
|
public HttpHeader()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -128,30 +107,6 @@ namespace NzbDrone.Common.Http
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public string ContentEncoding
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return GetSingleValue("Content-Encoding");
|
|
||||||
}
|
|
||||||
set
|
|
||||||
{
|
|
||||||
SetSingleValue("Content-Encoding", value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Vary
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return GetSingleValue("Vary");
|
|
||||||
}
|
|
||||||
set
|
|
||||||
{
|
|
||||||
SetSingleValue("Vary", value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string UserAgent
|
public string UserAgent
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using NzbDrone.Common.EnvironmentInfo;
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Http.Proxy;
|
|
||||||
|
|
||||||
namespace NzbDrone.Common.Http
|
namespace NzbDrone.Common.Http
|
||||||
{
|
{
|
||||||
@@ -14,13 +12,11 @@ namespace NzbDrone.Common.Http
|
|||||||
{
|
{
|
||||||
public HttpRequest(string url, HttpAccept httpAccept = null)
|
public HttpRequest(string url, HttpAccept httpAccept = null)
|
||||||
{
|
{
|
||||||
Method = HttpMethod.Get;
|
|
||||||
Url = new HttpUri(url);
|
Url = new HttpUri(url);
|
||||||
Headers = new HttpHeader();
|
Headers = new HttpHeader();
|
||||||
Method = HttpMethod.Get;
|
Method = HttpMethod.Get;
|
||||||
ConnectionKeepAlive = true;
|
ConnectionKeepAlive = true;
|
||||||
AllowAutoRedirect = true;
|
AllowAutoRedirect = true;
|
||||||
LogHttpError = true;
|
|
||||||
Cookies = new Dictionary<string, string>();
|
Cookies = new Dictionary<string, string>();
|
||||||
|
|
||||||
if (!RuntimeInfo.IsProduction)
|
if (!RuntimeInfo.IsProduction)
|
||||||
@@ -38,23 +34,19 @@ namespace NzbDrone.Common.Http
|
|||||||
public HttpMethod Method { get; set; }
|
public HttpMethod Method { get; set; }
|
||||||
public HttpHeader Headers { get; set; }
|
public HttpHeader Headers { get; set; }
|
||||||
public Encoding Encoding { get; set; }
|
public Encoding Encoding { get; set; }
|
||||||
public HttpProxySettings ProxySettings { get; set; }
|
public IWebProxy Proxy { get; set; }
|
||||||
public byte[] ContentData { get; set; }
|
public byte[] ContentData { get; set; }
|
||||||
public string ContentSummary { get; set; }
|
public string ContentSummary { get; set; }
|
||||||
public ICredentials Credentials { get; set; }
|
|
||||||
public bool SuppressHttpError { get; set; }
|
public bool SuppressHttpError { get; set; }
|
||||||
public IEnumerable<HttpStatusCode> SuppressHttpErrorStatusCodes { get; set; }
|
|
||||||
public bool UseSimplifiedUserAgent { get; set; }
|
public bool UseSimplifiedUserAgent { get; set; }
|
||||||
public bool AllowAutoRedirect { get; set; }
|
public bool AllowAutoRedirect { get; set; }
|
||||||
public bool ConnectionKeepAlive { get; set; }
|
public bool ConnectionKeepAlive { get; set; }
|
||||||
public bool LogResponseContent { get; set; }
|
public bool LogResponseContent { get; set; }
|
||||||
public bool LogHttpError { get; set; }
|
|
||||||
public Dictionary<string, string> Cookies { get; private set; }
|
public Dictionary<string, string> Cookies { get; private set; }
|
||||||
public bool StoreRequestCookie { get; set; }
|
public bool StoreRequestCookie { get; set; }
|
||||||
public bool StoreResponseCookie { get; set; }
|
public bool StoreResponseCookie { get; set; }
|
||||||
public TimeSpan RequestTimeout { get; set; }
|
public TimeSpan RequestTimeout { get; set; }
|
||||||
public TimeSpan RateLimit { get; set; }
|
public TimeSpan RateLimit { get; set; }
|
||||||
public Stream ResponseStream { get; set; }
|
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
@@ -111,5 +103,12 @@ namespace NzbDrone.Common.Http
|
|||||||
return encoding.GetString(ContentData);
|
return encoding.GetString(ContentData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void AddBasicAuthentication(string username, string password)
|
||||||
|
{
|
||||||
|
var authInfo = Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes($"{username}:{password}"));
|
||||||
|
|
||||||
|
Headers.Set("Authorization", "Basic " + authInfo);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,13 +21,12 @@ namespace NzbDrone.Common.Http
|
|||||||
public Dictionary<string, string> Segments { get; private set; }
|
public Dictionary<string, string> Segments { get; private set; }
|
||||||
public HttpHeader Headers { get; private set; }
|
public HttpHeader Headers { get; private set; }
|
||||||
public bool SuppressHttpError { get; set; }
|
public bool SuppressHttpError { get; set; }
|
||||||
public bool LogHttpError { get; set; }
|
|
||||||
public bool UseSimplifiedUserAgent { get; set; }
|
public bool UseSimplifiedUserAgent { get; set; }
|
||||||
public bool AllowAutoRedirect { get; set; }
|
public bool AllowAutoRedirect { get; set; }
|
||||||
public bool ConnectionKeepAlive { get; set; }
|
public bool ConnectionKeepAlive { get; set; }
|
||||||
public TimeSpan RateLimit { get; set; }
|
public TimeSpan RateLimit { get; set; }
|
||||||
public bool LogResponseContent { get; set; }
|
public bool LogResponseContent { get; set; }
|
||||||
public ICredentials NetworkCredential { get; set; }
|
public NetworkCredential NetworkCredential { get; set; }
|
||||||
public Dictionary<string, string> Cookies { get; private set; }
|
public Dictionary<string, string> Cookies { get; private set; }
|
||||||
public bool StoreRequestCookie { get; set; }
|
public bool StoreRequestCookie { get; set; }
|
||||||
public bool StoreResponseCookie { get; set; }
|
public bool StoreResponseCookie { get; set; }
|
||||||
@@ -47,7 +46,6 @@ namespace NzbDrone.Common.Http
|
|||||||
Headers = new HttpHeader();
|
Headers = new HttpHeader();
|
||||||
Cookies = new Dictionary<string, string>();
|
Cookies = new Dictionary<string, string>();
|
||||||
FormData = new List<HttpFormData>();
|
FormData = new List<HttpFormData>();
|
||||||
LogHttpError = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public HttpRequestBuilder(bool useHttps, string host, int port, string urlBase = null)
|
public HttpRequestBuilder(bool useHttps, string host, int port, string urlBase = null)
|
||||||
@@ -108,7 +106,6 @@ namespace NzbDrone.Common.Http
|
|||||||
request.Method = Method;
|
request.Method = Method;
|
||||||
request.Encoding = Encoding;
|
request.Encoding = Encoding;
|
||||||
request.SuppressHttpError = SuppressHttpError;
|
request.SuppressHttpError = SuppressHttpError;
|
||||||
request.LogHttpError = LogHttpError;
|
|
||||||
request.UseSimplifiedUserAgent = UseSimplifiedUserAgent;
|
request.UseSimplifiedUserAgent = UseSimplifiedUserAgent;
|
||||||
request.AllowAutoRedirect = AllowAutoRedirect;
|
request.AllowAutoRedirect = AllowAutoRedirect;
|
||||||
request.StoreRequestCookie = StoreRequestCookie;
|
request.StoreRequestCookie = StoreRequestCookie;
|
||||||
@@ -116,7 +113,13 @@ namespace NzbDrone.Common.Http
|
|||||||
request.ConnectionKeepAlive = ConnectionKeepAlive;
|
request.ConnectionKeepAlive = ConnectionKeepAlive;
|
||||||
request.RateLimit = RateLimit;
|
request.RateLimit = RateLimit;
|
||||||
request.LogResponseContent = LogResponseContent;
|
request.LogResponseContent = LogResponseContent;
|
||||||
request.Credentials = NetworkCredential;
|
|
||||||
|
if (NetworkCredential != null)
|
||||||
|
{
|
||||||
|
var authInfo = NetworkCredential.UserName + ":" + NetworkCredential.Password;
|
||||||
|
authInfo = Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes(authInfo));
|
||||||
|
request.Headers.Set("Authorization", "Basic " + authInfo);
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var header in Headers)
|
foreach (var header in Headers)
|
||||||
{
|
{
|
||||||
@@ -209,7 +212,7 @@ namespace NzbDrone.Common.Http
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
summary.AppendFormat("\r\n{0}={1}", formData.Name, Encoding.GetString(formData.ContentData));
|
summary.AppendFormat("\r\n{0}={1}", formData.Name, Encoding.UTF8.GetString(formData.ContentData));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,9 +232,9 @@ namespace NzbDrone.Common.Http
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var parameters = FormData.Select(v => string.Format("{0}={1}", v.Name, Uri.EscapeDataString(Encoding.GetString(v.ContentData))));
|
var parameters = FormData.Select(v => string.Format("{0}={1}", v.Name, Uri.EscapeDataString(Encoding.UTF8.GetString(v.ContentData))));
|
||||||
var urlencoded = string.Join("&", parameters);
|
var urlencoded = string.Join("&", parameters);
|
||||||
var body = Encoding.GetBytes(urlencoded);
|
var body = Encoding.UTF8.GetBytes(urlencoded);
|
||||||
|
|
||||||
request.Headers.ContentType = "application/x-www-form-urlencoded";
|
request.Headers.ContentType = "application/x-www-form-urlencoded";
|
||||||
request.SetContent(body);
|
request.SetContent(body);
|
||||||
@@ -403,7 +406,7 @@ namespace NzbDrone.Common.Http
|
|||||||
FormData.Add(new HttpFormData
|
FormData.Add(new HttpFormData
|
||||||
{
|
{
|
||||||
Name = key,
|
Name = key,
|
||||||
ContentData = Encoding.GetBytes(Convert.ToString(value, System.Globalization.CultureInfo.InvariantCulture))
|
ContentData = Encoding.UTF8.GetBytes(Convert.ToString(value, System.Globalization.CultureInfo.InvariantCulture))
|
||||||
});
|
});
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
|
|||||||
@@ -64,11 +64,10 @@ namespace NzbDrone.Common.Http
|
|||||||
public bool HasHttpError => (int)StatusCode >= 400;
|
public bool HasHttpError => (int)StatusCode >= 400;
|
||||||
|
|
||||||
public bool HasHttpRedirect => StatusCode == HttpStatusCode.Moved ||
|
public bool HasHttpRedirect => StatusCode == HttpStatusCode.Moved ||
|
||||||
StatusCode == HttpStatusCode.Found ||
|
StatusCode == HttpStatusCode.MovedPermanently ||
|
||||||
StatusCode == HttpStatusCode.SeeOther ||
|
StatusCode == HttpStatusCode.RedirectMethod ||
|
||||||
StatusCode == HttpStatusCode.TemporaryRedirect ||
|
StatusCode == HttpStatusCode.TemporaryRedirect ||
|
||||||
StatusCode == HttpStatusCode.MultipleChoices ||
|
StatusCode == HttpStatusCode.Found ||
|
||||||
StatusCode == HttpStatusCode.PermanentRedirect ||
|
|
||||||
Headers.ContainsKey("Refresh");
|
Headers.ContainsKey("Refresh");
|
||||||
|
|
||||||
public string RedirectUrl
|
public string RedirectUrl
|
||||||
@@ -89,13 +88,13 @@ namespace NzbDrone.Common.Http
|
|||||||
|
|
||||||
if (match.Success)
|
if (match.Success)
|
||||||
{
|
{
|
||||||
return (Request.Url + new HttpUri(match.Groups[2].Value)).FullUri;
|
return (Request.Url += new HttpUri(match.Groups[2].Value)).FullUri;
|
||||||
}
|
}
|
||||||
|
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (Request.Url + new HttpUri(newUrl)).FullUri;
|
return (Request.Url += new HttpUri(newUrl)).FullUri;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +117,7 @@ namespace NzbDrone.Common.Http
|
|||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
var result = string.Format("Res: [{0}] {1}: {2}.{3} ({4} bytes)", Request.Method, Request.Url, (int)StatusCode, StatusCode, ResponseData?.Length ?? 0);
|
var result = string.Format("Res: [{0}] {1}: {2}.{3}", Request.Method, Request.Url, (int)StatusCode, StatusCode);
|
||||||
|
|
||||||
if (HasHttpError && Headers.ContentType.IsNotNullOrWhiteSpace() && !Headers.ContentType.Equals("text/html", StringComparison.InvariantCultureIgnoreCase))
|
if (HasHttpError && Headers.ContentType.IsNotNullOrWhiteSpace() && !Headers.ContentType.Equals("text/html", StringComparison.InvariantCultureIgnoreCase))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ namespace NzbDrone.Common.Http
|
|||||||
{
|
{
|
||||||
public class HttpUri : IEquatable<HttpUri>
|
public class HttpUri : IEquatable<HttpUri>
|
||||||
{
|
{
|
||||||
private static readonly Regex RegexUri = new Regex(@"^(?:(?<scheme>[a-z]+):)?(?://(?<host>[-_A-Z0-9.]+|\[[[A-F0-9:]+\])(?::(?<port>[0-9]{1,5}))?)?(?<path>(?:(?:(?<=^)|/+)[^/?#\r\n]+)+/*|/+)?(?:\?(?<query>[^#\r\n]*))?(?:\#(?<fragment>.*))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
private static readonly Regex RegexUri = new Regex(@"^(?:(?<scheme>[a-z]+):)?(?://(?<host>[-_A-Z0-9.]+)(?::(?<port>[0-9]{1,5}))?)?(?<path>(?:(?:(?<=^)|/+)[^/?#\r\n]+)+/*|/+)?(?:\?(?<query>[^#\r\n]*))?(?:\#(?<fragment>.*))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
private readonly string _uri;
|
private readonly string _uri;
|
||||||
public string FullUri => _uri;
|
public string FullUri => _uri;
|
||||||
@@ -70,8 +70,6 @@ namespace NzbDrone.Common.Http
|
|||||||
|
|
||||||
private void Parse()
|
private void Parse()
|
||||||
{
|
{
|
||||||
var parseSuccess = Uri.TryCreate(_uri, UriKind.RelativeOrAbsolute, out var uri);
|
|
||||||
|
|
||||||
var match = RegexUri.Match(_uri);
|
var match = RegexUri.Match(_uri);
|
||||||
|
|
||||||
var scheme = match.Groups["scheme"];
|
var scheme = match.Groups["scheme"];
|
||||||
@@ -81,7 +79,7 @@ namespace NzbDrone.Common.Http
|
|||||||
var query = match.Groups["query"];
|
var query = match.Groups["query"];
|
||||||
var fragment = match.Groups["fragment"];
|
var fragment = match.Groups["fragment"];
|
||||||
|
|
||||||
if (!parseSuccess || (scheme.Success && !host.Success && path.Success))
|
if (!match.Success || (scheme.Success && !host.Success && path.Success))
|
||||||
{
|
{
|
||||||
throw new ArgumentException("Uri didn't match expected pattern: " + _uri);
|
throw new ArgumentException("Uri didn't match expected pattern: " + _uri);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace NzbDrone.Common.Http
|
namespace NzbDrone.Common.Http
|
||||||
{
|
{
|
||||||
@@ -25,11 +25,5 @@ namespace NzbDrone.Common.Http
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public TooManyRequestsException(HttpRequest request, HttpResponse response, TimeSpan retryWait)
|
|
||||||
: base(request, response)
|
|
||||||
{
|
|
||||||
RetryAfter = retryWait;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,103 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Xml.Linq;
|
|
||||||
using NLog;
|
|
||||||
using NzbDrone.Common.Instrumentation;
|
|
||||||
|
|
||||||
namespace NzbDrone.Common.Http
|
|
||||||
{
|
|
||||||
public class XmlRpcRequestBuilder : HttpRequestBuilder
|
|
||||||
{
|
|
||||||
public static string XmlRpcContentType = "text/xml";
|
|
||||||
|
|
||||||
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(XmlRpcRequestBuilder));
|
|
||||||
|
|
||||||
public string XmlMethod { get; private set; }
|
|
||||||
public List<object> XmlParameters { get; private set; }
|
|
||||||
|
|
||||||
public XmlRpcRequestBuilder(string baseUrl)
|
|
||||||
: base(baseUrl)
|
|
||||||
{
|
|
||||||
Method = HttpMethod.Post;
|
|
||||||
XmlParameters = new List<object>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public XmlRpcRequestBuilder(bool useHttps, string host, int port, string urlBase = null)
|
|
||||||
: this(BuildBaseUrl(useHttps, host, port, urlBase))
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public override HttpRequestBuilder Clone()
|
|
||||||
{
|
|
||||||
var clone = base.Clone() as XmlRpcRequestBuilder;
|
|
||||||
clone.XmlParameters = new List<object>(XmlParameters);
|
|
||||||
return clone;
|
|
||||||
}
|
|
||||||
|
|
||||||
public XmlRpcRequestBuilder Call(string method, params object[] parameters)
|
|
||||||
{
|
|
||||||
var clone = Clone() as XmlRpcRequestBuilder;
|
|
||||||
clone.XmlMethod = method;
|
|
||||||
clone.XmlParameters = parameters.ToList();
|
|
||||||
return clone;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Apply(HttpRequest request)
|
|
||||||
{
|
|
||||||
base.Apply(request);
|
|
||||||
|
|
||||||
request.Headers.ContentType = XmlRpcContentType;
|
|
||||||
|
|
||||||
var methodCallElements = new List<XElement> { new XElement("methodName", XmlMethod) };
|
|
||||||
|
|
||||||
if (XmlParameters.Any())
|
|
||||||
{
|
|
||||||
var argElements = XmlParameters.Select(x => new XElement("param", ConvertParameter(x))).ToList();
|
|
||||||
var paramsElement = new XElement("params", argElements);
|
|
||||||
methodCallElements.Add(paramsElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
var message = new XDocument(
|
|
||||||
new XDeclaration("1.0", "utf-8", "yes"),
|
|
||||||
new XElement("methodCall", methodCallElements));
|
|
||||||
|
|
||||||
var body = message.ToString();
|
|
||||||
|
|
||||||
Logger.Debug($"Executing remote method: {XmlMethod}");
|
|
||||||
|
|
||||||
Logger.Trace($"methodCall {XmlMethod} body:\n{body}");
|
|
||||||
|
|
||||||
request.SetContent(body);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static XElement ConvertParameter(object value)
|
|
||||||
{
|
|
||||||
XElement data;
|
|
||||||
|
|
||||||
if (value is string s)
|
|
||||||
{
|
|
||||||
data = new XElement("string", s);
|
|
||||||
}
|
|
||||||
else if (value is List<string> l)
|
|
||||||
{
|
|
||||||
data = new XElement("array", new XElement("data", l.Select(x => new XElement("value", new XElement("string", x)))));
|
|
||||||
}
|
|
||||||
else if (value is int i)
|
|
||||||
{
|
|
||||||
data = new XElement("int", i);
|
|
||||||
}
|
|
||||||
else if (value is byte[] bytes)
|
|
||||||
{
|
|
||||||
data = new XElement("base64", Convert.ToBase64String(bytes));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"Unhandled argument type {value.GetType().Name}");
|
|
||||||
}
|
|
||||||
|
|
||||||
return new XElement("value", data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user