mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2026-03-05 13:40:08 -05:00
Compare commits
218 Commits
v0.1.5.116
...
v0.2.0.167
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca0de18413 | ||
|
|
fb8b65a91b | ||
|
|
b4e0608b3b | ||
|
|
60d9f02830 | ||
|
|
4f83116413 | ||
|
|
d440bc079f | ||
|
|
a5c7c6cbcb | ||
|
|
710c3d6deb | ||
|
|
1959efbd09 | ||
|
|
c4468b9cbb | ||
|
|
8762d94dda | ||
|
|
e07ea80977 | ||
|
|
37c393a659 | ||
|
|
b4b779df5c | ||
|
|
1dbf35deb5 | ||
|
|
6ab226c43a | ||
|
|
3a896fc43e | ||
|
|
a81d632878 | ||
|
|
cb8c0d4aa7 | ||
|
|
788a5a3e24 | ||
|
|
4f056bf228 | ||
|
|
6ce9c779c1 | ||
|
|
32fc2ec365 | ||
|
|
9ddd38a334 | ||
|
|
b45e5a5e38 | ||
|
|
135efe6d94 | ||
|
|
69de6d18eb | ||
|
|
627da14a32 | ||
|
|
83bf3620c0 | ||
|
|
fa41bf3c58 | ||
|
|
31e7a101ef | ||
|
|
22f9e9e6b7 | ||
|
|
8834431ba6 | ||
|
|
01cc9b3d07 | ||
|
|
a0cbe1de5d | ||
|
|
75792c0760 | ||
|
|
83ca724120 | ||
|
|
aee6ee1a00 | ||
|
|
e66c69a292 | ||
|
|
930370729b | ||
|
|
2f5b55013a | ||
|
|
cb74fade18 | ||
|
|
e0da422b0e | ||
|
|
77caaa2a55 | ||
|
|
55d03967ec | ||
|
|
684c30893a | ||
|
|
3b51a3a618 | ||
|
|
09bd8137fc | ||
|
|
495daf7967 | ||
|
|
bb3821c254 | ||
|
|
855f8d35f2 | ||
|
|
15dab381af | ||
|
|
9c5a88e2e7 | ||
|
|
80d295cce5 | ||
|
|
76afb70b01 | ||
|
|
c29fba3a2b | ||
|
|
68e41f0860 | ||
|
|
4c68645175 | ||
|
|
aaef8fb29c | ||
|
|
b2e300b6da | ||
|
|
77c840b03a | ||
|
|
98c3408909 | ||
|
|
40f6c2e59d | ||
|
|
64c9bb4231 | ||
|
|
94ef3ea88f | ||
|
|
dab4500b16 | ||
|
|
d951943c67 | ||
|
|
bba6f9349b | ||
|
|
7e0f88ad7a | ||
|
|
88c6cbf943 | ||
|
|
69d31f96de | ||
|
|
a5718ad937 | ||
|
|
505d9c151d | ||
|
|
a637677ec4 | ||
|
|
a0d5421dc8 | ||
|
|
95f62be50c | ||
|
|
d7b5100e35 | ||
|
|
64c1e1fa54 | ||
|
|
74663ea077 | ||
|
|
bc1e397ce3 | ||
|
|
17608cf915 | ||
|
|
a3de574de5 | ||
|
|
22161e6d57 | ||
|
|
c46ed33544 | ||
|
|
7388655e6d | ||
|
|
5b5c186d0c | ||
|
|
ae5d93d6dd | ||
|
|
62f6670a21 | ||
|
|
c9951e7eba | ||
|
|
2da22c08b0 | ||
|
|
e480f53f7f | ||
|
|
8701e67b1e | ||
|
|
97f4a2e651 | ||
|
|
3c3272cb25 | ||
|
|
fa626a53e6 | ||
|
|
76daee3a1b | ||
|
|
1cbf61f4db | ||
|
|
34e57f27ff | ||
|
|
de17ae9969 | ||
|
|
06913a2975 | ||
|
|
dad16f2c57 | ||
|
|
2f22e7295c | ||
|
|
6880f38635 | ||
|
|
a7b1ef19f5 | ||
|
|
aa59da2f22 | ||
|
|
a62a4360e3 | ||
|
|
9e9e666204 | ||
|
|
8d23cbf52b | ||
|
|
d925b37066 | ||
|
|
9dadb35b98 | ||
|
|
79e3e31028 | ||
|
|
0af8e84d2d | ||
|
|
573dde97e5 | ||
|
|
08d112a96f | ||
|
|
76b6b0dead | ||
|
|
e04133d34a | ||
|
|
07575ae239 | ||
|
|
8e7acd8946 | ||
|
|
3ecc926298 | ||
|
|
1e532624af | ||
|
|
8a5194e604 | ||
|
|
8a73cf72c2 | ||
|
|
76982c5988 | ||
|
|
b9dfe5e359 | ||
|
|
a5e13ca776 | ||
|
|
e2ddfbff9c | ||
|
|
66b4c7891d | ||
|
|
480a76c290 | ||
|
|
1373ab255d | ||
|
|
1dc00eb445 | ||
|
|
a366bec684 | ||
|
|
ecca6e9f49 | ||
|
|
03db7a9bbd | ||
|
|
9cb04466c1 | ||
|
|
2bae37d0c5 | ||
|
|
0dbd23c52b | ||
|
|
66a6311dcc | ||
|
|
c5b111530c | ||
|
|
77724a50a4 | ||
|
|
22cbd01c57 | ||
|
|
fd55a624a7 | ||
|
|
75984e954e | ||
|
|
3fce120578 | ||
|
|
6e8fb22c71 | ||
|
|
8ec7a4898d | ||
|
|
642848d331 | ||
|
|
c9e6a0339e | ||
|
|
25620e8670 | ||
|
|
5b804e8f3a | ||
|
|
548db6a5cd | ||
|
|
7f28f64cbe | ||
|
|
9bad31af84 | ||
|
|
01c7a05841 | ||
|
|
9859b4a3d9 | ||
|
|
177084fe8b | ||
|
|
c57a91bc64 | ||
|
|
ca67a40c72 | ||
|
|
de7505bbe6 | ||
|
|
97956ce951 | ||
|
|
8a38e124fd | ||
|
|
38fcffe871 | ||
|
|
4c7b5a47d3 | ||
|
|
34597e6ecb | ||
|
|
735be4f467 | ||
|
|
1c737d77fb | ||
|
|
55788ac04d | ||
|
|
d108ab0339 | ||
|
|
5928eea83e | ||
|
|
27898aa3b5 | ||
|
|
5e3322c538 | ||
|
|
80c31e8660 | ||
|
|
46401ee187 | ||
|
|
3610becc64 | ||
|
|
06d9c157d8 | ||
|
|
d0d1f40128 | ||
|
|
383d5464b7 | ||
|
|
62d15536df | ||
|
|
147cdf2cce | ||
|
|
dd27d69e97 | ||
|
|
32fd0911a2 | ||
|
|
0e6ec58a83 | ||
|
|
69f5963f6f | ||
|
|
6ca708f523 | ||
|
|
9e7af8369e | ||
|
|
b05d8c930d | ||
|
|
6b886b938c | ||
|
|
4a7bf39723 | ||
|
|
7fcd320e23 | ||
|
|
88677ce236 | ||
|
|
d2cf060473 | ||
|
|
3b7b72d4e1 | ||
|
|
4e69b80a98 | ||
|
|
0f52258d53 | ||
|
|
4eadd4cb2f | ||
|
|
579b8a3d3b | ||
|
|
849b3de7d3 | ||
|
|
8855b2846d | ||
|
|
c64addb976 | ||
|
|
fab1304bcd | ||
|
|
bd834fb4d7 | ||
|
|
dcee9582bd | ||
|
|
89e500edfd | ||
|
|
ea83020714 | ||
|
|
6d62744667 | ||
|
|
08c68e26c1 | ||
|
|
574568e71d | ||
|
|
c83c818380 | ||
|
|
a2df38b1ca | ||
|
|
89510c4a65 | ||
|
|
b5a2f68bde | ||
|
|
1ffab661da | ||
|
|
bf0a627a4e | ||
|
|
df764ce8b4 | ||
|
|
a61d4ab88c | ||
|
|
01e7e924c4 | ||
|
|
5f5df99dab | ||
|
|
77e40e8e53 | ||
|
|
d3853c1a54 |
@@ -19,10 +19,10 @@ indent_size = 4
|
||||
dotnet_sort_system_directives_first = true
|
||||
|
||||
# Avoid "this." and "Me." if not necessary
|
||||
dotnet_style_qualification_for_field = false:refactoring
|
||||
dotnet_style_qualification_for_property = false:refactoring
|
||||
dotnet_style_qualification_for_method = false:refactoring
|
||||
dotnet_style_qualification_for_event = false:refactoring
|
||||
dotnet_style_qualification_for_field = false:warning
|
||||
dotnet_style_qualification_for_property = false:warning
|
||||
dotnet_style_qualification_for_method = false:warning
|
||||
dotnet_style_qualification_for_event = false:warning
|
||||
|
||||
# Indentation preferences
|
||||
csharp_indent_block_contents = true
|
||||
@@ -32,10 +32,6 @@ csharp_indent_case_contents_when_block = true
|
||||
csharp_indent_switch_labels = true
|
||||
csharp_indent_labels = flush_left
|
||||
|
||||
dotnet_style_qualification_for_field = false:suggestion
|
||||
dotnet_style_qualification_for_property = false:suggestion
|
||||
dotnet_style_qualification_for_method = false:suggestion
|
||||
dotnet_style_qualification_for_event = false:suggestion
|
||||
dotnet_naming_style.instance_field_style.capitalization = camel_case
|
||||
dotnet_naming_style.instance_field_style.required_prefix = _
|
||||
|
||||
|
||||
16
.github/label-actions.yml
vendored
Normal file
16
.github/label-actions.yml
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
# Configuration for Label Actions - https://github.com/dessant/label-actions
|
||||
|
||||
'Type: Support':
|
||||
comment: >
|
||||
:wave: @{issue-author}, we use the issue tracker exclusively
|
||||
for bug reports and feature requests. However, this issue appears
|
||||
to be a support request. Please hop over onto our [Discord](https://prowlarr.com/discord)
|
||||
or [Subreddit](https://reddit.com/r/prowlarr)
|
||||
close: true
|
||||
|
||||
'Type: Indexer Request':
|
||||
comment: >
|
||||
:wave: @{issue-author}, we use the issue tracker exclusively
|
||||
for bug reports and feature requests. However, this issue appears
|
||||
to be a indexer request. Please use our Indexer request [site](https://requests.prowlarr.com/)
|
||||
close: true
|
||||
23
.github/workflows/label-actions.yml
vendored
Normal file
23
.github/workflows/label-actions.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
name: 'Label Actions'
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled, unlabeled]
|
||||
pull_request:
|
||||
types: [labeled, unlabeled]
|
||||
discussion:
|
||||
types: [labeled, unlabeled]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
discussions: write
|
||||
|
||||
jobs:
|
||||
action:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/label-actions@v2
|
||||
with:
|
||||
process-only: 'issues, prs'
|
||||
21
.github/workflows/support.yml
vendored
21
.github/workflows/support.yml
vendored
@@ -1,21 +0,0 @@
|
||||
name: 'Support requests'
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled, unlabeled, reopened]
|
||||
|
||||
jobs:
|
||||
support:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/support-requests@v2
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
support-label: 'Type: Support'
|
||||
issue-comment: >
|
||||
:wave: @{issue-author}, we use the issue tracker exclusively
|
||||
for bug reports and feature requests. However, this issue appears
|
||||
to be a support request. Please hop over onto our [Discord](https://prowlarr.com/discord)
|
||||
or [Subreddit](https://reddit.com/r/prowlarr)
|
||||
close-issue: true
|
||||
lock-issue: false
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -188,6 +188,10 @@ packages.config.md5sum
|
||||
**/.idea/**/*.iml
|
||||
**/.idea/**/contentModel.xml
|
||||
**/.idea/**/modules.xml
|
||||
|
||||
# ignore node_modules symlink
|
||||
node_modules
|
||||
node_modules.nosync
|
||||
|
||||
# API doc generation
|
||||
.config/
|
||||
|
||||
64
README.md
64
README.md
@@ -4,74 +4,84 @@
|
||||
[](https://translate.servarr.com/engage/prowlarr/?utm_source=widget)
|
||||
[](https://wiki.servarr.com/prowlarr/installation#docker)
|
||||

|
||||
[](#backers)
|
||||
[](#backers)
|
||||
[](#sponsors)
|
||||
[](#mega-sponsors)
|
||||
|
||||
Prowlarr is an indexer manager/proxy built on the popular arr .net/reactjs base stack to integrate with your various PVR apps. Prowlarr supports management of both Torrent Trackers and Usenet Indexers. It integrates seamlessly with Lidarr, Mylar3, Radarr, Readarr, and Sonarr offering complete management of your indexers with no per app Indexer setup required (we do it all).
|
||||
Prowlarr is an indexer manager/proxy built on the popular \*arr .net/reactjs base stack to integrate with your various PVR apps. Prowlarr supports management of both Torrent Trackers and Usenet Indexers. It integrates seamlessly with Lidarr, Mylar3, Radarr, Readarr, and Sonarr offering complete management of your indexers with no per app Indexer setup required (we do it all).
|
||||
|
||||
## Major Features Include:
|
||||
- Usenet support for 24 indexers natively, including Headphones VIP, and support for any Newznab compatible indexer via "Generic Newznab"
|
||||
## Major Features Include
|
||||
|
||||
- Usenet support for 24 indexers natively, including Headphones VIP
|
||||
- Usenet support for any Newznab compatible indexer via "Generic Newznab"
|
||||
- Torrent support for over 500 trackers with more added all the time
|
||||
- Torrent support for any Torznab compatible tracker via "Generic Torznab"
|
||||
- Indexer Sync to Sonarr/Radarr/Readarr/Lidarr/Mylar3, so no manual configuration of the other applications are required
|
||||
- Support for custom YML definitions via Cardigann that includes JSON and XML parsing
|
||||
- Indexer Sync to Lidarr/Mylar3/Radarr/Readarr/Sonarr, so no manual configuration of the other applications are required
|
||||
- Indexer history and statistics
|
||||
- Manual searching of Trackers & Indexers at a category level
|
||||
- Support for pushing releases directly to your download clients from Prowlarr
|
||||
- Parameter based manual searching
|
||||
- Support for pushing multiple releases at once directly to your download clients from Prowlarr
|
||||
- Indexer health and status notifications
|
||||
- Per Indexer proxy support (SOCKS4, SOCKS5, HTTP, Flaresolverr)
|
||||
|
||||
## Support
|
||||
|
||||
Note: Prowlarr is currently early in life, thus bugs should be expected
|
||||
|
||||
[](https://wiki.servarr.com/prowlarr)
|
||||
|
||||
[](https://prowlarr.com/discord)
|
||||
[](https://www.reddit.com/r/Prowlarr)
|
||||
|
||||
Note: GitHub Issues are for Bugs and Feature Requests Only
|
||||
|
||||
[](https://github.com/Prowlarr/Prowlarr/issues)
|
||||
[](https://wiki.servarr.com/prowlarr)
|
||||
|
||||
## Indexers/Trackers
|
||||
## Indexers & Trackers
|
||||
|
||||
[Supported Indexers](https://wiki.servarr.com/en/prowlarr/supported-indexers)
|
||||
[](https://wiki.servarr.com/en/prowlarr/supported-indexers)
|
||||
|
||||
[Indexer Requests](https://requests.prowlarr.com)
|
||||
- Request or vote on an existing request for a new tracker/indexer
|
||||
[](https://requests.prowlarr.com)
|
||||
|
||||
## Contributors & Developers
|
||||
|
||||
[API Documentation](https://prowlarr.com/docs/api/)
|
||||
|
||||
This project exists thanks to all the people who contribute.
|
||||
|
||||
- [Contribute (GitHub)](CONTRIBUTING.md)
|
||||
- [Contribution (Wiki Article)](https://wiki.servarr.com/prowlarr/contributing)
|
||||
- [YML Indexer Defintion (Wiki Article)](https://wiki.servarr.com/prowlarr/cardigann-yml-definition)
|
||||
- [YML Indexer Definition (Wiki Article)](https://wiki.servarr.com/prowlarr/cardigann-yml-definition)
|
||||
|
||||
This project exists thanks to all the people who contribute.
|
||||
<a href="https://github.com/Prowlarr/Prowlarr/graphs/contributors"><img src="https://opencollective.com/Prowlarr/contributors.svg?width=890&button=false" /></a>
|
||||
[](https://github.com/Prowlarr/Prowlarr/graphs/contributors)
|
||||
|
||||
## Backers
|
||||
|
||||
Thank you to all our backers! 🙏 [Become a backer](https://opencollective.com/Prowlarr#backer)
|
||||
|
||||
<img src="https://opencollective.com/Prowlarr/backers.svg?width=890"></a>
|
||||

|
||||
|
||||
## Sponsors
|
||||
|
||||
Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [Become a sponsor](https://opencollective.com/Prowlarr#sponsor)
|
||||
|
||||
<img src="https://opencollective.com/Prowlarr/sponsors.svg?width=890"></a>
|
||||

|
||||
|
||||
## Mega Sponsors
|
||||
|
||||
<img src="https://opencollective.com/Prowlarr/tiers/mega-sponsor.svg?width=890"></a>
|
||||

|
||||
|
||||
## JetBrains
|
||||
|
||||
Thank you to [<img src="/Logo/jetbrains.svg" alt="JetBrains" width="32"> JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools.
|
||||
|
||||
* [<img src="/Logo/resharper.svg" alt="ReSharper" width="32"> ReSharper](http://www.jetbrains.com/resharper/)
|
||||
* [<img src="/Logo/webstorm.svg" alt="WebStorm" width="32"> WebStorm](http://www.jetbrains.com/webstorm/)
|
||||
* [<img src="/Logo/rider.svg" alt="Rider" width="32"> Rider](http://www.jetbrains.com/rider/)
|
||||
* [<img src="/Logo/dottrace.svg" alt="dotTrace" width="32"> dotTrace](http://www.jetbrains.com/dottrace/)
|
||||
- [<img src="/Logo/resharper.svg" alt="ReSharper" width="32"> ReSharper](http://www.jetbrains.com/resharper/)
|
||||
- [<img src="/Logo/webstorm.svg" alt="WebStorm" width="32"> WebStorm](http://www.jetbrains.com/webstorm/)
|
||||
- [<img src="/Logo/rider.svg" alt="Rider" width="32"> Rider](http://www.jetbrains.com/rider/)
|
||||
- [<img src="/Logo/dottrace.svg" alt="dotTrace" width="32"> dotTrace](http://www.jetbrains.com/dottrace/)
|
||||
|
||||
### License
|
||||
|
||||
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
|
||||
* Copyright 2010-2021
|
||||
- [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
|
||||
- Copyright 2010-2022
|
||||
|
||||
Icon Credit:
|
||||
<a href="https://www.freepik.com/vectors/box">Box vector created by freepik - www.freepik.com</a>
|
||||
Icon Credit - [Box vector created by freepik - www.freepik.com](https://www.freepik.com/vectors/box)
|
||||
|
||||
@@ -7,13 +7,15 @@ variables:
|
||||
outputFolder: './_output'
|
||||
artifactsFolder: './_artifacts'
|
||||
testsFolder: './_tests'
|
||||
majorVersion: '0.1.5'
|
||||
majorVersion: '0.2.0'
|
||||
minorVersion: $[counter('minorVersion', 1)]
|
||||
prowlarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
|
||||
sentryOrg: 'servarr'
|
||||
sentryUrl: 'https://sentry.servarr.com'
|
||||
dotnetVersion: '6.0.100'
|
||||
dotnetVersion: '6.0.201'
|
||||
innoVersion: '6.2.0'
|
||||
nodeVersion: '16.x'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
|
||||
trigger:
|
||||
@@ -29,6 +31,7 @@ pr:
|
||||
paths:
|
||||
exclude:
|
||||
- src/NzbDrone.Core/Localization/Core
|
||||
- src/Prowlarr.API.*/openapi.json
|
||||
|
||||
stages:
|
||||
- stage: Setup
|
||||
@@ -154,7 +157,7 @@ stages:
|
||||
- task: NodeTool@0
|
||||
displayName: Set Node.js version
|
||||
inputs:
|
||||
versionSpec: '12.x'
|
||||
versionSpec: $(nodeVersion)
|
||||
- checkout: self
|
||||
submodules: true
|
||||
fetchDepth: 1
|
||||
@@ -163,7 +166,6 @@ stages:
|
||||
key: 'yarn | "$(osName)" | yarn.lock'
|
||||
restoreKeys: |
|
||||
yarn | "$(osName)"
|
||||
yarn
|
||||
path: $(yarnCacheFolder)
|
||||
displayName: Cache Yarn packages
|
||||
- bash: ./build.sh --frontend
|
||||
@@ -200,16 +202,11 @@ stages:
|
||||
artifactName: WindowsFrontend
|
||||
targetPath: _output
|
||||
displayName: Fetch Frontend
|
||||
- bash: ./build.sh --packages
|
||||
displayName: Create Packages
|
||||
- bash: |
|
||||
distribution/windows/setup/inno/ISCC.exe distribution/windows/setup/prowlarr.iss //DFramework=net6.0 //DRuntime=win-x86
|
||||
cp distribution/windows/setup/output/Prowlarr.*windows.net6.0.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Prowlarr.${BUILDNAME}.windows-core-x86-installer.exe
|
||||
displayName: Create x86 .NET Core Windows installer
|
||||
- bash: |
|
||||
distribution/windows/setup/inno/ISCC.exe distribution/windows/setup/prowlarr.iss //DFramework=net6.0 //DRuntime=win-x64
|
||||
cp distribution/windows/setup/output/Prowlarr.*windows.net6.0.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Prowlarr.${BUILDNAME}.windows-core-x64-installer.exe
|
||||
displayName: Create x64 .NET Core Windows installer
|
||||
./build.sh --packages --installer
|
||||
cp distribution/windows/setup/output/Prowlarr.*win-x64.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Prowlarr.${BUILDNAME}.windows-core-x64-installer.exe
|
||||
cp distribution/windows/setup/output/Prowlarr.*win-x86.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Prowlarr.${BUILDNAME}.windows-core-x86-installer.exe
|
||||
displayName: Create Installers
|
||||
- publish: $(Build.ArtifactStagingDirectory)
|
||||
artifact: 'WindowsInstaller'
|
||||
displayName: Publish Installer
|
||||
@@ -706,17 +703,17 @@ stages:
|
||||
osName: 'Linux'
|
||||
imageName: 'ubuntu-18.04'
|
||||
pattern: 'Prowlarr.*.linux-core-x64.tar.gz'
|
||||
failBuild: false
|
||||
failBuild: true
|
||||
Mac:
|
||||
osName: 'Mac'
|
||||
imageName: 'macos-10.15'
|
||||
pattern: 'Prowlarr.*.osx-core-x64.tar.gz'
|
||||
failBuild: false
|
||||
failBuild: true
|
||||
Windows:
|
||||
osName: 'Windows'
|
||||
imageName: 'windows-2019'
|
||||
pattern: 'Prowlarr.*.windows-core-x64.zip'
|
||||
failBuild: false
|
||||
failBuild: true
|
||||
|
||||
pool:
|
||||
vmImage: $(imageName)
|
||||
@@ -807,7 +804,7 @@ stages:
|
||||
- task: NodeTool@0
|
||||
displayName: Set Node.js version
|
||||
inputs:
|
||||
versionSpec: '12.x'
|
||||
versionSpec: $(nodeVersion)
|
||||
- checkout: self
|
||||
submodules: true
|
||||
fetchDepth: 1
|
||||
@@ -816,7 +813,6 @@ stages:
|
||||
key: 'yarn | "$(osName)" | yarn.lock'
|
||||
restoreKeys: |
|
||||
yarn | "$(osName)"
|
||||
yarn
|
||||
path: $(yarnCacheFolder)
|
||||
displayName: Cache Yarn packages
|
||||
- bash: ./build.sh --lint
|
||||
@@ -825,6 +821,59 @@ stages:
|
||||
FORCE_COLOR: 0
|
||||
YARN_CACHE_FOLDER: $(yarnCacheFolder)
|
||||
|
||||
- job: Api_Docs
|
||||
displayName: API Docs
|
||||
dependsOn: Prepare
|
||||
condition: |
|
||||
and
|
||||
(
|
||||
and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/develop')),
|
||||
and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
|
||||
)
|
||||
|
||||
pool:
|
||||
vmImage: windows-2019
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Install .net core'
|
||||
inputs:
|
||||
version: $(dotnetVersion)
|
||||
- checkout: self
|
||||
submodules: true
|
||||
persistCredentials: true
|
||||
fetchDepth: 1
|
||||
- bash: ./docs.sh Windows
|
||||
displayName: Create openapi.json
|
||||
- bash: |
|
||||
git config --global user.email "development@lidarr.audio"
|
||||
git config --global user.name "Servarr"
|
||||
git checkout -b api-docs
|
||||
git add .
|
||||
if git status | grep -q modified
|
||||
then
|
||||
git commit -am 'Automated API Docs update'
|
||||
git push -f --set-upstream origin api-docs
|
||||
curl -X POST -H "Authorization: token ${GITHUBTOKEN}" -H "Accept: application/vnd.github.v3+json" https://api.github.com/repos/prowlarr/prowlarr/pulls -d '{"head":"api-docs","base":"develop","title":"Update API docs"}'
|
||||
else
|
||||
echo "No changes since last run"
|
||||
fi
|
||||
displayName: Commit API Doc Change
|
||||
continueOnError: true
|
||||
env:
|
||||
GITHUBTOKEN: $(githubToken)
|
||||
- task: CopyFiles@2
|
||||
displayName: 'Copy openapi.json to: $(Build.ArtifactStagingDirectory)'
|
||||
inputs:
|
||||
SourceFolder: '$(Build.SourcesDirectory)'
|
||||
Contents: |
|
||||
**/*openapi.json
|
||||
TargetFolder: '$(Build.ArtifactStagingDirectory)/api_docs'
|
||||
- publish: $(Build.ArtifactStagingDirectory)/api_docs
|
||||
artifact: 'APIDocs'
|
||||
displayName: Publish API Docs Bundle
|
||||
condition: and(succeeded(), eq(variables['System.JobAttempt'], '1'))
|
||||
|
||||
- job: Analyze_Backend
|
||||
displayName: Backend
|
||||
dependsOn: Prepare
|
||||
@@ -892,7 +941,7 @@ stages:
|
||||
- job:
|
||||
displayName: Discord Notification
|
||||
pool:
|
||||
vmImage: 'windows-2019'
|
||||
vmImage: 'ubuntu-18.04'
|
||||
steps:
|
||||
- task: DownloadPipelineArtifact@2
|
||||
continueOnError: true
|
||||
|
||||
39
build.sh
39
build.sh
@@ -234,6 +234,32 @@ Package()
|
||||
esac
|
||||
}
|
||||
|
||||
BuildInstaller()
|
||||
{
|
||||
local framework="$1"
|
||||
local runtime="$2"
|
||||
|
||||
./_inno/ISCC.exe distribution/windows/setup/prowlarr.iss "//DFramework=$framework" "//DRuntime=$runtime"
|
||||
}
|
||||
|
||||
InstallInno()
|
||||
{
|
||||
ProgressStart "Installing portable Inno Setup"
|
||||
|
||||
rm -rf _inno
|
||||
curl -s --output innosetup.exe "https://files.jrsoftware.org/is/6/innosetup-${INNOVERSION:-6.2.0}.exe"
|
||||
mkdir _inno
|
||||
./innosetup.exe //portable=1 //silent //currentuser //dir=.\\_inno
|
||||
rm innosetup.exe
|
||||
|
||||
ProgressEnd "Installed portable Inno Setup"
|
||||
}
|
||||
|
||||
RemoveInno()
|
||||
{
|
||||
rm -rf _inno
|
||||
}
|
||||
|
||||
PackageTests()
|
||||
{
|
||||
local framework="$1"
|
||||
@@ -265,6 +291,7 @@ if [ $# -eq 0 ]; then
|
||||
BACKEND=YES
|
||||
FRONTEND=YES
|
||||
PACKAGES=YES
|
||||
INSTALLER=NO
|
||||
LINT=YES
|
||||
ENABLE_BSD=NO
|
||||
fi
|
||||
@@ -300,6 +327,10 @@ case $key in
|
||||
PACKAGES=YES
|
||||
shift # past argument
|
||||
;;
|
||||
--installer)
|
||||
INSTALLER=YES
|
||||
shift # past argument
|
||||
;;
|
||||
--lint)
|
||||
LINT=YES
|
||||
shift # past argument
|
||||
@@ -383,3 +414,11 @@ then
|
||||
Package "$FRAMEWORK" "$RID"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$INSTALLER" = "YES" ];
|
||||
then
|
||||
InstallInno
|
||||
BuildInstaller "net6.0" "win-x64"
|
||||
BuildInstaller "net6.0" "win-x86"
|
||||
RemoveInno
|
||||
fi
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>prowlarr.icns</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.osx.prowlarr.video</string>
|
||||
<string>com.osx.prowlarr.com</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
#SET BUILD_NUMBER=1
|
||||
#SET branch=develop
|
||||
inno\ISCC.exe prowlarr.iss
|
||||
@@ -1,336 +0,0 @@
|
||||
; *** Inno Setup version 5.5.3+ English messages ***
|
||||
;
|
||||
; To download user-contributed translations of this file, go to:
|
||||
; http://www.jrsoftware.org/files/istrans/
|
||||
;
|
||||
; Note: When translating this text, do not add periods (.) to the end of
|
||||
; messages that didn't have them already, because on those messages Inno
|
||||
; Setup adds the periods automatically (appending a period would result in
|
||||
; two periods being displayed).
|
||||
|
||||
[LangOptions]
|
||||
; The following three entries are very important. Be sure to read and
|
||||
; understand the '[LangOptions] section' topic in the help file.
|
||||
LanguageName=English
|
||||
LanguageID=$0409
|
||||
LanguageCodePage=0
|
||||
; If the language you are translating to requires special font faces or
|
||||
; sizes, uncomment any of the following entries and change them accordingly.
|
||||
;DialogFontName=
|
||||
;DialogFontSize=8
|
||||
;WelcomeFontName=Verdana
|
||||
;WelcomeFontSize=12
|
||||
;TitleFontName=Arial
|
||||
;TitleFontSize=29
|
||||
;CopyrightFontName=Arial
|
||||
;CopyrightFontSize=8
|
||||
|
||||
[Messages]
|
||||
|
||||
; *** Application titles
|
||||
SetupAppTitle=Setup
|
||||
SetupWindowTitle=Setup - %1
|
||||
UninstallAppTitle=Uninstall
|
||||
UninstallAppFullTitle=%1 Uninstall
|
||||
|
||||
; *** Misc. common
|
||||
InformationTitle=Information
|
||||
ConfirmTitle=Confirm
|
||||
ErrorTitle=Error
|
||||
|
||||
; *** SetupLdr messages
|
||||
SetupLdrStartupMessage=This will install %1. Do you wish to continue?
|
||||
LdrCannotCreateTemp=Unable to create a temporary file. Setup aborted
|
||||
LdrCannotExecTemp=Unable to execute file in the temporary directory. Setup aborted
|
||||
|
||||
; *** Startup error messages
|
||||
LastErrorMessage=%1.%n%nError %2: %3
|
||||
SetupFileMissing=The file %1 is missing from the installation directory. Please correct the problem or obtain a new copy of the program.
|
||||
SetupFileCorrupt=The setup files are corrupted. Please obtain a new copy of the program.
|
||||
SetupFileCorruptOrWrongVer=The setup files are corrupted, or are incompatible with this version of Setup. Please correct the problem or obtain a new copy of the program.
|
||||
InvalidParameter=An invalid parameter was passed on the command line:%n%n%1
|
||||
SetupAlreadyRunning=Setup is already running.
|
||||
WindowsVersionNotSupported=This program does not support the version of Windows your computer is running.
|
||||
WindowsServicePackRequired=This program requires %1 Service Pack %2 or later.
|
||||
NotOnThisPlatform=This program will not run on %1.
|
||||
OnlyOnThisPlatform=This program must be run on %1.
|
||||
OnlyOnTheseArchitectures=This program can only be installed on versions of Windows designed for the following processor architectures:%n%n%1
|
||||
MissingWOW64APIs=The version of Windows you are running does not include functionality required by Setup to perform a 64-bit installation. To correct this problem, please install Service Pack %1.
|
||||
WinVersionTooLowError=This program requires %1 version %2 or later.
|
||||
WinVersionTooHighError=This program cannot be installed on %1 version %2 or later.
|
||||
AdminPrivilegesRequired=You must be logged in as an administrator when installing this program.
|
||||
PowerUserPrivilegesRequired=You must be logged in as an administrator or as a member of the Power Users group when installing this program.
|
||||
SetupAppRunningError=Setup has detected that %1 is currently running.%n%nPlease close all instances of it now, then click OK to continue, or Cancel to exit.
|
||||
UninstallAppRunningError=Uninstall has detected that %1 is currently running.%n%nPlease close all instances of it now, then click OK to continue, or Cancel to exit.
|
||||
|
||||
; *** Misc. errors
|
||||
ErrorCreatingDir=Setup was unable to create the directory "%1"
|
||||
ErrorTooManyFilesInDir=Unable to create a file in the directory "%1" because it contains too many files
|
||||
|
||||
; *** Setup common messages
|
||||
ExitSetupTitle=Exit Setup
|
||||
ExitSetupMessage=Setup is not complete. If you exit now, the program will not be installed.%n%nYou may run Setup again at another time to complete the installation.%n%nExit Setup?
|
||||
AboutSetupMenuItem=&About Setup...
|
||||
AboutSetupTitle=About Setup
|
||||
AboutSetupMessage=%1 version %2%n%3%n%n%1 home page:%n%4
|
||||
AboutSetupNote=
|
||||
TranslatorNote=
|
||||
|
||||
; *** Buttons
|
||||
ButtonBack=< &Back
|
||||
ButtonNext=&Next >
|
||||
ButtonInstall=&Install
|
||||
ButtonOK=OK
|
||||
ButtonCancel=Cancel
|
||||
ButtonYes=&Yes
|
||||
ButtonYesToAll=Yes to &All
|
||||
ButtonNo=&No
|
||||
ButtonNoToAll=N&o to All
|
||||
ButtonFinish=&Finish
|
||||
ButtonBrowse=&Browse...
|
||||
ButtonWizardBrowse=B&rowse...
|
||||
ButtonNewFolder=&Make New Folder
|
||||
|
||||
; *** "Select Language" dialog messages
|
||||
SelectLanguageTitle=Select Setup Language
|
||||
SelectLanguageLabel=Select the language to use during the installation:
|
||||
|
||||
; *** Common wizard text
|
||||
ClickNext=Click Next to continue, or Cancel to exit Setup.
|
||||
BeveledLabel=
|
||||
BrowseDialogTitle=Browse For Folder
|
||||
BrowseDialogLabel=Select a folder in the list below, then click OK.
|
||||
NewFolderName=New Folder
|
||||
|
||||
; *** "Welcome" wizard page
|
||||
WelcomeLabel1=Welcome to the [name] Setup Wizard
|
||||
WelcomeLabel2=This will install [name/ver] on your computer.%n%nIt is recommended that you close all other applications before continuing.
|
||||
|
||||
; *** "Password" wizard page
|
||||
WizardPassword=Password
|
||||
PasswordLabel1=This installation is password protected.
|
||||
PasswordLabel3=Please provide the password, then click Next to continue. Passwords are case-sensitive.
|
||||
PasswordEditLabel=&Password:
|
||||
IncorrectPassword=The password you entered is not correct. Please try again.
|
||||
|
||||
; *** "License Agreement" wizard page
|
||||
WizardLicense=License Agreement
|
||||
LicenseLabel=Please read the following important information before continuing.
|
||||
LicenseLabel3=Please read the following License Agreement. You must accept the terms of this agreement before continuing with the installation.
|
||||
LicenseAccepted=I &accept the agreement
|
||||
LicenseNotAccepted=I &do not accept the agreement
|
||||
|
||||
; *** "Information" wizard pages
|
||||
WizardInfoBefore=Information
|
||||
InfoBeforeLabel=Please read the following important information before continuing.
|
||||
InfoBeforeClickLabel=When you are ready to continue with Setup, click Next.
|
||||
WizardInfoAfter=Information
|
||||
InfoAfterLabel=Please read the following important information before continuing.
|
||||
InfoAfterClickLabel=When you are ready to continue with Setup, click Next.
|
||||
|
||||
; *** "User Information" wizard page
|
||||
WizardUserInfo=User Information
|
||||
UserInfoDesc=Please enter your information.
|
||||
UserInfoName=&User Name:
|
||||
UserInfoOrg=&Organization:
|
||||
UserInfoSerial=&Serial Number:
|
||||
UserInfoNameRequired=You must enter a name.
|
||||
|
||||
; *** "Select Destination Location" wizard page
|
||||
WizardSelectDir=Select Destination Location
|
||||
SelectDirDesc=Where should [name] be installed?
|
||||
SelectDirLabel3=Setup will install [name] into the following folder.
|
||||
SelectDirBrowseLabel=To continue, click Next. If you would like to select a different folder, click Browse.
|
||||
DiskSpaceMBLabel=At least [mb] MB of free disk space is required.
|
||||
CannotInstallToNetworkDrive=Setup cannot install to a network drive.
|
||||
CannotInstallToUNCPath=Setup cannot install to a UNC path.
|
||||
InvalidPath=You must enter a full path with drive letter; for example:%n%nC:\APP%n%nor a UNC path in the form:%n%n\\server\share
|
||||
InvalidDrive=The drive or UNC share you selected does not exist or is not accessible. Please select another.
|
||||
DiskSpaceWarningTitle=Not Enough Disk Space
|
||||
DiskSpaceWarning=Setup requires at least %1 KB of free space to install, but the selected drive only has %2 KB available.%n%nDo you want to continue anyway?
|
||||
DirNameTooLong=The folder name or path is too long.
|
||||
InvalidDirName=The folder name is not valid.
|
||||
BadDirName32=Folder names cannot include any of the following characters:%n%n%1
|
||||
DirExistsTitle=Folder Exists
|
||||
DirExists=The folder:%n%n%1%n%nalready exists. Would you like to install to that folder anyway?
|
||||
DirDoesntExistTitle=Folder Does Not Exist
|
||||
DirDoesntExist=The folder:%n%n%1%n%ndoes not exist. Would you like the folder to be created?
|
||||
|
||||
; *** "Select Components" wizard page
|
||||
WizardSelectComponents=Select Components
|
||||
SelectComponentsDesc=Which components should be installed?
|
||||
SelectComponentsLabel2=Select the components you want to install; clear the components you do not want to install. Click Next when you are ready to continue.
|
||||
FullInstallation=Full installation
|
||||
; if possible don't translate 'Compact' as 'Minimal' (I mean 'Minimal' in your language)
|
||||
CompactInstallation=Compact installation
|
||||
CustomInstallation=Custom installation
|
||||
NoUninstallWarningTitle=Components Exist
|
||||
NoUninstallWarning=Setup has detected that the following components are already installed on your computer:%n%n%1%n%nDeselecting these components will not uninstall them.%n%nWould you like to continue anyway?
|
||||
ComponentSize1=%1 KB
|
||||
ComponentSize2=%1 MB
|
||||
ComponentsDiskSpaceMBLabel=Current selection requires at least [mb] MB of disk space.
|
||||
|
||||
; *** "Select Additional Tasks" wizard page
|
||||
WizardSelectTasks=Select Additional Tasks
|
||||
SelectTasksDesc=Which additional tasks should be performed?
|
||||
SelectTasksLabel2=Select the additional tasks you would like Setup to perform while installing [name], then click Next.
|
||||
|
||||
; *** "Select Start Menu Folder" wizard page
|
||||
WizardSelectProgramGroup=Select Start Menu Folder
|
||||
SelectStartMenuFolderDesc=Where should Setup place the program's shortcuts?
|
||||
SelectStartMenuFolderLabel3=Setup will create the program's shortcuts in the following Start Menu folder.
|
||||
SelectStartMenuFolderBrowseLabel=To continue, click Next. If you would like to select a different folder, click Browse.
|
||||
MustEnterGroupName=You must enter a folder name.
|
||||
GroupNameTooLong=The folder name or path is too long.
|
||||
InvalidGroupName=The folder name is not valid.
|
||||
BadGroupName=The folder name cannot include any of the following characters:%n%n%1
|
||||
NoProgramGroupCheck2=&Don't create a Start Menu folder
|
||||
|
||||
; *** "Ready to Install" wizard page
|
||||
WizardReady=Ready to Install
|
||||
ReadyLabel1=Setup is now ready to begin installing [name] on your computer.
|
||||
ReadyLabel2a=Click Install to continue with the installation, or click Back if you want to review or change any settings.
|
||||
ReadyLabel2b=Click Install to continue with the installation.
|
||||
ReadyMemoUserInfo=User information:
|
||||
ReadyMemoDir=Destination location:
|
||||
ReadyMemoType=Setup type:
|
||||
ReadyMemoComponents=Selected components:
|
||||
ReadyMemoGroup=Start Menu folder:
|
||||
ReadyMemoTasks=Additional tasks:
|
||||
|
||||
; *** "Preparing to Install" wizard page
|
||||
WizardPreparing=Preparing to Install
|
||||
PreparingDesc=Setup is preparing to install [name] on your computer.
|
||||
PreviousInstallNotCompleted=The installation/removal of a previous program was not completed. You will need to restart your computer to complete that installation.%n%nAfter restarting your computer, run Setup again to complete the installation of [name].
|
||||
CannotContinue=Setup cannot continue. Please click Cancel to exit.
|
||||
ApplicationsFound=The following applications are using files that need to be updated by Setup. It is recommended that you allow Setup to automatically close these applications.
|
||||
ApplicationsFound2=The following applications are using files that need to be updated by Setup. It is recommended that you allow Setup to automatically close these applications. After the installation has completed, Setup will attempt to restart the applications.
|
||||
CloseApplications=&Automatically close the applications
|
||||
DontCloseApplications=&Do not close the applications
|
||||
ErrorCloseApplications=Setup was unable to automatically close all applications. It is recommended that you close all applications using files that need to be updated by Setup before continuing.
|
||||
|
||||
; *** "Installing" wizard page
|
||||
WizardInstalling=Installing
|
||||
InstallingLabel=Please wait while Setup installs [name] on your computer.
|
||||
|
||||
; *** "Setup Completed" wizard page
|
||||
FinishedHeadingLabel=Completing the [name] Setup Wizard
|
||||
FinishedLabelNoIcons=Setup has finished installing [name] on your computer.
|
||||
FinishedLabel=Setup has finished installing [name] on your computer. The application may be launched by selecting the installed icons.
|
||||
ClickFinish=Click Finish to exit Setup.
|
||||
FinishedRestartLabel=To complete the installation of [name], Setup must restart your computer. Would you like to restart now?
|
||||
FinishedRestartMessage=To complete the installation of [name], Setup must restart your computer.%n%nWould you like to restart now?
|
||||
ShowReadmeCheck=Yes, I would like to view the README file
|
||||
YesRadio=&Yes, restart the computer now
|
||||
NoRadio=&No, I will restart the computer later
|
||||
; used for example as 'Run MyProg.exe'
|
||||
RunEntryExec=Run %1
|
||||
; used for example as 'View Readme.txt'
|
||||
RunEntryShellExec=View %1
|
||||
|
||||
; *** "Setup Needs the Next Disk" stuff
|
||||
ChangeDiskTitle=Setup Needs the Next Disk
|
||||
SelectDiskLabel2=Please insert Disk %1 and click OK.%n%nIf the files on this disk can be found in a folder other than the one displayed below, enter the correct path or click Browse.
|
||||
PathLabel=&Path:
|
||||
FileNotInDir2=The file "%1" could not be located in "%2". Please insert the correct disk or select another folder.
|
||||
SelectDirectoryLabel=Please specify the location of the next disk.
|
||||
|
||||
; *** Installation phase messages
|
||||
SetupAborted=Setup was not completed.%n%nPlease correct the problem and run Setup again.
|
||||
EntryAbortRetryIgnore=Click Retry to try again, Ignore to proceed anyway, or Abort to cancel installation.
|
||||
|
||||
; *** Installation status messages
|
||||
StatusClosingApplications=Closing applications...
|
||||
StatusCreateDirs=Creating directories...
|
||||
StatusExtractFiles=Extracting files...
|
||||
StatusCreateIcons=Creating shortcuts...
|
||||
StatusCreateIniEntries=Creating INI entries...
|
||||
StatusCreateRegistryEntries=Creating registry entries...
|
||||
StatusRegisterFiles=Registering files...
|
||||
StatusSavingUninstall=Saving uninstall information...
|
||||
StatusRunProgram=Finishing installation...
|
||||
StatusRestartingApplications=Restarting applications...
|
||||
StatusRollback=Rolling back changes...
|
||||
|
||||
; *** Misc. errors
|
||||
ErrorInternal2=Internal error: %1
|
||||
ErrorFunctionFailedNoCode=%1 failed
|
||||
ErrorFunctionFailed=%1 failed; code %2
|
||||
ErrorFunctionFailedWithMessage=%1 failed; code %2.%n%3
|
||||
ErrorExecutingProgram=Unable to execute file:%n%1
|
||||
|
||||
; *** Registry errors
|
||||
ErrorRegOpenKey=Error opening registry key:%n%1\%2
|
||||
ErrorRegCreateKey=Error creating registry key:%n%1\%2
|
||||
ErrorRegWriteKey=Error writing to registry key:%n%1\%2
|
||||
|
||||
; *** INI errors
|
||||
ErrorIniEntry=Error creating INI entry in file "%1".
|
||||
|
||||
; *** File copying errors
|
||||
FileAbortRetryIgnore=Click Retry to try again, Ignore to skip this file (not recommended), or Abort to cancel installation.
|
||||
FileAbortRetryIgnore2=Click Retry to try again, Ignore to proceed anyway (not recommended), or Abort to cancel installation.
|
||||
SourceIsCorrupted=The source file is corrupted
|
||||
SourceDoesntExist=The source file "%1" does not exist
|
||||
ExistingFileReadOnly=The existing file is marked as read-only.%n%nClick Retry to remove the read-only attribute and try again, Ignore to skip this file, or Abort to cancel installation.
|
||||
ErrorReadingExistingDest=An error occurred while trying to read the existing file:
|
||||
FileExists=The file already exists.%n%nWould you like Setup to overwrite it?
|
||||
ExistingFileNewer=The existing file is newer than the one Setup is trying to install. It is recommended that you keep the existing file.%n%nDo you want to keep the existing file?
|
||||
ErrorChangingAttr=An error occurred while trying to change the attributes of the existing file:
|
||||
ErrorCreatingTemp=An error occurred while trying to create a file in the destination directory:
|
||||
ErrorReadingSource=An error occurred while trying to read the source file:
|
||||
ErrorCopying=An error occurred while trying to copy a file:
|
||||
ErrorReplacingExistingFile=An error occurred while trying to replace the existing file:
|
||||
ErrorRestartReplace=RestartReplace failed:
|
||||
ErrorRenamingTemp=An error occurred while trying to rename a file in the destination directory:
|
||||
ErrorRegisterServer=Unable to register the DLL/OCX: %1
|
||||
ErrorRegSvr32Failed=RegSvr32 failed with exit code %1
|
||||
ErrorRegisterTypeLib=Unable to register the type library: %1
|
||||
|
||||
; *** Post-installation errors
|
||||
ErrorOpeningReadme=An error occurred while trying to open the README file.
|
||||
ErrorRestartingComputer=Setup was unable to restart the computer. Please do this manually.
|
||||
|
||||
; *** Uninstaller messages
|
||||
UninstallNotFound=File "%1" does not exist. Cannot uninstall.
|
||||
UninstallOpenError=File "%1" could not be opened. Cannot uninstall
|
||||
UninstallUnsupportedVer=The uninstall log file "%1" is in a format not recognized by this version of the uninstaller. Cannot uninstall
|
||||
UninstallUnknownEntry=An unknown entry (%1) was encountered in the uninstall log
|
||||
ConfirmUninstall=Are you sure you want to completely remove %1 and all of its components?
|
||||
UninstallOnlyOnWin64=This installation can only be uninstalled on 64-bit Windows.
|
||||
OnlyAdminCanUninstall=This installation can only be uninstalled by a user with administrative privileges.
|
||||
UninstallStatusLabel=Please wait while %1 is removed from your computer.
|
||||
UninstalledAll=%1 was successfully removed from your computer.
|
||||
UninstalledMost=%1 uninstall complete.%n%nSome elements could not be removed. These can be removed manually.
|
||||
UninstalledAndNeedsRestart=To complete the uninstallation of %1, your computer must be restarted.%n%nWould you like to restart now?
|
||||
UninstallDataCorrupted="%1" file is corrupted. Cannot uninstall
|
||||
|
||||
; *** Uninstallation phase messages
|
||||
ConfirmDeleteSharedFileTitle=Remove Shared File?
|
||||
ConfirmDeleteSharedFile2=The system indicates that the following shared file is no longer in use by any programs. Would you like for Uninstall to remove this shared file?%n%nIf any programs are still using this file and it is removed, those programs may not function properly. If you are unsure, choose No. Leaving the file on your system will not cause any harm.
|
||||
SharedFileNameLabel=File name:
|
||||
SharedFileLocationLabel=Location:
|
||||
WizardUninstalling=Uninstall Status
|
||||
StatusUninstalling=Uninstalling %1...
|
||||
|
||||
; *** Shutdown block reasons
|
||||
ShutdownBlockReasonInstallingApp=Installing %1.
|
||||
ShutdownBlockReasonUninstallingApp=Uninstalling %1.
|
||||
|
||||
; The custom messages below aren't used by Setup itself, but if you make
|
||||
; use of them in your scripts, you'll want to translate them.
|
||||
|
||||
[CustomMessages]
|
||||
|
||||
NameAndVersion=%1 version %2
|
||||
AdditionalIcons=Additional icons:
|
||||
CreateDesktopIcon=Create a &desktop icon
|
||||
CreateQuickLaunchIcon=Create a &Quick Launch icon
|
||||
ProgramOnTheWeb=%1 on the Web
|
||||
UninstallProgram=Uninstall %1
|
||||
LaunchProgram=Launch %1
|
||||
AssocFileExtension=&Associate %1 with the %2 file extension
|
||||
AssocingFileExtension=Associating %1 with the %2 file extension...
|
||||
AutoStartProgramGroupDescription=Startup:
|
||||
AutoStartProgram=Automatically start %1
|
||||
AddonHostProgramNotFound=%1 could not be located in the folder you selected.%n%nDo you want to continue anyway?
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 51 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 4.1 KiB |
Binary file not shown.
@@ -3,13 +3,12 @@
|
||||
|
||||
#define AppName "Prowlarr"
|
||||
#define AppPublisher "Team Prowlarr"
|
||||
#define AppURL "https://prowlarr.video/"
|
||||
#define ForumsURL "https://forums.prowlarr.video/"
|
||||
#define AppURL "https://prowlarr.com/"
|
||||
#define ForumsURL "https://prowlarr.com/discord/"
|
||||
#define AppExeName "Prowlarr.exe"
|
||||
#define BaseVersion GetEnv('MAJORVERSION')
|
||||
#define BuildNumber GetEnv('MINORVERSION')
|
||||
#define BuildVersion GetEnv('PROWLARRVERSION')
|
||||
#define BranchName GetEnv('BUILD_SOURCEBRANCHNAME')
|
||||
|
||||
[Setup]
|
||||
; NOTE: The value of AppId uniquely identifies this application.
|
||||
@@ -22,15 +21,15 @@ AppPublisher={#AppPublisher}
|
||||
AppPublisherURL={#AppURL}
|
||||
AppSupportURL={#ForumsURL}
|
||||
AppUpdatesURL={#AppURL}
|
||||
DefaultDirName={commonappdata}\Prowlarr\bin
|
||||
DefaultDirName={commonappdata}\Prowlarr
|
||||
DisableDirPage=yes
|
||||
DefaultGroupName={#AppName}
|
||||
DisableProgramGroupPage=yes
|
||||
OutputBaseFilename=Prowlarr.{#BranchName}.{#BuildVersion}.windows.{#Framework}
|
||||
OutputBaseFilename=Prowlarr.{#BuildVersion}.{#Runtime}
|
||||
SolidCompression=yes
|
||||
AppCopyright=Creative Commons 3.0 License
|
||||
AllowUNCPath=False
|
||||
UninstallDisplayIcon={app}\Prowlarr.exe
|
||||
UninstallDisplayIcon={app}\bin\Prowlarr.exe
|
||||
DisableReadyPage=True
|
||||
CompressionThreads=2
|
||||
Compression=lzma2/normal
|
||||
@@ -38,6 +37,7 @@ AppContact={#ForumsURL}
|
||||
VersionInfoVersion={#BaseVersion}.{#BuildNumber}
|
||||
SetupLogging=yes
|
||||
OutputDir=output
|
||||
WizardStyle=modern
|
||||
|
||||
[Languages]
|
||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||
@@ -48,28 +48,31 @@ Name: "windowsService"; Description: "Install Windows Service (Starts when the c
|
||||
Name: "startupShortcut"; Description: "Create shortcut in Startup folder (Starts when you log into Windows)"; GroupDescription: "Start automatically"; Flags: exclusive unchecked
|
||||
Name: "none"; Description: "Do not start automatically"; GroupDescription: "Start automatically"; Flags: exclusive unchecked
|
||||
|
||||
[Dirs]
|
||||
Name: "{app}"; Permissions: users-modify
|
||||
|
||||
[Files]
|
||||
Source: "..\..\..\_artifacts\{#Runtime}\{#Framework}\Prowlarr\Prowlarr.exe"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "..\..\..\_artifacts\{#Runtime}\{#Framework}\Prowlarr\*"; Excludes: "Prowlarr.Update"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
Source: "..\..\..\_artifacts\{#Runtime}\{#Framework}\Prowlarr\Prowlarr.exe"; DestDir: "{app}\bin"; Flags: ignoreversion
|
||||
Source: "..\..\..\_artifacts\{#Runtime}\{#Framework}\Prowlarr\*"; Excludes: "Prowlarr.Update"; DestDir: "{app}\bin"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
|
||||
|
||||
[Icons]
|
||||
Name: "{group}\{#AppName}"; Filename: "{app}\{#AppExeName}"; Parameters: "/icon"
|
||||
Name: "{commondesktop}\{#AppName}"; Filename: "{app}\{#AppExeName}"; Parameters: "/icon"; Tasks: desktopIcon
|
||||
Name: "{userstartup}\{#AppName}"; Filename: "{app}\Prowlarr.exe"; WorkingDir: "{app}"; Tasks: startupShortcut
|
||||
Name: "{group}\{#AppName}"; Filename: "{app}\bin\{#AppExeName}"; Parameters: "/icon"
|
||||
Name: "{commondesktop}\{#AppName}"; Filename: "{app}\bin\{#AppExeName}"; Parameters: "/icon"; Tasks: desktopIcon
|
||||
Name: "{userstartup}\{#AppName}"; Filename: "{app}\bin\Prowlarr.exe"; WorkingDir: "{app}\bin"; Tasks: startupShortcut
|
||||
|
||||
[InstallDelete]
|
||||
Name: "{app}"; Type: filesandordirs
|
||||
Name: "{app}\bin"; Type: filesandordirs
|
||||
|
||||
[Run]
|
||||
Filename: "{app}\Prowlarr.Console.exe"; StatusMsg: "Removing previous Windows Service"; Parameters: "/u /exitimmediately"; Flags: runhidden waituntilterminated;
|
||||
Filename: "{app}\Prowlarr.Console.exe"; Description: "Enable Access from Other Devices"; StatusMsg: "Enabling Remote access"; Parameters: "/registerurl /exitimmediately"; Flags: postinstall runascurrentuser runhidden waituntilterminated; Tasks: startupShortcut none;
|
||||
Filename: "{app}\Prowlarr.Console.exe"; StatusMsg: "Installing Windows Service"; Parameters: "/i /exitimmediately"; Flags: runhidden waituntilterminated; Tasks: windowsService
|
||||
Filename: "{app}\Prowlarr.exe"; Description: "Open Prowlarr Web UI"; Flags: postinstall skipifsilent nowait; Tasks: windowsService;
|
||||
Filename: "{app}\Prowlarr.exe"; Description: "Start Prowlarr"; Flags: postinstall skipifsilent nowait; Tasks: startupShortcut none;
|
||||
Filename: "{app}\bin\Prowlarr.Console.exe"; StatusMsg: "Removing previous Windows Service"; Parameters: "/u /exitimmediately"; Flags: runhidden waituntilterminated;
|
||||
Filename: "{app}\bin\Prowlarr.Console.exe"; Description: "Enable Access from Other Devices"; StatusMsg: "Enabling Remote access"; Parameters: "/registerurl /exitimmediately"; Flags: postinstall runascurrentuser runhidden waituntilterminated; Tasks: startupShortcut none;
|
||||
Filename: "{app}\bin\Prowlarr.Console.exe"; StatusMsg: "Installing Windows Service"; Parameters: "/i /exitimmediately"; Flags: runhidden waituntilterminated; Tasks: windowsService
|
||||
Filename: "{app}\bin\Prowlarr.exe"; Description: "Open Prowlarr Web UI"; Flags: postinstall skipifsilent nowait; Tasks: windowsService;
|
||||
Filename: "{app}\bin\Prowlarr.exe"; Description: "Start Prowlarr"; Flags: postinstall skipifsilent nowait; Tasks: startupShortcut none;
|
||||
|
||||
[UninstallRun]
|
||||
Filename: "{app}\prowlarr.console.exe"; Parameters: "/u"; Flags: waituntilterminated skipifdoesntexist
|
||||
Filename: "{app}\bin\prowlarr.console.exe"; Parameters: "/u"; Flags: waituntilterminated skipifdoesntexist
|
||||
|
||||
[Code]
|
||||
function PrepareToInstall(var NeedsRestart: Boolean): String;
|
||||
|
||||
38
docs.sh
Normal file
38
docs.sh
Normal file
@@ -0,0 +1,38 @@
|
||||
PLATFORM=$1
|
||||
|
||||
if [ "$PLATFORM" = "Windows" ]; then
|
||||
RUNTIME="win-x64"
|
||||
elif [ "$PLATFORM" = "Linux" ]; then
|
||||
WHERE="linux-x64"
|
||||
elif [ "$PLATFORM" = "Mac" ]; then
|
||||
WHERE="osx-x64"
|
||||
else
|
||||
echo "Platform must be provided as first arguement: Windows, Linux or Mac"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
outputFolder='_output'
|
||||
testPackageFolder='_tests'
|
||||
|
||||
rm -rf $outputFolder
|
||||
rm -rf $testPackageFolder
|
||||
|
||||
slnFile=src/Prowlarr.sln
|
||||
|
||||
platform=Posix
|
||||
|
||||
dotnet clean $slnFile -c Debug
|
||||
dotnet clean $slnFile -c Release
|
||||
|
||||
dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p:RuntimeIdentifiers=$RUNTIME -t:PublishAllRids
|
||||
|
||||
dotnet new tool-manifest
|
||||
dotnet tool install --version 6.3.0 Swashbuckle.AspNetCore.Cli
|
||||
|
||||
dotnet tool run swagger tofile --output ./src/Prowlarr.Api.V1/openapi.json "$outputFolder/net6.0/$RUNTIME/prowlarr.console.dll" v1 &
|
||||
|
||||
sleep 30
|
||||
|
||||
kill %1
|
||||
|
||||
exit 0
|
||||
@@ -77,7 +77,9 @@ function AppUpdatedModalContent(props) {
|
||||
<div>
|
||||
{
|
||||
!update.changes &&
|
||||
<div className={styles.maintenance}>Maintenance release</div>
|
||||
<div className={styles.maintenance}>
|
||||
{translate('MaintenanceRelease')}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -166,7 +166,9 @@ class FilterBuilderModalContent extends Component {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.label}>Filters</div>
|
||||
<div className={styles.label}>
|
||||
{translate('Filters')}
|
||||
</div>
|
||||
|
||||
<div className={styles.rows}>
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@ import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue';
|
||||
import DateFilterBuilderRowValue from './DateFilterBuilderRowValue';
|
||||
import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
|
||||
import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector';
|
||||
import PrivacyFilterBuilderRowValue from './PrivacyFilterBuilderRowValue';
|
||||
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
|
||||
import TagFilterBuilderRowValueConnector from './TagFilterBuilderRowValueConnector';
|
||||
import styles from './FilterBuilderRow.css';
|
||||
@@ -63,6 +64,9 @@ function getRowValueConnector(selectedFilterBuilderProp) {
|
||||
case filterBuilderValueTypes.PROTOCOL:
|
||||
return ProtocolFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.PRIVACY:
|
||||
return PrivacyFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.TAG:
|
||||
return TagFilterBuilderRowValueConnector;
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
|
||||
const privacyTypes = [
|
||||
{ id: 'public', name: translate('Public') },
|
||||
{ id: 'private', name: translate('Private') },
|
||||
{ id: 'semiPrivate', name: translate('SemiPrivate') }
|
||||
];
|
||||
|
||||
function PrivacyFilterBuilderRowValue(props) {
|
||||
return (
|
||||
<FilterBuilderRowValue
|
||||
tagList={privacyTypes}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default PrivacyFilterBuilderRowValue;
|
||||
@@ -68,6 +68,7 @@ function ProviderFieldFormGroup(props) {
|
||||
label,
|
||||
helpText,
|
||||
helpLink,
|
||||
placeholder,
|
||||
value,
|
||||
type,
|
||||
advanced,
|
||||
@@ -100,6 +101,7 @@ function ProviderFieldFormGroup(props) {
|
||||
label={label}
|
||||
helpText={helpText}
|
||||
helpLink={helpLink}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
values={getSelectValues(selectOptions)}
|
||||
errors={errors}
|
||||
@@ -125,6 +127,7 @@ ProviderFieldFormGroup.propTypes = {
|
||||
label: PropTypes.string,
|
||||
helpText: PropTypes.string,
|
||||
helpLink: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
value: PropTypes.any,
|
||||
type: PropTypes.string.isRequired,
|
||||
advanced: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -47,10 +47,6 @@ class Link extends Component {
|
||||
el = 'a';
|
||||
linkProps.href = to;
|
||||
linkProps.target = target || '_self';
|
||||
} else if (to.startsWith(`${window.Prowlarr.urlBase}/`)) {
|
||||
el = RouterLink;
|
||||
linkProps.to = to;
|
||||
linkProps.target = target;
|
||||
} else {
|
||||
el = RouterLink;
|
||||
linkProps.to = `${window.Prowlarr.urlBase}/${to.replace(/^\//, '')}`;
|
||||
|
||||
@@ -37,7 +37,8 @@ function ModalError(props) {
|
||||
{translate('Close')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>);
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
ModalError.propTypes = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { Scrollbars } from 'react-custom-scrollbars';
|
||||
import { Scrollbars } from 'react-custom-scrollbars-2';
|
||||
import { scrollDirections } from 'Helpers/Props';
|
||||
import styles from './OverlayScroller.css';
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ export const DATE = 'date';
|
||||
export const DEFAULT = 'default';
|
||||
export const INDEXER = 'indexer';
|
||||
export const PROTOCOL = 'protocol';
|
||||
export const PRIVACY = 'privacy';
|
||||
export const APP_PROFILE = 'appProfile';
|
||||
export const MOVIE_STATUS = 'movieStatus';
|
||||
export const TAG = 'tag';
|
||||
|
||||
@@ -41,7 +41,7 @@ function HistoryDetails(props) {
|
||||
{
|
||||
!!data &&
|
||||
<DescriptionListItem
|
||||
title={'Query Results'}
|
||||
title={translate('QueryResults')}
|
||||
data={queryResults ? queryResults : '-'}
|
||||
/>
|
||||
}
|
||||
@@ -49,7 +49,7 @@ function HistoryDetails(props) {
|
||||
{
|
||||
!!data &&
|
||||
<DescriptionListItem
|
||||
title={'Categories'}
|
||||
title={translate('Categories')}
|
||||
data={categories ? categories : '-'}
|
||||
/>
|
||||
}
|
||||
@@ -57,7 +57,7 @@ function HistoryDetails(props) {
|
||||
{
|
||||
!!data &&
|
||||
<DescriptionListItem
|
||||
title={'Source'}
|
||||
title={translate('Source')}
|
||||
data={source}
|
||||
/>
|
||||
}
|
||||
@@ -65,7 +65,7 @@ function HistoryDetails(props) {
|
||||
{
|
||||
!!data &&
|
||||
<DescriptionListItem
|
||||
title={'Url'}
|
||||
title={translate('Url')}
|
||||
data={url ? <Link to={url}>{translate('Link')}</Link> : '-'}
|
||||
/>
|
||||
}
|
||||
@@ -93,7 +93,7 @@ function HistoryDetails(props) {
|
||||
{
|
||||
!!data &&
|
||||
<DescriptionListItem
|
||||
title={'Source'}
|
||||
title={translate('Source')}
|
||||
data={source ? source : '-'}
|
||||
/>
|
||||
}
|
||||
@@ -101,7 +101,7 @@ function HistoryDetails(props) {
|
||||
{
|
||||
!!data &&
|
||||
<DescriptionListItem
|
||||
title={'Title'}
|
||||
title={translate('Title')}
|
||||
data={title ? title : '-'}
|
||||
/>
|
||||
}
|
||||
@@ -109,7 +109,7 @@ function HistoryDetails(props) {
|
||||
{
|
||||
!!data &&
|
||||
<DescriptionListItem
|
||||
title={'Url'}
|
||||
title={translate('Url')}
|
||||
data={url ? <Link to={url}>{translate('Link')}</Link> : '-'}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
class HistoryOptions extends Component {
|
||||
|
||||
@@ -56,14 +57,14 @@ class HistoryOptions extends Component {
|
||||
return (
|
||||
<Fragment>
|
||||
<FormGroup>
|
||||
<FormLabel>History Cleanup</FormLabel>
|
||||
<FormLabel>{translate('HistoryCleanup')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="historyCleanupDays"
|
||||
value={historyCleanupDays}
|
||||
helpText="Set to 0 to disable automatic cleanup"
|
||||
helpTextWarning="History items older than the selected number of days will be cleaned up automatically"
|
||||
helpText={translate('HistoryCleanupDaysHelpText')}
|
||||
helpTextWarning={translate('HistoryCleanupDaysHelpTextWarning')}
|
||||
onChange={this.onGlobalInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
@@ -4,7 +4,7 @@ import Modal from 'Components/Modal/Modal';
|
||||
import AddIndexerModalContentConnector from './AddIndexerModalContentConnector';
|
||||
import styles from './AddIndexerModal.css';
|
||||
|
||||
function AddIndexerModal({ isOpen, onModalClose, ...otherProps }) {
|
||||
function AddIndexerModal({ isOpen, onModalClose, onSelectIndexer, ...otherProps }) {
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
@@ -14,6 +14,7 @@ function AddIndexerModal({ isOpen, onModalClose, ...otherProps }) {
|
||||
<AddIndexerModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
onSelectIndexer={onSelectIndexer}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
@@ -21,7 +22,8 @@ function AddIndexerModal({ isOpen, onModalClose, ...otherProps }) {
|
||||
|
||||
AddIndexerModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onSelectIndexer: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AddIndexerModal;
|
||||
|
||||
@@ -15,7 +15,7 @@ import TableBody from 'Components/Table/TableBody';
|
||||
import { kinds, scrollDirections } from 'Helpers/Props';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import SelectIndexerRow from './SelectIndexerRow';
|
||||
import SelectIndexerRowConnector from './SelectIndexerRowConnector';
|
||||
import styles from './AddIndexerModalContent.css';
|
||||
|
||||
const columns = [
|
||||
@@ -37,6 +37,12 @@ const columns = [
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
label: translate('Description'),
|
||||
isSortable: false,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'privacy',
|
||||
label: translate('Privacy'),
|
||||
@@ -56,6 +62,21 @@ const protocols = [
|
||||
}
|
||||
];
|
||||
|
||||
const privacyLevels = [
|
||||
{
|
||||
key: 'private',
|
||||
value: translate('Private')
|
||||
},
|
||||
{
|
||||
key: 'semiPrivate',
|
||||
value: translate('SemiPrivate')
|
||||
},
|
||||
{
|
||||
key: 'public',
|
||||
value: translate('Public')
|
||||
}
|
||||
];
|
||||
|
||||
class AddIndexerModalContent extends Component {
|
||||
|
||||
//
|
||||
@@ -99,10 +120,6 @@ class AddIndexerModalContent extends Component {
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.map((language) => ({ key: language, value: language }));
|
||||
|
||||
const privacyLevels = Array.from(new Set(indexers.map(({ privacy }) => privacy)))
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.map((privacy) => ({ key: privacy, value: privacy }));
|
||||
|
||||
const filteredIndexers = indexers.filter((indexer) => {
|
||||
const { filter, filterProtocols, filterLanguages, filterPrivacyLevels } = this.state;
|
||||
|
||||
@@ -125,12 +142,12 @@ class AddIndexerModalContent extends Component {
|
||||
return true;
|
||||
});
|
||||
|
||||
const errorMessage = getErrorMessage(error, 'Unable to load indexers');
|
||||
const errorMessage = getErrorMessage(error, translate('UnableToLoadIndexers'));
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Add Indexer
|
||||
{translate('AddIndexer')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody
|
||||
@@ -208,7 +225,7 @@ class AddIndexerModalContent extends Component {
|
||||
<TableBody>
|
||||
{
|
||||
filteredIndexers.map((indexer) => (
|
||||
<SelectIndexerRow
|
||||
<SelectIndexerRowConnector
|
||||
key={indexer.name}
|
||||
implementation={indexer.implementation}
|
||||
{...indexer}
|
||||
|
||||
@@ -51,7 +51,7 @@ class AddIndexerModalContentConnector extends Component {
|
||||
|
||||
onIndexerSelect = ({ implementation, name }) => {
|
||||
this.props.selectIndexerSchema({ implementation, name });
|
||||
this.props.onModalClose({ indexerSelected: true });
|
||||
this.props.onSelectIndexer();
|
||||
};
|
||||
|
||||
onSortPress = (sortKey, sortDirection) => {
|
||||
@@ -76,7 +76,8 @@ AddIndexerModalContentConnector.propTypes = {
|
||||
fetchIndexerSchema: PropTypes.func.isRequired,
|
||||
selectIndexerSchema: PropTypes.func.isRequired,
|
||||
setIndexerSchemaSort: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onSelectIndexer: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(AddIndexerModalContentConnector);
|
||||
|
||||
@@ -3,3 +3,9 @@
|
||||
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
.alreadyExistsIcon {
|
||||
margin-left: 10px;
|
||||
color: #37bc9b;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRowButton from 'Components/Table/TableRowButton';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import ProtocolLabel from 'Indexer/Index/Table/ProtocolLabel';
|
||||
import firstCharToUpper from 'Utilities/String/firstCharToUpper';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './SelectIndexerRow.css';
|
||||
|
||||
class SelectIndexerRow extends Component {
|
||||
@@ -27,7 +31,9 @@ class SelectIndexerRow extends Component {
|
||||
protocol,
|
||||
privacy,
|
||||
name,
|
||||
language
|
||||
language,
|
||||
description,
|
||||
isExistingIndexer
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
@@ -40,6 +46,16 @@ class SelectIndexerRow extends Component {
|
||||
|
||||
<TableRowCell>
|
||||
{name}
|
||||
{
|
||||
isExistingIndexer ?
|
||||
<Icon
|
||||
className={styles.alreadyExistsIcon}
|
||||
name={icons.CHECK_CIRCLE}
|
||||
size={15}
|
||||
title={translate('IndexerAlreadySetup')}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
@@ -47,7 +63,11 @@ class SelectIndexerRow extends Component {
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
{privacy}
|
||||
{description}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
{translate(firstCharToUpper(privacy))}
|
||||
</TableRowCell>
|
||||
</TableRowButton>
|
||||
);
|
||||
@@ -59,8 +79,10 @@ SelectIndexerRow.propTypes = {
|
||||
protocol: PropTypes.string.isRequired,
|
||||
privacy: PropTypes.string.isRequired,
|
||||
language: PropTypes.string.isRequired,
|
||||
description: PropTypes.string.isRequired,
|
||||
implementation: PropTypes.string.isRequired,
|
||||
onIndexerSelect: PropTypes.func.isRequired
|
||||
onIndexerSelect: PropTypes.func.isRequired,
|
||||
isExistingIndexer: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default SelectIndexerRow;
|
||||
|
||||
18
frontend/src/Indexer/Add/SelectIndexerRowConnector.js
Normal file
18
frontend/src/Indexer/Add/SelectIndexerRowConnector.js
Normal file
@@ -0,0 +1,18 @@
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createExistingIndexerSelector from 'Store/Selectors/createExistingIndexerSelector';
|
||||
import SelectIndexerRow from './SelectIndexerRow';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createExistingIndexerSelector(),
|
||||
(isExistingIndexer, dimensions) => {
|
||||
return {
|
||||
isExistingIndexer
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(SelectIndexerRow);
|
||||
@@ -39,6 +39,7 @@ function EditIndexerModalContent(props) {
|
||||
const {
|
||||
id,
|
||||
implementationName,
|
||||
definitionName,
|
||||
name,
|
||||
enable,
|
||||
redirect,
|
||||
@@ -50,10 +51,12 @@ function EditIndexerModalContent(props) {
|
||||
priority
|
||||
} = item;
|
||||
|
||||
const indexerDisplayName = implementationName === definitionName ? implementationName : `${implementationName} (${definitionName})`;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{`${id ? translate('EditIndexer') : translate('AddIndexer')} - ${implementationName}`}
|
||||
{`${id ? translate('EditIndexer') : translate('AddIndexer')} - ${indexerDisplayName}`}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
@@ -159,7 +162,7 @@ function EditIndexerModalContent(props) {
|
||||
<FormInputGroup
|
||||
type={inputTypes.TAG}
|
||||
name="tags"
|
||||
helpText="Use tags to specify default clients, specify Indexer Proxies, or just to organize your indexers."
|
||||
helpText={translate('IndexerTagsHelpText')}
|
||||
{...tags}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
|
||||
@@ -193,11 +193,12 @@ class IndexerIndex extends Component {
|
||||
this.setState({ isAddIndexerModalOpen: true });
|
||||
};
|
||||
|
||||
onAddIndexerModalClose = ({ indexerSelected = false } = {}) => {
|
||||
this.setState({
|
||||
isAddIndexerModalOpen: false,
|
||||
isEditIndexerModalOpen: indexerSelected
|
||||
});
|
||||
onAddIndexerModalClose = () => {
|
||||
this.setState({ isAddIndexerModalOpen: false });
|
||||
};
|
||||
|
||||
onAddIndexerSelectIndexer = () => {
|
||||
this.setState({ isEditIndexerModalOpen: true });
|
||||
};
|
||||
|
||||
onEditIndexerModalClose = () => {
|
||||
@@ -302,14 +303,14 @@ class IndexerIndex extends Component {
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label={'Add Indexer'}
|
||||
label={translate('AddIndexer')}
|
||||
iconName={icons.ADD}
|
||||
spinningName={icons.ADD}
|
||||
onPress={this.onAddIndexerPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label={'Test All Indexers'}
|
||||
label={translate('TestAllIndexers')}
|
||||
iconName={icons.TEST}
|
||||
isSpinning={isTestingAll}
|
||||
isDisabled={hasNoIndexer}
|
||||
@@ -321,13 +322,13 @@ class IndexerIndex extends Component {
|
||||
{
|
||||
isMovieEditorActive ?
|
||||
<PageToolbarButton
|
||||
label={'Indexers'}
|
||||
label={translate('Indexers')}
|
||||
iconName={icons.MOVIE_CONTINUING}
|
||||
isDisabled={hasNoIndexer}
|
||||
onPress={this.onMovieEditorTogglePress}
|
||||
/> :
|
||||
<PageToolbarButton
|
||||
label={'Mass Editor'}
|
||||
label={translate('MassEditor')}
|
||||
iconName={icons.EDIT}
|
||||
isDisabled={hasNoIndexer}
|
||||
onPress={this.onMovieEditorTogglePress}
|
||||
@@ -463,6 +464,7 @@ class IndexerIndex extends Component {
|
||||
<AddIndexerModal
|
||||
isOpen={isAddIndexerModalOpen}
|
||||
onModalClose={this.onAddIndexerModalClose}
|
||||
onSelectIndexer={this.onAddIndexerSelectIndexer}
|
||||
/>
|
||||
|
||||
<EditIndexerModalConnector
|
||||
|
||||
@@ -190,7 +190,7 @@ class IndexerIndexRow extends Component {
|
||||
key={name}
|
||||
className={styles[column.name]}
|
||||
>
|
||||
{appProfile.name}
|
||||
{appProfile?.name || ''}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
@@ -240,16 +240,19 @@ class IndexerIndexRow extends Component {
|
||||
>
|
||||
<IconButton
|
||||
name={icons.INFO}
|
||||
title={'Indexer info'}
|
||||
title={translate('IndexerInfo')}
|
||||
onPress={this.onIndexerInfoPress}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
className={styles.externalLink}
|
||||
name={icons.EXTERNAL_LINK}
|
||||
title={'Website'}
|
||||
to={indexerUrls[0].replace('api.', '')}
|
||||
/>
|
||||
{
|
||||
indexerUrls ?
|
||||
<IconButton
|
||||
className={styles.externalLink}
|
||||
name={icons.EXTERNAL_LINK}
|
||||
title={translate('Website')}
|
||||
to={indexerUrls[0].replace('api.', '')}
|
||||
/> : null
|
||||
}
|
||||
|
||||
<IconButton
|
||||
name={icons.EDIT}
|
||||
@@ -289,7 +292,7 @@ class IndexerIndexRow extends Component {
|
||||
|
||||
IndexerIndexRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
indexerUrls: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
indexerUrls: PropTypes.arrayOf(PropTypes.string),
|
||||
protocol: PropTypes.string.isRequired,
|
||||
privacy: PropTypes.string.isRequired,
|
||||
priority: PropTypes.number.isRequired,
|
||||
@@ -298,7 +301,7 @@ IndexerIndexRow.propTypes = {
|
||||
redirect: PropTypes.bool.isRequired,
|
||||
appProfile: PropTypes.object.isRequired,
|
||||
status: PropTypes.object,
|
||||
capabilities: PropTypes.object.isRequired,
|
||||
capabilities: PropTypes.object,
|
||||
added: PropTypes.string.isRequired,
|
||||
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
|
||||
@@ -4,6 +4,7 @@ import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
import DescriptionListItemDescription from 'Components/DescriptionList/DescriptionListItemDescription';
|
||||
import DescriptionListItemTitle from 'Components/DescriptionList/DescriptionListItemTitle';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Link from 'Components/Link/Link';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
@@ -20,6 +21,7 @@ function IndexerInfoModalContent(props) {
|
||||
language,
|
||||
indexerUrls,
|
||||
protocol,
|
||||
capabilities,
|
||||
onModalClose
|
||||
} = props;
|
||||
|
||||
@@ -30,41 +32,78 @@ function IndexerInfoModalContent(props) {
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('Id')}
|
||||
data={id}
|
||||
/>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('Description')}
|
||||
data={description ? description : '-'}
|
||||
/>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('Encoding')}
|
||||
data={encoding ? encoding : '-'}
|
||||
/>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('Language')}
|
||||
data={language ?? '-'}
|
||||
/>
|
||||
|
||||
<DescriptionListItemTitle>Indexer Site</DescriptionListItemTitle>
|
||||
<DescriptionListItemDescription>
|
||||
<Link to={indexerUrls[0]}>{indexerUrls[0]}</Link>
|
||||
</DescriptionListItemDescription>
|
||||
|
||||
<DescriptionListItemTitle>{`${protocol === 'usenet' ? 'Newznab' : 'Torznab'} Url`}</DescriptionListItemTitle>
|
||||
<DescriptionListItemDescription>
|
||||
{`${window.location.origin}${window.Prowlarr.urlBase}/${id}/api`}
|
||||
</DescriptionListItemDescription>
|
||||
|
||||
</DescriptionList>
|
||||
<FieldSet legend={translate('IndexerDetails')}>
|
||||
<div className={styles.groups}>
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('Id')}
|
||||
data={id}
|
||||
/>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('Description')}
|
||||
data={description ? description : '-'}
|
||||
/>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('Encoding')}
|
||||
data={encoding ? encoding : '-'}
|
||||
/>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('Language')}
|
||||
data={language ?? '-'}
|
||||
/>
|
||||
<DescriptionListItemTitle>{translate('IndexerSite')}</DescriptionListItemTitle>
|
||||
<DescriptionListItemDescription>
|
||||
<Link to={indexerUrls[0]}>{indexerUrls[0]}</Link>
|
||||
</DescriptionListItemDescription>
|
||||
<DescriptionListItemTitle>{`${protocol === 'usenet' ? 'Newznab' : 'Torznab'} Url`}</DescriptionListItemTitle>
|
||||
<DescriptionListItemDescription>
|
||||
{`${window.location.origin}${window.Prowlarr.urlBase}/${id}/api`}
|
||||
</DescriptionListItemDescription>
|
||||
</DescriptionList>
|
||||
</div>
|
||||
</FieldSet>
|
||||
<FieldSet legend={translate('SearchCapabilities')}>
|
||||
<div className={styles.groups}>
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('RawSearchSupported')}
|
||||
data={capabilities.supportsRawSearch ? translate('Yes') : translate('No')}
|
||||
/>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('SearchTypes')}
|
||||
data={capabilities.searchParams.length === 0 ? translate('NotSupported') : capabilities.searchParams[0]}
|
||||
/>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('TVSearchTypes')}
|
||||
data={capabilities.tvSearchParams.length === 0 ? translate('NotSupported') : capabilities.tvSearchParams.join(', ')}
|
||||
/>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('MovieSearchTypes')}
|
||||
data={capabilities.movieSearchParams.length === 0 ? translate('NotSupported') : capabilities.movieSearchParams.join(', ')}
|
||||
/>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('BookSearchTypes')}
|
||||
data={capabilities.bookSearchParams.length === 0 ? translate('NotSupported') : capabilities.bookSearchParams.join(', ')}
|
||||
/>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('MusicSearchTypes')}
|
||||
data={capabilities.musicSearchParams.length === 0 ? translate('NotSupported') : capabilities.musicSearchParams.join(', ')}
|
||||
/>
|
||||
</DescriptionList>
|
||||
</div>
|
||||
</FieldSet>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</ModalContent >
|
||||
);
|
||||
}
|
||||
|
||||
@@ -76,6 +115,7 @@ IndexerInfoModalContent.propTypes = {
|
||||
language: PropTypes.string.isRequired,
|
||||
indexerUrls: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
protocol: PropTypes.string.isRequired,
|
||||
capabilities: PropTypes.object.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -7,8 +7,10 @@ import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import { align, kinds } from 'Helpers/Props';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import StatsFilterMenu from './StatsFilterMenu';
|
||||
import styles from './Stats.css';
|
||||
|
||||
function getAverageResponseTimeData(indexerStats) {
|
||||
@@ -144,14 +146,29 @@ function Stats(props) {
|
||||
item,
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error
|
||||
error,
|
||||
filters,
|
||||
selectedFilterKey,
|
||||
onFilterSelect
|
||||
} = props;
|
||||
|
||||
const isLoaded = !!(!error && isPopulated);
|
||||
|
||||
return (
|
||||
<PageContent>
|
||||
<PageToolbar />
|
||||
<PageToolbar>
|
||||
<PageToolbarSection
|
||||
alignContent={align.RIGHT}
|
||||
collapseButtons={false}
|
||||
>
|
||||
<StatsFilterMenu
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
onFilterSelect={onFilterSelect}
|
||||
isDisabled={false}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
<PageContentBody>
|
||||
{
|
||||
isFetching && !isPopulated &&
|
||||
@@ -232,6 +249,10 @@ Stats.propTypes = {
|
||||
item: PropTypes.object.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
selectedFilterKey: PropTypes.string.isRequired,
|
||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onFilterSelect: PropTypes.func.isRequired,
|
||||
error: PropTypes.object,
|
||||
data: PropTypes.object
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchIndexerStats } from 'Store/Actions/indexerStatsActions';
|
||||
import { fetchIndexerStats, setIndexerStatsFilter } from 'Store/Actions/indexerStatsActions';
|
||||
import Stats from './Stats';
|
||||
|
||||
function createMapStateToProps() {
|
||||
@@ -12,9 +12,16 @@ function createMapStateToProps() {
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchFetchIndexers: fetchIndexerStats
|
||||
};
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onFilterSelect(selectedFilterKey) {
|
||||
dispatch(setIndexerStatsFilter({ selectedFilterKey }));
|
||||
},
|
||||
dispatchFetchIndexerStats() {
|
||||
dispatch(fetchIndexerStats());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class StatsConnector extends Component {
|
||||
|
||||
@@ -22,7 +29,7 @@ class StatsConnector extends Component {
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatchFetchIndexers();
|
||||
this.props.dispatchFetchIndexerStats();
|
||||
}
|
||||
|
||||
//
|
||||
@@ -38,7 +45,7 @@ class StatsConnector extends Component {
|
||||
}
|
||||
|
||||
StatsConnector.propTypes = {
|
||||
dispatchFetchIndexers: PropTypes.func.isRequired
|
||||
dispatchFetchIndexerStats: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(StatsConnector);
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(StatsConnector);
|
||||
|
||||
37
frontend/src/Indexer/Stats/StatsFilterMenu.js
Normal file
37
frontend/src/Indexer/Stats/StatsFilterMenu.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import { align } from 'Helpers/Props';
|
||||
|
||||
function StatsFilterMenu(props) {
|
||||
const {
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
isDisabled,
|
||||
onFilterSelect
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<FilterMenu
|
||||
alignMenu={align.RIGHT}
|
||||
isDisabled={isDisabled}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={[]}
|
||||
onFilterSelect={onFilterSelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
StatsFilterMenu.propTypes = {
|
||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isDisabled: PropTypes.bool.isRequired,
|
||||
onFilterSelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
StatsFilterMenu.defaultProps = {
|
||||
showCustomFilters: false
|
||||
};
|
||||
|
||||
export default StatsFilterMenu;
|
||||
24
frontend/src/Indexer/Stats/StatsFilterModalConnector.js
Normal file
24
frontend/src/Indexer/Stats/StatsFilterModalConnector.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import FilterModal from 'Components/Filter/FilterModal';
|
||||
import { setIndexerStatsFilter } from 'Store/Actions/indexerStatsActions';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.indexerStats.items,
|
||||
(state) => state.indexerStats.filterBuilderProps,
|
||||
(sectionItems, filterBuilderProps) => {
|
||||
return {
|
||||
sectionItems,
|
||||
filterBuilderProps,
|
||||
customFilterType: 'indexerStats'
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchSetFilter: setIndexerStatsFilter
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(FilterModal);
|
||||
@@ -248,7 +248,7 @@ class QueryParameterModal extends Component {
|
||||
onSelectionChange={this.onInputSelectionChange}
|
||||
/>
|
||||
<Button onPress={onModalClose}>
|
||||
Close
|
||||
{translate('Close')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
||||
@@ -37,7 +37,10 @@ class SearchFooter extends Component {
|
||||
searchingReleases: false,
|
||||
searchQuery: defaultSearchQuery || '',
|
||||
searchIndexerIds: defaultIndexerIds,
|
||||
searchCategories: defaultCategories
|
||||
searchCategories: defaultCategories,
|
||||
searchLimit: 100,
|
||||
searchOffset: 0,
|
||||
newSearch: true
|
||||
};
|
||||
}
|
||||
|
||||
@@ -115,11 +118,28 @@ class SearchFooter extends Component {
|
||||
};
|
||||
|
||||
onSearchPress = () => {
|
||||
this.props.onSearchPress(this.state.searchQuery, this.state.searchIndexerIds, this.state.searchCategories, this.state.searchType);
|
||||
|
||||
const {
|
||||
searchLimit,
|
||||
searchOffset,
|
||||
searchQuery,
|
||||
searchIndexerIds,
|
||||
searchCategories,
|
||||
searchType
|
||||
} = this.state;
|
||||
|
||||
this.props.onSearchPress(searchQuery, searchIndexerIds, searchCategories, searchType, searchLimit, searchOffset);
|
||||
|
||||
this.setState({ searchOffset: searchOffset + 100, newSearch: false });
|
||||
};
|
||||
|
||||
onSearchInputChange = ({ value }) => {
|
||||
this.setState({ searchQuery: value });
|
||||
this.setState({ searchQuery: value, newSearch: true, searchOffset: 0 });
|
||||
};
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.onInputChange({ name, value });
|
||||
this.setState({ newSearch: true, searchOffset: 0 });
|
||||
};
|
||||
|
||||
//
|
||||
@@ -141,6 +161,7 @@ class SearchFooter extends Component {
|
||||
searchQuery,
|
||||
searchIndexerIds,
|
||||
searchCategories,
|
||||
newSearch,
|
||||
isQueryParameterModalOpen,
|
||||
queryModalOptions,
|
||||
searchType
|
||||
@@ -206,7 +227,7 @@ class SearchFooter extends Component {
|
||||
name='searchIndexerIds'
|
||||
value={searchIndexerIds}
|
||||
isDisabled={isFetching}
|
||||
onChange={onInputChange}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -220,7 +241,7 @@ class SearchFooter extends Component {
|
||||
name='searchCategories'
|
||||
value={searchCategories}
|
||||
isDisabled={isFetching}
|
||||
onChange={onInputChange}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -243,7 +264,7 @@ class SearchFooter extends Component {
|
||||
isDisabled={isFetching || !hasIndexers || selectedCount === 0}
|
||||
onPress={onBulkGrabPress}
|
||||
>
|
||||
{translate('Grab Releases')}
|
||||
{translate('GrabReleases')}
|
||||
</SpinnerButton>
|
||||
}
|
||||
|
||||
@@ -253,7 +274,7 @@ class SearchFooter extends Component {
|
||||
isDisabled={isFetching || !hasIndexers}
|
||||
onPress={this.onSearchPress}
|
||||
>
|
||||
{translate('Search')}
|
||||
{newSearch ? translate('Search') : translate('More')}
|
||||
</SpinnerButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -196,8 +196,8 @@ class SearchIndex extends Component {
|
||||
this.setState({ jumpToCharacter });
|
||||
};
|
||||
|
||||
onSearchPress = (query, indexerIds, categories, type) => {
|
||||
this.props.onSearchPress({ query, indexerIds, categories, type });
|
||||
onSearchPress = (query, indexerIds, categories, type, limit, offset) => {
|
||||
this.props.onSearchPress({ query, indexerIds, categories, type, limit, offset });
|
||||
};
|
||||
|
||||
onBulkGrabPress = () => {
|
||||
|
||||
@@ -299,7 +299,7 @@ class SearchIndexRow extends Component {
|
||||
<IconButton
|
||||
className={styles.downloadLink}
|
||||
name={icons.SAVE}
|
||||
title={'Save'}
|
||||
title={translate('Save')}
|
||||
to={downloadUrl}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
|
||||
@@ -61,7 +61,7 @@ class Applications extends Component {
|
||||
return (
|
||||
<FieldSet legend={translate('Applications')}>
|
||||
<PageSectionContent
|
||||
errorMessage="Unable to load application list"
|
||||
errorMessage={translate('UnableToLoadApplicationList')}
|
||||
{...otherProps}
|
||||
>
|
||||
<div className={styles.applications}>
|
||||
|
||||
@@ -61,7 +61,7 @@ class IndexerProxies extends Component {
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<FieldSet legend={translate('Indexer Proxies')}>
|
||||
<FieldSet legend={translate('IndexerProxies')}>
|
||||
<PageSectionContent
|
||||
errorMessage={translate('UnableToLoadIndexerProxies')}
|
||||
{...otherProps}
|
||||
|
||||
@@ -40,7 +40,7 @@ class Notification extends Component {
|
||||
});
|
||||
};
|
||||
|
||||
onDeleteNotificationModalClose= () => {
|
||||
onDeleteNotificationModalClose = () => {
|
||||
this.setState({ isDeleteNotificationModalOpen: false });
|
||||
};
|
||||
|
||||
@@ -61,12 +61,14 @@ class Notification extends Component {
|
||||
onRename,
|
||||
onDelete,
|
||||
onHealthIssue,
|
||||
onApplicationUpdate,
|
||||
supportsOnGrab,
|
||||
supportsOnDownload,
|
||||
supportsOnUpgrade,
|
||||
supportsOnRename,
|
||||
supportsOnDelete,
|
||||
supportsOnHealthIssue
|
||||
supportsOnHealthIssue,
|
||||
supportsOnApplicationUpdate
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
@@ -82,53 +84,62 @@ class Notification extends Component {
|
||||
{
|
||||
supportsOnGrab && onGrab &&
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
On Grab
|
||||
{translate('OnGrab')}
|
||||
</Label>
|
||||
}
|
||||
|
||||
{
|
||||
supportsOnDelete && onDelete &&
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
On Delete
|
||||
{translate('OnDelete')}
|
||||
</Label>
|
||||
}
|
||||
|
||||
{
|
||||
supportsOnDownload && onDownload &&
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
On Import
|
||||
{translate('OnImport')}
|
||||
</Label>
|
||||
}
|
||||
|
||||
{
|
||||
supportsOnUpgrade && onDownload && onUpgrade &&
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
On Upgrade
|
||||
{translate('OnUpgrade')}
|
||||
</Label>
|
||||
}
|
||||
|
||||
{
|
||||
supportsOnRename && onRename &&
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
On Rename
|
||||
{translate('OnRename')}
|
||||
</Label>
|
||||
}
|
||||
|
||||
{
|
||||
supportsOnHealthIssue && onHealthIssue &&
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
On Health Issue
|
||||
{translate('OnHealthIssue')}
|
||||
</Label>
|
||||
}
|
||||
|
||||
{
|
||||
!onGrab && !onDownload && !onRename && !onHealthIssue && !onDelete &&
|
||||
supportsOnApplicationUpdate && onApplicationUpdate ?
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
{translate('OnApplicationUpdate')}
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!onGrab && !onDownload && !onRename && !onHealthIssue && !onDelete && !onApplicationUpdate ?
|
||||
<Label
|
||||
kind={kinds.DISABLED}
|
||||
outline={true}
|
||||
>
|
||||
Disabled
|
||||
</Label>
|
||||
{translate('Disabled')}
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
|
||||
<EditNotificationModalConnector
|
||||
@@ -161,12 +172,14 @@ Notification.propTypes = {
|
||||
onRename: PropTypes.bool.isRequired,
|
||||
onDelete: PropTypes.bool.isRequired,
|
||||
onHealthIssue: PropTypes.bool.isRequired,
|
||||
onApplicationUpdate: PropTypes.bool.isRequired,
|
||||
supportsOnGrab: PropTypes.bool.isRequired,
|
||||
supportsOnDownload: PropTypes.bool.isRequired,
|
||||
supportsOnDelete: PropTypes.bool.isRequired,
|
||||
supportsOnUpgrade: PropTypes.bool.isRequired,
|
||||
supportsOnRename: PropTypes.bool.isRequired,
|
||||
supportsOnHealthIssue: PropTypes.bool.isRequired,
|
||||
supportsOnApplicationUpdate: PropTypes.bool.isRequired,
|
||||
onConfirmDeleteNotification: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -16,8 +16,10 @@ function NotificationEventItems(props) {
|
||||
|
||||
const {
|
||||
onHealthIssue,
|
||||
onApplicationUpdate,
|
||||
supportsOnHealthIssue,
|
||||
includeHealthWarnings
|
||||
includeHealthWarnings,
|
||||
supportsOnApplicationUpdate
|
||||
} = item;
|
||||
|
||||
return (
|
||||
@@ -53,6 +55,17 @@ function NotificationEventItems(props) {
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div>
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="onApplicationUpdate"
|
||||
helpText={translate('OnApplicationUpdateHelpText')}
|
||||
isDisabled={!supportsOnApplicationUpdate.value}
|
||||
{...onApplicationUpdate}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FormGroup>
|
||||
|
||||
@@ -67,7 +67,7 @@ function TagDetailsModalContent(props) {
|
||||
|
||||
{
|
||||
!!indexerProxies.length &&
|
||||
<FieldSet legend={translate('Indexer Proxies')}>
|
||||
<FieldSet legend={translate('IndexerProxies')}>
|
||||
{
|
||||
indexerProxies.map((item) => {
|
||||
return (
|
||||
|
||||
@@ -14,7 +14,6 @@ function createRemoveItemHandler(section, url) {
|
||||
|
||||
const ajaxOptions = {
|
||||
url: `${url}/${id}?${$.param(queryParams, true)}`,
|
||||
dataType: 'text',
|
||||
method: 'DELETE'
|
||||
};
|
||||
|
||||
|
||||
@@ -106,6 +106,7 @@ export default {
|
||||
selectedSchema.onDownload = selectedSchema.supportsOnDownload;
|
||||
selectedSchema.onUpgrade = selectedSchema.supportsOnUpgrade;
|
||||
selectedSchema.onRename = selectedSchema.supportsOnRename;
|
||||
selectedSchema.onApplicationUpdate = selectedSchema.supportsOnApplicationUpdate;
|
||||
|
||||
return selectedSchema;
|
||||
});
|
||||
|
||||
@@ -118,12 +118,12 @@ export const defaultState = {
|
||||
filterBuilderProps: [
|
||||
{
|
||||
name: 'name',
|
||||
label: 'Indexer Name',
|
||||
label: translate('IndexerName'),
|
||||
type: filterBuilderTypes.STRING
|
||||
},
|
||||
{
|
||||
name: 'enable',
|
||||
label: 'Enabled',
|
||||
label: translate('Enabled'),
|
||||
type: filterBuilderTypes.EXACT,
|
||||
valueType: filterBuilderValueTypes.BOOL
|
||||
},
|
||||
@@ -135,15 +135,21 @@ export const defaultState = {
|
||||
},
|
||||
{
|
||||
name: 'priority',
|
||||
label: 'Priority',
|
||||
label: translate('Priority'),
|
||||
type: filterBuilderTypes.NUMBER
|
||||
},
|
||||
{
|
||||
name: 'protocol',
|
||||
label: 'Protocol',
|
||||
label: translate('Protocol'),
|
||||
type: filterBuilderTypes.EXACT,
|
||||
valueType: filterBuilderValueTypes.PROTOCOL
|
||||
},
|
||||
{
|
||||
name: 'privacy',
|
||||
label: translate('Privacy'),
|
||||
type: filterBuilderTypes.EXACT,
|
||||
valueType: filterBuilderValueTypes.PRIVACY
|
||||
},
|
||||
{
|
||||
name: 'appProfileId',
|
||||
label: translate('AppProfile'),
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import moment from 'moment';
|
||||
import { batchActions } from 'redux-batched-actions';
|
||||
import { filterBuilderTypes, filterBuilderValueTypes } from 'Helpers/Props';
|
||||
import { createThunk, handleThunks } from 'Store/thunks';
|
||||
import createFetchHandler from './Creators/createFetchHandler';
|
||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import { set, update } from './baseActions';
|
||||
import createHandleActions from './Creators/createHandleActions';
|
||||
|
||||
//
|
||||
@@ -15,30 +20,140 @@ export const defaultState = {
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
item: {},
|
||||
start: null,
|
||||
end: null,
|
||||
|
||||
details: {
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
item: []
|
||||
}
|
||||
},
|
||||
|
||||
filters: [
|
||||
{
|
||||
key: 'all',
|
||||
label: translate('All'),
|
||||
filters: []
|
||||
},
|
||||
{
|
||||
key: 'lastSeven',
|
||||
label: 'Last 7 Days',
|
||||
filters: []
|
||||
},
|
||||
{
|
||||
key: 'lastThirty',
|
||||
label: 'Last 30 Days',
|
||||
filters: []
|
||||
},
|
||||
{
|
||||
key: 'lastNinety',
|
||||
label: 'Last 90 Days',
|
||||
filters: []
|
||||
}
|
||||
],
|
||||
|
||||
filterBuilderProps: [
|
||||
{
|
||||
name: 'startDate',
|
||||
label: 'Start Date',
|
||||
type: filterBuilderTypes.EXACT,
|
||||
valueType: filterBuilderValueTypes.DATE
|
||||
},
|
||||
{
|
||||
name: 'endDate',
|
||||
label: 'End Date',
|
||||
type: filterBuilderTypes.EXACT,
|
||||
valueType: filterBuilderValueTypes.DATE
|
||||
}
|
||||
],
|
||||
selectedFilterKey: 'all'
|
||||
};
|
||||
|
||||
export const persistState = [
|
||||
'indexerStats.customFilters',
|
||||
'indexerStats.selectedFilterKey'
|
||||
];
|
||||
|
||||
//
|
||||
// Actions Types
|
||||
|
||||
export const FETCH_INDEXER_STATS = 'indexerStats/fetchIndexerStats';
|
||||
export const SET_INDEXER_STATS_FILTER = 'indexerStats/setIndexerStatsFilter';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
|
||||
export const fetchIndexerStats = createThunk(FETCH_INDEXER_STATS);
|
||||
export const setIndexerStatsFilter = createThunk(SET_INDEXER_STATS_FILTER);
|
||||
|
||||
//
|
||||
// Action Handlers
|
||||
|
||||
export const actionHandlers = handleThunks({
|
||||
[FETCH_INDEXER_STATS]: createFetchHandler(section, '/indexerStats')
|
||||
[FETCH_INDEXER_STATS]: function(getState, payload, dispatch) {
|
||||
const state = getState();
|
||||
const indexerStats = state.indexerStats;
|
||||
|
||||
const requestParams = {
|
||||
endDate: moment().toISOString()
|
||||
};
|
||||
|
||||
if (indexerStats.selectedFilterKey !== 'all') {
|
||||
let dayCount = 7;
|
||||
|
||||
if (indexerStats.selectedFilterKey === 'lastThirty') {
|
||||
dayCount = 30;
|
||||
}
|
||||
|
||||
if (indexerStats.selectedFilterKey === 'lastNinety') {
|
||||
dayCount = 90;
|
||||
}
|
||||
|
||||
requestParams.startDate = moment().add(-dayCount, 'days').endOf('day').toISOString();
|
||||
}
|
||||
|
||||
const basesAttrs = {
|
||||
section,
|
||||
isFetching: true
|
||||
};
|
||||
|
||||
const attrs = basesAttrs;
|
||||
|
||||
dispatch(set(attrs));
|
||||
|
||||
const promise = createAjaxRequest({
|
||||
url: '/indexerStats',
|
||||
data: requestParams
|
||||
}).request;
|
||||
|
||||
promise.done((data) => {
|
||||
dispatch(batchActions([
|
||||
update({ section, data }),
|
||||
|
||||
set({
|
||||
section,
|
||||
isFetching: false,
|
||||
isPopulated: true,
|
||||
error: null
|
||||
})
|
||||
]));
|
||||
});
|
||||
|
||||
promise.fail((xhr) => {
|
||||
dispatch(set({
|
||||
section,
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: xhr
|
||||
}));
|
||||
});
|
||||
},
|
||||
|
||||
[SET_INDEXER_STATS_FILTER]: function(getState, payload, dispatch) {
|
||||
dispatch(set({ section, ...payload }));
|
||||
dispatch(fetchIndexerStats());
|
||||
}
|
||||
});
|
||||
|
||||
//
|
||||
|
||||
@@ -80,8 +80,8 @@ export default function createSentryMiddleware() {
|
||||
return;
|
||||
}
|
||||
|
||||
const dsn = isProduction ? 'https://b0fb75c38ef4487dbf742f79c4ba62d2@sentry.servarr.com/12' :
|
||||
'https://da610619280249f891ec3ee306906793@sentry.servarr.com/13';
|
||||
const dsn = isProduction ? 'https://b233094711fe4430a0b0c5da2e01df93@sentry.servarr.com/28' :
|
||||
'https://116efebd253a4dff9df9475a31510001@sentry.servarr.com/37';
|
||||
|
||||
sentry.init({
|
||||
dsn,
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
function createExclusionMovieSelector() {
|
||||
return createSelector(
|
||||
(state, { tmdbId }) => tmdbId,
|
||||
(state) => state.settings.importExclusions,
|
||||
(tmdbId, importExclusions) => {
|
||||
return _.some(importExclusions.items, { tmdbId });
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default createExclusionMovieSelector;
|
||||
@@ -0,0 +1,15 @@
|
||||
import _ from 'lodash';
|
||||
import { createSelector } from 'reselect';
|
||||
import createAllIndexersSelector from './createAllIndexersSelector';
|
||||
|
||||
function createExistingIndexerSelector() {
|
||||
return createSelector(
|
||||
(state, { definitionName }) => definitionName,
|
||||
createAllIndexersSelector(),
|
||||
(definitionName, indexers) => {
|
||||
return _.some(indexers, { definitionName });
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default createExistingIndexerSelector;
|
||||
@@ -5,13 +5,14 @@ import createAllIndexersSelector from './createAllIndexersSelector';
|
||||
function createProfileInUseSelector(profileProp) {
|
||||
return createSelector(
|
||||
(state, { id }) => id,
|
||||
(state) => state.settings.appProfiles.items,
|
||||
createAllIndexersSelector(),
|
||||
(id, indexers) => {
|
||||
(id, profiles, indexers) => {
|
||||
if (!id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_.some(indexers, { [profileProp]: id })) {
|
||||
if (_.some(indexers, { [profileProp]: id }) || profiles.length <= 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,12 @@ function selectSettings(item, pendingChanges, saveError) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (key === 'definitionName') {
|
||||
result.definitionName = item[key];
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const setting = {
|
||||
value: item[key],
|
||||
errors: _.map(_.remove(validationFailures, (failure) => {
|
||||
|
||||
@@ -20,10 +20,11 @@ class About extends Component {
|
||||
packageVersion,
|
||||
packageAuthor,
|
||||
isNetCore,
|
||||
isMono,
|
||||
isDocker,
|
||||
runtimeVersion,
|
||||
migrationVersion,
|
||||
databaseVersion,
|
||||
databaseType,
|
||||
appData,
|
||||
startupPath,
|
||||
mode,
|
||||
@@ -48,14 +49,6 @@ class About extends Component {
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
isMono &&
|
||||
<DescriptionListItem
|
||||
title={translate('MonoVersion')}
|
||||
data={runtimeVersion}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
isNetCore &&
|
||||
<DescriptionListItem
|
||||
@@ -77,6 +70,11 @@ class About extends Component {
|
||||
data={migrationVersion}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('Database')}
|
||||
data={`${titleCase(databaseType)} ${databaseVersion}`}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('AppDataDirectory')}
|
||||
data={appData}
|
||||
@@ -114,9 +112,10 @@ About.propTypes = {
|
||||
packageVersion: PropTypes.string,
|
||||
packageAuthor: PropTypes.string,
|
||||
isNetCore: PropTypes.bool.isRequired,
|
||||
isMono: PropTypes.bool.isRequired,
|
||||
runtimeVersion: PropTypes.string.isRequired,
|
||||
isDocker: PropTypes.bool.isRequired,
|
||||
databaseType: PropTypes.string.isRequired,
|
||||
databaseVersion: PropTypes.string.isRequired,
|
||||
migrationVersion: PropTypes.number.isRequired,
|
||||
appData: PropTypes.string.isRequired,
|
||||
startupPath: PropTypes.string.isRequired,
|
||||
|
||||
@@ -199,7 +199,7 @@ class QueuedTaskRow extends Component {
|
||||
</span>
|
||||
{
|
||||
clientUserAgent ?
|
||||
<span className={styles.userAgent} title="User-Agent provided by the app that called the API">
|
||||
<span className={styles.userAgent} title={translate('UserAgentProvidedByTheAppThatCalledTheAPI')}>
|
||||
from: {clientUserAgent}
|
||||
</span> :
|
||||
null
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
|
||||
<!-- Chrome, Opera, and Firefox OS -->
|
||||
<meta name="theme-color" content="#3a3f51" />
|
||||
<!-- Chrome, Safari, Opera, and Firefox OS -->
|
||||
<meta name="theme-color" content="#e66001" />
|
||||
<!-- Windows Phone -->
|
||||
<meta name="msapplication-navbutton-color" content="#3a3f51" />
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
|
||||
<!-- Chrome, Opera, and Firefox OS -->
|
||||
<meta name="theme-color" content="#464b51" />
|
||||
<!-- Chrome, Safari, Opera, and Firefox OS -->
|
||||
<meta name="theme-color" content="#e66001" />
|
||||
<!-- Windows Phone -->
|
||||
<meta name="msapplication-navbutton-color" content="#464b51" />
|
||||
|
||||
|
||||
84
package.json
84
package.json
@@ -25,17 +25,17 @@
|
||||
"not chrome < 60"
|
||||
],
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "5.15.3",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.35",
|
||||
"@fortawesome/free-regular-svg-icons": "5.15.3",
|
||||
"@fortawesome/free-solid-svg-icons": "5.15.3",
|
||||
"@fortawesome/react-fontawesome": "0.1.14",
|
||||
"@microsoft/signalr": "6.0.0",
|
||||
"@sentry/browser": "6.15.0",
|
||||
"@sentry/integrations": "6.15.0",
|
||||
"chart.js": "3.2.0",
|
||||
"@fortawesome/fontawesome-free": "6.1.1",
|
||||
"@fortawesome/fontawesome-svg-core": "6.1.1",
|
||||
"@fortawesome/free-regular-svg-icons": "6.1.1",
|
||||
"@fortawesome/free-solid-svg-icons": "6.1.1",
|
||||
"@fortawesome/react-fontawesome": "0.1.18",
|
||||
"@microsoft/signalr": "6.0.3",
|
||||
"@sentry/browser": "6.19.2",
|
||||
"@sentry/integrations": "6.19.2",
|
||||
"chart.js": "3.7.1",
|
||||
"classnames": "2.3.1",
|
||||
"clipboard": "2.0.8",
|
||||
"clipboard": "2.0.10",
|
||||
"connected-react-router": "6.9.1",
|
||||
"element-class": "0.2.2",
|
||||
"filesize": "6.3.0",
|
||||
@@ -45,16 +45,16 @@
|
||||
"jquery": "3.6.0",
|
||||
"lodash": "4.17.21",
|
||||
"mobile-detect": "1.4.5",
|
||||
"moment": "2.29.1",
|
||||
"moment": "2.29.2",
|
||||
"mousetrap": "1.6.5",
|
||||
"normalize.css": "8.0.1",
|
||||
"prop-types": "15.7.2",
|
||||
"qs": "6.10.1",
|
||||
"prop-types": "15.8.1",
|
||||
"qs": "6.10.3",
|
||||
"react": "17.0.2",
|
||||
"react-addons-shallow-compare": "15.6.3",
|
||||
"react-async-script": "1.2.0",
|
||||
"react-autosuggest": "10.1.0",
|
||||
"react-custom-scrollbars": "4.2.1",
|
||||
"react-custom-scrollbars-2": "4.4.0",
|
||||
"react-dnd": "14.0.4",
|
||||
"react-dnd-html5-backend": "14.0.2",
|
||||
"react-dnd-multi-backend": "6.0.2",
|
||||
@@ -78,41 +78,41 @@
|
||||
"reselect": "4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.16.0",
|
||||
"@babel/eslint-parser": "7.16.3",
|
||||
"@babel/plugin-proposal-class-properties": "7.16.0",
|
||||
"@babel/plugin-proposal-decorators": "7.16.4",
|
||||
"@babel/plugin-proposal-export-default-from": "7.16.0",
|
||||
"@babel/plugin-proposal-export-namespace-from": "7.16.0",
|
||||
"@babel/plugin-proposal-function-sent": "7.16.0",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "7.16.0",
|
||||
"@babel/plugin-proposal-numeric-separator": "7.16.0",
|
||||
"@babel/plugin-proposal-optional-chaining": "7.16.0",
|
||||
"@babel/plugin-proposal-throw-expressions": "7.16.0",
|
||||
"@babel/core": "7.17.8",
|
||||
"@babel/eslint-parser": "7.17.0",
|
||||
"@babel/plugin-proposal-class-properties": "7.16.7",
|
||||
"@babel/plugin-proposal-decorators": "7.17.8",
|
||||
"@babel/plugin-proposal-export-default-from": "7.16.7",
|
||||
"@babel/plugin-proposal-export-namespace-from": "7.16.7",
|
||||
"@babel/plugin-proposal-function-sent": "7.16.7",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "7.16.7",
|
||||
"@babel/plugin-proposal-numeric-separator": "7.16.7",
|
||||
"@babel/plugin-proposal-optional-chaining": "7.16.7",
|
||||
"@babel/plugin-proposal-throw-expressions": "7.16.7",
|
||||
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
||||
"@babel/preset-env": "7.16.4",
|
||||
"@babel/preset-react": "7.16.0",
|
||||
"autoprefixer": "10.2.5",
|
||||
"babel-loader": "8.2.3",
|
||||
"@babel/preset-env": "7.16.11",
|
||||
"@babel/preset-react": "7.16.7",
|
||||
"autoprefixer": "10.4.4",
|
||||
"babel-loader": "8.2.4",
|
||||
"babel-plugin-inline-classnames": "2.0.1",
|
||||
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
|
||||
"core-js": "3.11.0",
|
||||
"css-loader": "6.5.1",
|
||||
"eslint": "8.3.0",
|
||||
"core-js": "3.21.1",
|
||||
"css-loader": "6.7.1",
|
||||
"eslint": "8.11.0",
|
||||
"eslint-plugin-filenames": "1.3.2",
|
||||
"eslint-plugin-import": "2.25.3",
|
||||
"eslint-plugin-react": "7.27.1",
|
||||
"eslint-plugin-import": "2.25.4",
|
||||
"eslint-plugin-react": "7.29.4",
|
||||
"eslint-plugin-simple-import-sort": "7.0.0",
|
||||
"esprint": "3.1.0",
|
||||
"esprint": "3.3.0",
|
||||
"file-loader": "6.2.0",
|
||||
"filemanager-webpack-plugin": "6.1.7",
|
||||
"html-webpack-plugin": "5.5.0",
|
||||
"loader-utils": "^3.0.0",
|
||||
"mini-css-extract-plugin": "2.4.5",
|
||||
"postcss": "8.3.11",
|
||||
"mini-css-extract-plugin": "2.6.0",
|
||||
"postcss": "8.4.12",
|
||||
"postcss-color-function": "4.1.0",
|
||||
"postcss-loader": "6.2.0",
|
||||
"postcss-mixins": "8.1.0",
|
||||
"postcss-loader": "6.2.1",
|
||||
"postcss-mixins": "9.0.2",
|
||||
"postcss-nested": "5.0.6",
|
||||
"postcss-simple-vars": "6.0.3",
|
||||
"postcss-url": "10.1.3",
|
||||
@@ -121,11 +121,11 @@
|
||||
"run-sequence": "2.2.1",
|
||||
"streamqueue": "1.1.2",
|
||||
"style-loader": "3.3.1",
|
||||
"stylelint": "14.1.0",
|
||||
"stylelint": "14.6.0",
|
||||
"stylelint-order": "5.0.0",
|
||||
"url-loader": "4.1.1",
|
||||
"webpack": "5.64.2",
|
||||
"webpack-cli": "4.9.1",
|
||||
"webpack": "5.70.0",
|
||||
"webpack-cli": "4.9.2",
|
||||
"webpack-livereload-plugin": "3.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,10 +94,10 @@
|
||||
|
||||
<!-- Standard testing packages -->
|
||||
<ItemGroup Condition="'$(TestProject)'=='true'">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
|
||||
<PackageReference Include="NunitXml.TestLogger" Version="3.0.97" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
|
||||
<PackageReference Include="NunitXml.TestLogger" Version="3.0.117" />
|
||||
<PackageReference Include="coverlet.collector" Version="3.0.4-preview.27.ge7cb7c3b40" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace NzbDrone.Automation.Test
|
||||
public abstract class AutomationTest
|
||||
{
|
||||
private NzbDroneRunner _runner;
|
||||
protected RemoteWebDriver driver;
|
||||
protected WebDriver driver;
|
||||
|
||||
public AutomationTest()
|
||||
{
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using OpenQA.Selenium;
|
||||
using OpenQA.Selenium.Remote;
|
||||
using OpenQA.Selenium.Support.UI;
|
||||
|
||||
namespace NzbDrone.Automation.Test.PageModel
|
||||
{
|
||||
public class PageBase
|
||||
{
|
||||
private readonly RemoteWebDriver _driver;
|
||||
private readonly WebDriver _driver;
|
||||
|
||||
public PageBase(RemoteWebDriver driver)
|
||||
public PageBase(WebDriver driver)
|
||||
{
|
||||
_driver = driver;
|
||||
driver.Manage().Window.Maximize();
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<TargetFrameworks>net6.0</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Selenium.Support" Version="3.141.0" />
|
||||
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="90.0.4430.2400" />
|
||||
<PackageReference Include="Selenium.Support" Version="4.1.0" />
|
||||
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="99.0.4844.5100" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\NzbDrone.Test.Common\Prowlarr.Test.Common.csproj" />
|
||||
|
||||
@@ -229,11 +229,6 @@ namespace NzbDrone.Common.Test.Http
|
||||
[Test]
|
||||
public void should_follow_redirects_to_https()
|
||||
{
|
||||
if (typeof(TDispatcher) == typeof(ManagedHttpDispatcher) && PlatformInfo.IsMono)
|
||||
{
|
||||
Assert.Ignore("Will fail on tls1.2 via managed dispatcher, ignore.");
|
||||
}
|
||||
|
||||
var request = new HttpRequestBuilder($"https://{_httpBinHost}/redirect-to")
|
||||
.AddQueryParam("url", $"https://radarr.video/")
|
||||
.Build();
|
||||
|
||||
@@ -23,20 +23,25 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
||||
[TestCase(@"https://hd-space.org/index.php?page=login: uid=mySecret&pwd=mySecret")]
|
||||
[TestCase(@"https://beyond-hd.me/api/torrents/2b51db35e1912ffc138825a12b9933d2")]
|
||||
[TestCase(@"Req: [POST] https://www3.yggtorrent.nz/user/login: id=mySecret&pass=mySecret&ci_csrf_token=2b51db35e1912ffc138825a12b9933d2")]
|
||||
[TestCase(@"https://torrentseeds.org/api/torrents/filter?api_token=2b51db35e1912ffc138825a12b9933d2&name=&sortField=created_at&sortDirection=desc&perPage=100&page=1")]
|
||||
|
||||
//Indexer Responses
|
||||
|
||||
// avistaz response
|
||||
// avistaz response
|
||||
[TestCase(@"""download"":""https:\/\/avistaz.to\/rss\/download\/2b51db35e1910123321025a12b9933d2\/tb51db35e1910123321025a12b9933d2.torrent"",")]
|
||||
[TestCase(@",""info_hash"":""2b51db35e1910123321025a12b9933d2"",")]
|
||||
|
||||
// danish bytes response
|
||||
// danish bytes response
|
||||
[TestCase(@",""rsskey"":""2b51db35e1910123321025a12b9933d2"",")]
|
||||
[TestCase(@",""passkey"":""2b51db35e1910123321025a12b9933d2"",")]
|
||||
|
||||
// nzbgeek & usenet response
|
||||
// nzbgeek & usenet response
|
||||
[TestCase(@"<guid isPermaLink=""true"">https://api.nzbgeek.info/api?t=details&id=2b51db35e1910123321025a12b9933d2&apikey=2b51db35e1910123321025a12b9933d2</guid>")]
|
||||
|
||||
// UNIT3D Response
|
||||
[TestCase(@"""download_link"":""https://blutopia.xyz/torrent/download/114592.2b51db35e1910123321025a12b9933d2"",")]
|
||||
[TestCase(@"""download_link"":""https://desitorrents.tv/torrent/download/114592.2b51db35e1910123321025a12b9933d2"",")]
|
||||
|
||||
// NzbGet
|
||||
[TestCase(@"{ ""Name"" : ""ControlUsername"", ""Value"" : ""mySecret"" }, { ""Name"" : ""ControlPassword"", ""Value"" : ""mySecret"" }, ")]
|
||||
[TestCase(@"{ ""Name"" : ""Server1.Username"", ""Value"" : ""mySecret"" }, { ""Name"" : ""Server1.Password"", ""Value"" : ""mySecret"" }, ")]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
|
||||
@@ -24,7 +24,8 @@ namespace NzbDrone.Common.Disk
|
||||
"/boot",
|
||||
"/lib",
|
||||
"/sbin",
|
||||
"/proc"
|
||||
"/proc",
|
||||
"/usr/bin"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace NzbDrone.Common.EnvironmentInfo
|
||||
private readonly Logger _logger;
|
||||
private readonly DateTime _startTime = DateTime.UtcNow;
|
||||
|
||||
public RuntimeInfo(IHostLifetime hostLifetime, Logger logger)
|
||||
public RuntimeInfo(Logger logger, IHostLifetime hostLifetime = null)
|
||||
{
|
||||
_logger = logger;
|
||||
|
||||
|
||||
@@ -258,6 +258,11 @@ namespace NzbDrone.Common.Extensions
|
||||
return appFolderInfo.AppDataFolder;
|
||||
}
|
||||
|
||||
public static string GetDataProtectionPath(this IAppFolderInfo appFolderInfo)
|
||||
{
|
||||
return Path.Combine(GetAppDataPath(appFolderInfo), "asp");
|
||||
}
|
||||
|
||||
public static string GetLogFolder(this IAppFolderInfo appFolderInfo)
|
||||
{
|
||||
return Path.Combine(GetAppDataPath(appFolderInfo), "logs");
|
||||
|
||||
@@ -34,19 +34,10 @@ namespace NzbDrone.Common.Http.Dispatchers
|
||||
{
|
||||
var webRequest = (HttpWebRequest)WebRequest.Create((Uri)request.Url);
|
||||
|
||||
if (PlatformInfo.IsMono)
|
||||
{
|
||||
// On Mono GZipStream/DeflateStream leaks memory if an exception is thrown, use an intermediate buffer in that case.
|
||||
webRequest.AutomaticDecompression = DecompressionMethods.None;
|
||||
webRequest.Headers.Add("Accept-Encoding", "gzip");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Deflate is not a standard and could break depending on implementation.
|
||||
// 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
|
||||
webRequest.AutomaticDecompression = DecompressionMethods.GZip;
|
||||
}
|
||||
// Deflate is not a standard and could break depending on implementation.
|
||||
// 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
|
||||
webRequest.AutomaticDecompression = DecompressionMethods.GZip;
|
||||
|
||||
webRequest.Method = request.Method.ToString();
|
||||
webRequest.UserAgent = _userAgentBuilder.GetUserAgent(request.UseSimplifiedUserAgent);
|
||||
@@ -91,9 +82,6 @@ namespace NzbDrone.Common.Http.Dispatchers
|
||||
|
||||
if (httpWebResponse == null)
|
||||
{
|
||||
// Workaround for mono not closing connections properly in certain situations.
|
||||
AbortWebRequest(webRequest);
|
||||
|
||||
// The default messages for WebException on mono are pretty horrible.
|
||||
if (e.Status == WebExceptionStatus.NameResolutionFailure)
|
||||
{
|
||||
@@ -127,19 +115,6 @@ namespace NzbDrone.Common.Http.Dispatchers
|
||||
try
|
||||
{
|
||||
data = await responseStream.ToBytes();
|
||||
|
||||
if (PlatformInfo.IsMono && httpWebResponse.ContentEncoding == "gzip")
|
||||
{
|
||||
using (var compressedStream = new MemoryStream(data))
|
||||
using (var gzip = new GZipStream(compressedStream, CompressionMode.Decompress))
|
||||
using (var decompressedStream = new MemoryStream())
|
||||
{
|
||||
gzip.CopyTo(decompressedStream);
|
||||
data = decompressedStream.ToArray();
|
||||
}
|
||||
|
||||
httpWebResponse.Headers.Remove("Content-Encoding");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -254,7 +229,8 @@ namespace NzbDrone.Common.Http.Dispatchers
|
||||
webRequest.TransferEncoding = header.Value;
|
||||
break;
|
||||
case "User-Agent":
|
||||
throw new NotSupportedException("User-Agent other than Prowlarr not allowed.");
|
||||
webRequest.UserAgent = header.Value;
|
||||
break;
|
||||
case "Proxy-Connection":
|
||||
throw new NotImplementedException();
|
||||
default:
|
||||
@@ -263,36 +239,5 @@ namespace NzbDrone.Common.Http.Dispatchers
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Workaround for mono not closing connections properly on timeouts
|
||||
private void AbortWebRequest(HttpWebRequest webRequest)
|
||||
{
|
||||
// First affected version was mono 5.16
|
||||
if (OsInfo.IsNotWindows && _platformInfo.Version >= new Version(5, 16))
|
||||
{
|
||||
try
|
||||
{
|
||||
var currentOperationInfo = webRequest.GetType().GetField("currentOperation", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
var currentOperation = currentOperationInfo.GetValue(webRequest);
|
||||
|
||||
if (currentOperation != null)
|
||||
{
|
||||
var responseStreamInfo = currentOperation.GetType().GetField("responseStream", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
var responseStream = responseStreamInfo.GetValue(currentOperation) as Stream;
|
||||
|
||||
// Note that responseStream will likely be null once mono fixes it.
|
||||
responseStream?.Dispose();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// This can fail randomly on future mono versions that have been changed/fixed. Log to sentry and ignore.
|
||||
_logger.Trace()
|
||||
.Exception(ex)
|
||||
.Message("Unable to dispose responseStream on mono {0}", _platformInfo.Version)
|
||||
.Write();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cache;
|
||||
@@ -74,7 +75,7 @@ namespace NzbDrone.Common.Http
|
||||
|
||||
do
|
||||
{
|
||||
request.Url += new HttpUri(response.Headers.GetSingleValue("Location"));
|
||||
request.Url = new HttpUri(response.RedirectUrl);
|
||||
autoRedirectChain.Add(request.Url.ToString());
|
||||
|
||||
_logger.Trace("Redirected to {0}", request.Url);
|
||||
@@ -87,7 +88,7 @@ namespace NzbDrone.Common.Http
|
||||
// 302 or 303 should default to GET on redirect even if POST on original
|
||||
if (response.StatusCode == HttpStatusCode.Redirect || response.StatusCode == HttpStatusCode.RedirectMethod)
|
||||
{
|
||||
request.Method = HttpMethod.GET;
|
||||
request.Method = HttpMethod.Get;
|
||||
request.ContentData = null;
|
||||
}
|
||||
|
||||
@@ -263,7 +264,7 @@ namespace NzbDrone.Common.Http
|
||||
|
||||
public Task<HttpResponse> GetAsync(HttpRequest request)
|
||||
{
|
||||
request.Method = HttpMethod.GET;
|
||||
request.Method = HttpMethod.Get;
|
||||
return ExecuteAsync(request);
|
||||
}
|
||||
|
||||
@@ -288,7 +289,7 @@ namespace NzbDrone.Common.Http
|
||||
|
||||
public Task<HttpResponse> HeadAsync(HttpRequest request)
|
||||
{
|
||||
request.Method = HttpMethod.HEAD;
|
||||
request.Method = HttpMethod.Head;
|
||||
return ExecuteAsync(request);
|
||||
}
|
||||
|
||||
@@ -299,7 +300,7 @@ namespace NzbDrone.Common.Http
|
||||
|
||||
public Task<HttpResponse> PostAsync(HttpRequest request)
|
||||
{
|
||||
request.Method = HttpMethod.POST;
|
||||
request.Method = HttpMethod.Post;
|
||||
return ExecuteAsync(request);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
@@ -107,6 +107,18 @@ namespace NzbDrone.Common.Http
|
||||
}
|
||||
}
|
||||
|
||||
public string UserAgent
|
||||
{
|
||||
get
|
||||
{
|
||||
return GetSingleValue("User-Agent");
|
||||
}
|
||||
set
|
||||
{
|
||||
SetSingleValue("User-Agent", value);
|
||||
}
|
||||
}
|
||||
|
||||
public string Accept
|
||||
{
|
||||
get
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
namespace NzbDrone.Common.Http
|
||||
{
|
||||
public enum HttpMethod
|
||||
{
|
||||
GET,
|
||||
POST,
|
||||
PUT,
|
||||
DELETE,
|
||||
HEAD,
|
||||
OPTIONS,
|
||||
PATCH,
|
||||
MERGE
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
@@ -13,6 +14,7 @@ namespace NzbDrone.Common.Http
|
||||
{
|
||||
Url = new HttpUri(url);
|
||||
Headers = new HttpHeader();
|
||||
Method = HttpMethod.Get;
|
||||
ConnectionKeepAlive = true;
|
||||
AllowAutoRedirect = true;
|
||||
Cookies = new Dictionary<string, string>();
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using NzbDrone.Common.Extensions;
|
||||
|
||||
@@ -37,7 +38,7 @@ namespace NzbDrone.Common.Http
|
||||
{
|
||||
BaseUrl = new HttpUri(baseUrl);
|
||||
ResourceUrl = string.Empty;
|
||||
Method = HttpMethod.GET;
|
||||
Method = HttpMethod.Get;
|
||||
Encoding = Encoding.UTF8;
|
||||
QueryParams = new List<KeyValuePair<string, string>>();
|
||||
SuffixQueryParams = new List<KeyValuePair<string, string>>();
|
||||
@@ -275,7 +276,7 @@ namespace NzbDrone.Common.Http
|
||||
|
||||
public virtual HttpRequestBuilder Post()
|
||||
{
|
||||
Method = HttpMethod.POST;
|
||||
Method = HttpMethod.Post;
|
||||
|
||||
return this;
|
||||
}
|
||||
@@ -397,7 +398,7 @@ namespace NzbDrone.Common.Http
|
||||
|
||||
public virtual HttpRequestBuilder AddFormParameter(string key, object value)
|
||||
{
|
||||
if (Method != HttpMethod.POST)
|
||||
if (Method != HttpMethod.Post)
|
||||
{
|
||||
throw new NotSupportedException("HttpRequest Method must be POST to add FormParameter.");
|
||||
}
|
||||
@@ -413,7 +414,7 @@ namespace NzbDrone.Common.Http
|
||||
|
||||
public virtual HttpRequestBuilder AddFormUpload(string name, string fileName, byte[] data, string contentType = "application/octet-stream")
|
||||
{
|
||||
if (Method != HttpMethod.POST)
|
||||
if (Method != HttpMethod.Post)
|
||||
{
|
||||
throw new NotSupportedException("HttpRequest Method must be POST to add FormUpload.");
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace NzbDrone.Common.Http
|
||||
{
|
||||
public class HttpResponse
|
||||
{
|
||||
private static readonly Regex RegexSetCookie = new Regex("^(.*?)=(.*?)(?:;|$)", RegexOptions.Compiled);
|
||||
private static readonly Regex RegexRefresh = new Regex("^(.*?url)=(.*?)(?:;|$)", RegexOptions.Compiled);
|
||||
|
||||
public HttpResponse(HttpRequest request, HttpHeader headers, CookieCollection cookies, byte[] binaryData, long elapsedTime = 0, HttpStatusCode statusCode = HttpStatusCode.OK)
|
||||
{
|
||||
@@ -67,7 +67,8 @@ namespace NzbDrone.Common.Http
|
||||
StatusCode == HttpStatusCode.MovedPermanently ||
|
||||
StatusCode == HttpStatusCode.RedirectMethod ||
|
||||
StatusCode == HttpStatusCode.TemporaryRedirect ||
|
||||
StatusCode == HttpStatusCode.Found;
|
||||
StatusCode == HttpStatusCode.Found ||
|
||||
Headers.ContainsKey("Refresh");
|
||||
|
||||
public string RedirectUrl
|
||||
{
|
||||
@@ -76,6 +77,20 @@ namespace NzbDrone.Common.Http
|
||||
var newUrl = Headers["Location"];
|
||||
if (newUrl == null)
|
||||
{
|
||||
newUrl = Headers["Refresh"];
|
||||
|
||||
if (newUrl == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var match = RegexRefresh.Match(newUrl);
|
||||
|
||||
if (match.Success)
|
||||
{
|
||||
return (Request.Url += new HttpUri(match.Groups[2].Value)).FullUri;
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using Newtonsoft.Json;
|
||||
using NzbDrone.Common.Serializer;
|
||||
|
||||
@@ -17,14 +18,14 @@ namespace NzbDrone.Common.Http
|
||||
public JsonRpcRequestBuilder(string baseUrl)
|
||||
: base(baseUrl)
|
||||
{
|
||||
Method = HttpMethod.POST;
|
||||
Method = HttpMethod.Post;
|
||||
JsonParameters = new List<object>();
|
||||
}
|
||||
|
||||
public JsonRpcRequestBuilder(string baseUrl, string method, IEnumerable<object> parameters)
|
||||
: base(baseUrl)
|
||||
{
|
||||
Method = HttpMethod.POST;
|
||||
Method = HttpMethod.Post;
|
||||
JsonMethod = method;
|
||||
JsonParameters = parameters.ToList();
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace NzbDrone.Common.Instrumentation
|
||||
private static readonly Regex[] CleansingRules = new[]
|
||||
{
|
||||
// Url
|
||||
new Regex(@"(?<=[?&: ;])(apikey|(?:access[-_]?)?token|pass(?:key|wd)?|auth|authkey|user|u?id|api|[a-z_]*apikey|account|pwd)=(?<secret>[^&=]+?)(?= |&|$|<)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"(?<=[?&: ;])(apikey|(?:(?:access|api)[-_]?)?token|pass(?:key|wd)?|auth|authkey|user|u?id|api|[a-z_]*apikey|account|pwd)=(?<secret>[^&=]+?)(?= |&|$|<)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"(?<=[?& ;])[^=]*?(_?(?<!use|get_)token|username|passwo?rd)=(?<secret>[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"rss\.torrentleech\.org/(?!rss)(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"rss\.torrentleech\.org/rss/download/[0-9]+/(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
@@ -21,6 +21,9 @@ namespace NzbDrone.Common.Instrumentation
|
||||
new Regex(@"(?<=authkey = "")(?<secret>[^&=]+?)(?="")", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"(?<=beyond-hd\.[a-z]+/api/torrents/)(?<secret>[^&=][a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
// UNIT3D
|
||||
new Regex(@"(?<=[a-z0-9-]+\.[a-z]+/torrent/download/\d+\.)(?<secret>[^&=][a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
// Path
|
||||
new Regex(@"""C:\\Users\\(?<secret>[^\""]+?)(\\|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"""/home/(?<secret>[^/""]+?)(/|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
@@ -38,16 +38,6 @@ namespace NzbDrone.Common.Instrumentation
|
||||
return;
|
||||
}
|
||||
|
||||
if (PlatformInfo.IsMono)
|
||||
{
|
||||
if ((exception is TypeInitializationException && exception.InnerException is DllNotFoundException) ||
|
||||
exception is DllNotFoundException)
|
||||
{
|
||||
Logger.Debug(exception, "Minor Fail: " + exception.Message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine("EPIC FAIL: {0}", exception);
|
||||
Logger.Fatal(exception, "EPIC FAIL.");
|
||||
}
|
||||
|
||||
@@ -106,13 +106,6 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
||||
o.Debug = false;
|
||||
o.DiagnosticLevel = SentryLevel.Debug;
|
||||
o.Release = BuildInfo.Release;
|
||||
if (PlatformInfo.IsMono)
|
||||
{
|
||||
// Mono 6.0 broke GzipStream.WriteAsync
|
||||
// TODO: Check specific version
|
||||
o.RequestBodyCompressionLevel = System.IO.Compression.CompressionLevel.NoCompression;
|
||||
}
|
||||
|
||||
o.BeforeSend = x => SentryCleanser.CleanseEvent(x);
|
||||
o.BeforeBreadcrumb = x => SentryCleanser.CleanseBreadcrumb(x);
|
||||
o.Environment = BuildInfo.Branch;
|
||||
@@ -155,7 +148,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
||||
{
|
||||
scope.SetTag("is_docker", $"{osInfo.IsDocker}");
|
||||
|
||||
if (osInfo.Name != null && PlatformInfo.IsMono)
|
||||
if (osInfo.Name != null && !OsInfo.IsWindows)
|
||||
{
|
||||
// Sentry auto-detection of non-Windows platforms isn't that accurate on certain devices.
|
||||
scope.Contexts.OperatingSystem.Name = osInfo.Name.FirstCharToUpper();
|
||||
|
||||
@@ -366,11 +366,6 @@ namespace NzbDrone.Common.Processes
|
||||
|
||||
private (string Path, string Args) GetPathAndArgs(string path, string args)
|
||||
{
|
||||
if (PlatformInfo.IsMono && path.EndsWith(".exe", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
return ("mono", $"--debug {path} {args}");
|
||||
}
|
||||
|
||||
if (OsInfo.IsWindows && path.EndsWith(".bat", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
return ("cmd.exe", $"/c {path} {args}");
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
<DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DryIoc.dll" Version="4.8.1" />
|
||||
<PackageReference Include="DryIoc.dll" Version="4.8.8" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
|
||||
<PackageReference Include="NLog.Extensions.Logging" Version="1.7.2" />
|
||||
<PackageReference Include="NLog.Extensions.Logging" Version="1.7.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
<PackageReference Include="NLog" Version="4.7.9" />
|
||||
<PackageReference Include="Sentry" Version="3.8.3" />
|
||||
<PackageReference Include="SharpZipLib" Version="1.3.1" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="NLog" Version="4.7.14" />
|
||||
<PackageReference Include="Sentry" Version="3.15.0" />
|
||||
<PackageReference Include="SharpZipLib" Version="1.3.3" />
|
||||
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
||||
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.0" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Data;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
@@ -38,6 +38,7 @@ namespace NzbDrone.Core.Test.Datastore.SqliteSchemaDumperTests
|
||||
result.Name.Should().Be(tableName);
|
||||
result.Columns.Count.Should().Be(1);
|
||||
result.Columns.First().Name.Should().Be(columnName);
|
||||
result.Columns.First().IsIdentity.Should().BeTrue();
|
||||
}
|
||||
|
||||
[TestCase(@"CREATE INDEX TestIndex ON TestTable (MyId)", "TestIndex", "TestTable", "MyId")]
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Applications;
|
||||
using NzbDrone.Core.Housekeeping.Housekeepers;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
|
||||
{
|
||||
[TestFixture]
|
||||
public class CleanupOrphanedApplicationFixture : DbTest<CleanupOrphanedApplicationStatus, ApplicationStatus>
|
||||
{
|
||||
private ApplicationDefinition _application;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_application = Builder<ApplicationDefinition>.CreateNew()
|
||||
.BuildNew();
|
||||
}
|
||||
|
||||
private void GivenApplication()
|
||||
{
|
||||
Db.Insert(_application);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_delete_orphaned_applicationstatus()
|
||||
{
|
||||
var status = Builder<ApplicationStatus>.CreateNew()
|
||||
.With(h => h.ProviderId = _application.Id)
|
||||
.BuildNew();
|
||||
Db.Insert(status);
|
||||
|
||||
Subject.Clean();
|
||||
AllStoredModels.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_delete_unorphaned_applicationstatus()
|
||||
{
|
||||
GivenApplication();
|
||||
|
||||
var status = Builder<ApplicationStatus>.CreateNew()
|
||||
.With(h => h.ProviderId = _application.Id)
|
||||
.BuildNew();
|
||||
Db.Insert(status);
|
||||
|
||||
Subject.Clean();
|
||||
AllStoredModels.Should().HaveCount(1);
|
||||
AllStoredModels.Should().Contain(h => h.ProviderId == _application.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.Download.Clients.Flood;
|
||||
using NzbDrone.Core.Housekeeping.Housekeepers;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
|
||||
{
|
||||
[TestFixture]
|
||||
public class CleanupOrphanedDownloadClientStatusFixture : DbTest<CleanupOrphanedDownloadClientStatus, DownloadClientStatus>
|
||||
{
|
||||
private DownloadClientDefinition _downloadClient;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_downloadClient = Builder<DownloadClientDefinition>.CreateNew()
|
||||
.With(c => c.Settings = new FloodSettings())
|
||||
.BuildNew();
|
||||
}
|
||||
|
||||
private void GivenClient()
|
||||
{
|
||||
Db.Insert(_downloadClient);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_delete_orphaned_downloadclientstatus()
|
||||
{
|
||||
var status = Builder<DownloadClientStatus>.CreateNew()
|
||||
.With(h => h.ProviderId = _downloadClient.Id)
|
||||
.BuildNew();
|
||||
Db.Insert(status);
|
||||
|
||||
Subject.Clean();
|
||||
AllStoredModels.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_delete_unorphaned_downloadclientstatus()
|
||||
{
|
||||
GivenClient();
|
||||
|
||||
var status = Builder<DownloadClientStatus>.CreateNew()
|
||||
.With(h => h.ProviderId = _downloadClient.Id)
|
||||
.BuildNew();
|
||||
Db.Insert(status);
|
||||
|
||||
Subject.Clean();
|
||||
AllStoredModels.Should().HaveCount(1);
|
||||
AllStoredModels.Should().Contain(h => h.ProviderId == _downloadClient.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FizzWare.NBuilder;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Applications;
|
||||
using NzbDrone.Core.Housekeeping.Housekeepers;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Core.ThingiProvider.Status;
|
||||
|
||||
namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
|
||||
{
|
||||
[TestFixture]
|
||||
public class FixFutureApplicationStatusTimesFixture : CoreTest<FixFutureApplicationStatusTimes>
|
||||
{
|
||||
[Test]
|
||||
public void should_set_disabled_till_when_its_too_far_in_the_future()
|
||||
{
|
||||
var disabledTillTime = EscalationBackOff.Periods[1];
|
||||
var applicationStatuses = Builder<ApplicationStatus>.CreateListOfSize(5)
|
||||
.All()
|
||||
.With(t => t.DisabledTill = DateTime.UtcNow.AddDays(5))
|
||||
.With(t => t.InitialFailure = DateTime.UtcNow.AddDays(-5))
|
||||
.With(t => t.MostRecentFailure = DateTime.UtcNow.AddDays(-5))
|
||||
.With(t => t.EscalationLevel = 1)
|
||||
.BuildListOfNew();
|
||||
|
||||
Mocker.GetMock<IApplicationStatusRepository>()
|
||||
.Setup(s => s.All())
|
||||
.Returns(applicationStatuses);
|
||||
|
||||
Subject.Clean();
|
||||
|
||||
Mocker.GetMock<IApplicationStatusRepository>()
|
||||
.Verify(v => v.UpdateMany(
|
||||
It.Is<List<ApplicationStatus>>(i => i.All(
|
||||
s => s.DisabledTill.Value <= DateTime.UtcNow.AddMinutes(disabledTillTime)))));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_set_initial_failure_when_its_in_the_future()
|
||||
{
|
||||
var applicationStatuses = Builder<ApplicationStatus>.CreateListOfSize(5)
|
||||
.All()
|
||||
.With(t => t.DisabledTill = DateTime.UtcNow.AddDays(-5))
|
||||
.With(t => t.InitialFailure = DateTime.UtcNow.AddDays(5))
|
||||
.With(t => t.MostRecentFailure = DateTime.UtcNow.AddDays(-5))
|
||||
.With(t => t.EscalationLevel = 1)
|
||||
.BuildListOfNew();
|
||||
|
||||
Mocker.GetMock<IApplicationStatusRepository>()
|
||||
.Setup(s => s.All())
|
||||
.Returns(applicationStatuses);
|
||||
|
||||
Subject.Clean();
|
||||
|
||||
Mocker.GetMock<IApplicationStatusRepository>()
|
||||
.Verify(v => v.UpdateMany(
|
||||
It.Is<List<ApplicationStatus>>(i => i.All(
|
||||
s => s.InitialFailure.Value <= DateTime.UtcNow))));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_set_most_recent_failure_when_its_in_the_future()
|
||||
{
|
||||
var applicationStatuses = Builder<ApplicationStatus>.CreateListOfSize(5)
|
||||
.All()
|
||||
.With(t => t.DisabledTill = DateTime.UtcNow.AddDays(-5))
|
||||
.With(t => t.InitialFailure = DateTime.UtcNow.AddDays(-5))
|
||||
.With(t => t.MostRecentFailure = DateTime.UtcNow.AddDays(5))
|
||||
.With(t => t.EscalationLevel = 1)
|
||||
.BuildListOfNew();
|
||||
|
||||
Mocker.GetMock<IApplicationStatusRepository>()
|
||||
.Setup(s => s.All())
|
||||
.Returns(applicationStatuses);
|
||||
|
||||
Subject.Clean();
|
||||
|
||||
Mocker.GetMock<IApplicationStatusRepository>()
|
||||
.Verify(v => v.UpdateMany(
|
||||
It.Is<List<ApplicationStatus>>(i => i.All(
|
||||
s => s.MostRecentFailure.Value <= DateTime.UtcNow))));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_change_statuses_when_times_are_in_the_past()
|
||||
{
|
||||
var indexerStatuses = Builder<ApplicationStatus>.CreateListOfSize(5)
|
||||
.All()
|
||||
.With(t => t.DisabledTill = DateTime.UtcNow.AddDays(-5))
|
||||
.With(t => t.InitialFailure = DateTime.UtcNow.AddDays(-5))
|
||||
.With(t => t.MostRecentFailure = DateTime.UtcNow.AddDays(-5))
|
||||
.With(t => t.EscalationLevel = 0)
|
||||
.BuildListOfNew();
|
||||
|
||||
Mocker.GetMock<IApplicationStatusRepository>()
|
||||
.Setup(s => s.All())
|
||||
.Returns(indexerStatuses);
|
||||
|
||||
Subject.Clean();
|
||||
|
||||
Mocker.GetMock<IApplicationStatusRepository>()
|
||||
.Verify(v => v.UpdateMany(
|
||||
It.Is<List<ApplicationStatus>>(i => i.Count == 0)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FizzWare.NBuilder;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.Housekeeping.Housekeepers;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Core.ThingiProvider.Status;
|
||||
|
||||
namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
|
||||
{
|
||||
[TestFixture]
|
||||
public class FixFutureDownloadClientStatusTimesFixture : CoreTest<FixFutureDownloadClientStatusTimes>
|
||||
{
|
||||
[Test]
|
||||
public void should_set_disabled_till_when_its_too_far_in_the_future()
|
||||
{
|
||||
var disabledTillTime = EscalationBackOff.Periods[1];
|
||||
var clientStatuses = Builder<DownloadClientStatus>.CreateListOfSize(5)
|
||||
.All()
|
||||
.With(t => t.DisabledTill = DateTime.UtcNow.AddDays(5))
|
||||
.With(t => t.InitialFailure = DateTime.UtcNow.AddDays(-5))
|
||||
.With(t => t.MostRecentFailure = DateTime.UtcNow.AddDays(-5))
|
||||
.With(t => t.EscalationLevel = 1)
|
||||
.BuildListOfNew();
|
||||
|
||||
Mocker.GetMock<IDownloadClientStatusRepository>()
|
||||
.Setup(s => s.All())
|
||||
.Returns(clientStatuses);
|
||||
|
||||
Subject.Clean();
|
||||
|
||||
Mocker.GetMock<IDownloadClientStatusRepository>()
|
||||
.Verify(v => v.UpdateMany(
|
||||
It.Is<List<DownloadClientStatus>>(i => i.All(
|
||||
s => s.DisabledTill.Value <= DateTime.UtcNow.AddMinutes(disabledTillTime)))));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_set_initial_failure_when_its_in_the_future()
|
||||
{
|
||||
var clientStatuses = Builder<DownloadClientStatus>.CreateListOfSize(5)
|
||||
.All()
|
||||
.With(t => t.DisabledTill = DateTime.UtcNow.AddDays(-5))
|
||||
.With(t => t.InitialFailure = DateTime.UtcNow.AddDays(5))
|
||||
.With(t => t.MostRecentFailure = DateTime.UtcNow.AddDays(-5))
|
||||
.With(t => t.EscalationLevel = 1)
|
||||
.BuildListOfNew();
|
||||
|
||||
Mocker.GetMock<IDownloadClientStatusRepository>()
|
||||
.Setup(s => s.All())
|
||||
.Returns(clientStatuses);
|
||||
|
||||
Subject.Clean();
|
||||
|
||||
Mocker.GetMock<IDownloadClientStatusRepository>()
|
||||
.Verify(v => v.UpdateMany(
|
||||
It.Is<List<DownloadClientStatus>>(i => i.All(
|
||||
s => s.InitialFailure.Value <= DateTime.UtcNow))));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_set_most_recent_failure_when_its_in_the_future()
|
||||
{
|
||||
var clientStatuses = Builder<DownloadClientStatus>.CreateListOfSize(5)
|
||||
.All()
|
||||
.With(t => t.DisabledTill = DateTime.UtcNow.AddDays(-5))
|
||||
.With(t => t.InitialFailure = DateTime.UtcNow.AddDays(-5))
|
||||
.With(t => t.MostRecentFailure = DateTime.UtcNow.AddDays(5))
|
||||
.With(t => t.EscalationLevel = 1)
|
||||
.BuildListOfNew();
|
||||
|
||||
Mocker.GetMock<IDownloadClientStatusRepository>()
|
||||
.Setup(s => s.All())
|
||||
.Returns(clientStatuses);
|
||||
|
||||
Subject.Clean();
|
||||
|
||||
Mocker.GetMock<IDownloadClientStatusRepository>()
|
||||
.Verify(v => v.UpdateMany(
|
||||
It.Is<List<DownloadClientStatus>>(i => i.All(
|
||||
s => s.MostRecentFailure.Value <= DateTime.UtcNow))));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_change_statuses_when_times_are_in_the_past()
|
||||
{
|
||||
var clientStatuses = Builder<DownloadClientStatus>.CreateListOfSize(5)
|
||||
.All()
|
||||
.With(t => t.DisabledTill = DateTime.UtcNow.AddDays(-5))
|
||||
.With(t => t.InitialFailure = DateTime.UtcNow.AddDays(-5))
|
||||
.With(t => t.MostRecentFailure = DateTime.UtcNow.AddDays(-5))
|
||||
.With(t => t.EscalationLevel = 0)
|
||||
.BuildListOfNew();
|
||||
|
||||
Mocker.GetMock<IDownloadClientStatusRepository>()
|
||||
.Setup(s => s.All())
|
||||
.Returns(clientStatuses);
|
||||
|
||||
Subject.Clean();
|
||||
|
||||
Mocker.GetMock<IDownloadClientStatusRepository>()
|
||||
.Verify(v => v.UpdateMany(
|
||||
It.Is<List<DownloadClientStatus>>(i => i.Count == 0)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
@@ -34,7 +35,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
|
||||
var recentFeed = ReadAllText(@"Files/Indexers/Avistaz/recentfeed.json");
|
||||
|
||||
Mocker.GetMock<IIndexerHttpClient>()
|
||||
.Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET), Subject.Definition))
|
||||
.Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get), Subject.Definition))
|
||||
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed)));
|
||||
|
||||
var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new int[] { 2000 } })).Releases;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
@@ -34,7 +35,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
|
||||
var recentFeed = ReadAllText(@"Files/Indexers/PrivateHD/recentfeed.json");
|
||||
|
||||
Mocker.GetMock<IIndexerHttpClient>()
|
||||
.Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET), Subject.Definition))
|
||||
.Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get), Subject.Definition))
|
||||
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed)));
|
||||
|
||||
var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new int[] { 2000 } })).Releases;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
@@ -33,7 +34,7 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
|
||||
var recentFeed = ReadAllText(@"Files/Indexers/FileList/recentfeed.json");
|
||||
|
||||
Mocker.GetMock<IIndexerHttpClient>()
|
||||
.Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET), Subject.Definition))
|
||||
.Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get), Subject.Definition))
|
||||
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed)));
|
||||
|
||||
var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new int[] { 2000 } })).Releases;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
@@ -45,7 +46,7 @@ namespace NzbDrone.Core.Test.IndexerTests.HDBitsTests
|
||||
var responseJson = ReadAllText(fileName);
|
||||
|
||||
Mocker.GetMock<IIndexerHttpClient>()
|
||||
.Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.POST), Subject.Definition))
|
||||
.Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Post), Subject.Definition))
|
||||
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), responseJson)));
|
||||
|
||||
var torrents = (await Subject.Fetch(_movieSearchCriteria)).Releases;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user