mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2026-03-05 13:40:08 -05:00
Compare commits
198 Commits
v0.1.0.447
...
v0.1.1.842
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27064cd293 | ||
|
|
1fe8c63d41 | ||
|
|
b18e226718 | ||
|
|
682afc2c75 | ||
|
|
1ef43c40c0 | ||
|
|
b46e2c6ad1 | ||
|
|
0a17b7e8ae | ||
|
|
b54c7e220e | ||
|
|
efb2a5751c | ||
|
|
2cd0dde4e2 | ||
|
|
de86274b08 | ||
|
|
4a957b618e | ||
|
|
891ca0f56b | ||
|
|
635fa78da9 | ||
|
|
b7731faedc | ||
|
|
7c1f5f769d | ||
|
|
e8c6103cc7 | ||
|
|
742c0d02bc | ||
|
|
7480ebea85 | ||
|
|
31886e8d35 | ||
|
|
252b9a1b6b | ||
|
|
77892a3885 | ||
|
|
09d839ffb1 | ||
|
|
1c15932b90 | ||
|
|
27c643d2f5 | ||
|
|
9a6391873f | ||
|
|
12ae8edc50 | ||
|
|
868f779c5d | ||
|
|
204052de9c | ||
|
|
878e269e70 | ||
|
|
1e317dac6b | ||
|
|
305df2fb7b | ||
|
|
3e07a9397c | ||
|
|
3e2d3c510a | ||
|
|
bdcead007c | ||
|
|
6c5d48621f | ||
|
|
a95465195d | ||
|
|
5d5e2042d0 | ||
|
|
5d980a175c | ||
|
|
2114db02d8 | ||
|
|
4bd23a60bd | ||
|
|
fe324dcc0c | ||
|
|
d5b34e8c03 | ||
|
|
e47c7e6a47 | ||
|
|
d937e0324f | ||
|
|
ff623d4c39 | ||
|
|
0b05f5ef24 | ||
|
|
e6c3292485 | ||
|
|
ba1c1baeb5 | ||
|
|
5e7f4f3fc1 | ||
|
|
3dd11213fa | ||
|
|
50cae0719f | ||
|
|
2c6680e4fa | ||
|
|
96afb7f327 | ||
|
|
0d1025d60a | ||
|
|
0508dd2b66 | ||
|
|
026a503d5f | ||
|
|
b3f8e648cd | ||
|
|
13b458090d | ||
|
|
f97c3ff9bd | ||
|
|
841ff7b6ee | ||
|
|
377db47daf | ||
|
|
2addcab765 | ||
|
|
ddc676c608 | ||
|
|
924db7a394 | ||
|
|
580113d6ce | ||
|
|
d532a69edc | ||
|
|
01f7e11d5a | ||
|
|
de97ec95db | ||
|
|
6e66467ab3 | ||
|
|
4c5131708d | ||
|
|
bb2e1a6037 | ||
|
|
5949bd97fd | ||
|
|
eaff071b16 | ||
|
|
a922586aba | ||
|
|
80beea9bdb | ||
|
|
8c326fc5c2 | ||
|
|
e26081acff | ||
|
|
b3b0467d22 | ||
|
|
e1c98d2b38 | ||
|
|
e304461dfb | ||
|
|
a127e5a30f | ||
|
|
e252cd4d3e | ||
|
|
9a1bd3db4c | ||
|
|
acce098e02 | ||
|
|
afa87b7113 | ||
|
|
4cbd2cd8bc | ||
|
|
e81d0f3e97 | ||
|
|
34a6a0e0c9 | ||
|
|
4254a05ea3 | ||
|
|
d32a94c14d | ||
|
|
6a9155bcf5 | ||
|
|
de442cc659 | ||
|
|
7f514c8f1e | ||
|
|
4c51f09acb | ||
|
|
a60388fcf9 | ||
|
|
11b656dabf | ||
|
|
535f29bef4 | ||
|
|
0ddc530dd4 | ||
|
|
b5321d33c9 | ||
|
|
4116c10caa | ||
|
|
0fe2cf5c2d | ||
|
|
c94573e868 | ||
|
|
1945af060d | ||
|
|
cf399ffdcc | ||
|
|
5846188202 | ||
|
|
fc65a89fbc | ||
|
|
b62ae41de8 | ||
|
|
8107f309b4 | ||
|
|
c2c12297bd | ||
|
|
81cbdab5eb | ||
|
|
9ee5a3e94b | ||
|
|
a570fd2a8f | ||
|
|
5cffb10e08 | ||
|
|
b11bf284dc | ||
|
|
5c4c042b2e | ||
|
|
d1cb744efd | ||
|
|
79b910a80c | ||
|
|
01e08e0c31 | ||
|
|
dd3c9c268e | ||
|
|
725f738ee1 | ||
|
|
22c738f43e | ||
|
|
e45f88473c | ||
|
|
889591d0b1 | ||
|
|
fe8247df8a | ||
|
|
7a5721bcee | ||
|
|
eea5c3e9a4 | ||
|
|
f55493c9a9 | ||
|
|
79618adaf9 | ||
|
|
af13d6ed80 | ||
|
|
38c09277d9 | ||
|
|
1fb693d066 | ||
|
|
7414a2f690 | ||
|
|
000590bcf7 | ||
|
|
d5d6625a63 | ||
|
|
00f33cb48f | ||
|
|
fd265c5734 | ||
|
|
316543b9aa | ||
|
|
117ebcff2d | ||
|
|
aab394b2c8 | ||
|
|
6ae520c061 | ||
|
|
dfb254d2dc | ||
|
|
07c03b0a12 | ||
|
|
eb0cf2d5f6 | ||
|
|
69c04ebe7a | ||
|
|
135db6d2ff | ||
|
|
c3deace9e6 | ||
|
|
9042594b14 | ||
|
|
44df0f5c3d | ||
|
|
7b9446eb35 | ||
|
|
c6b6daaf80 | ||
|
|
3b42b6a7e0 | ||
|
|
b6238f469c | ||
|
|
ae00c3aa6b | ||
|
|
8bf9d1b016 | ||
|
|
19ed7aa804 | ||
|
|
4d129ada95 | ||
|
|
4ec8ea0e4d | ||
|
|
bd79d3c828 | ||
|
|
7fb6c539d4 | ||
|
|
7f6fa2efbe | ||
|
|
b1727d9d91 | ||
|
|
3435d9db6e | ||
|
|
8e597c8179 | ||
|
|
80ec66514e | ||
|
|
88e677d973 | ||
|
|
a00f32c508 | ||
|
|
538db52d16 | ||
|
|
1ce7b0e56e | ||
|
|
87d91a0f15 | ||
|
|
61c1e934a5 | ||
|
|
97b09335df | ||
|
|
5e34fd2a9f | ||
|
|
7e620bd156 | ||
|
|
a97b801b24 | ||
|
|
9e64acd407 | ||
|
|
f72269f91b | ||
|
|
94d7f768a1 | ||
|
|
78cdc78cf9 | ||
|
|
2fc1257f42 | ||
|
|
210311cb38 | ||
|
|
9d7ec89314 | ||
|
|
9c279701a6 | ||
|
|
743e2e9b21 | ||
|
|
4a8daea940 | ||
|
|
bd90b74c12 | ||
|
|
4dff0c075a | ||
|
|
b8f57507dd | ||
|
|
61bfa9e7ed | ||
|
|
bbea256c85 | ||
|
|
5a1186639e | ||
|
|
a8f2700fe6 | ||
|
|
eeec505182 | ||
|
|
66dc53b92f | ||
|
|
334f3514df | ||
|
|
6ce35f6a24 | ||
|
|
f6a5f887ce | ||
|
|
bc90415394 |
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,6 +1,6 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
github: Prowlarr # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: prowlarr
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
|
||||
36
.github/ISSUE_TEMPLATE/bug_report.md
vendored
36
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,36 +0,0 @@
|
||||
---
|
||||
name: Bug Report
|
||||
about: Support Requests will be closed immediately, if you are not 100% certain this is a bug please go to our Reddit or Discord first. Exceptions do not mean you found a bug!
|
||||
title: ''
|
||||
labels: 'Type: Bug'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
<!-- Support Requests will be closed immediately, if you are unsure go to our Reddit or Discord first. Exceptions do not mean you found a bug! -->
|
||||
**Describe the bug**
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
**To Reproduce**
|
||||
<!-- Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error -->
|
||||
|
||||
**Expected behavior**
|
||||
<!-- A clear and concise description of what you expected to happen.-->
|
||||
|
||||
**Screenshots**
|
||||
<!-- If applicable, add screenshots to help explain your problem.-->
|
||||
|
||||
**Platform Information (please complete the following information):**
|
||||
- OS: <!-- [e.g. Windows 10 2004 / Ubuntu 20.04] -->
|
||||
- Docker: <!-- [Yes/No] -->
|
||||
- .NET Version (System -> Status): <!--[e.g. .NET 5.0.1] -->
|
||||
- Browser and Version (Only needed for UI issues): <!--[e.g. chrome 86.0.4240.198] -->
|
||||
- Prowlarr Version: <!--[e.g. 0.1.2.1854-->
|
||||
- Prowlarr Branch: <!--[e.g. develop, nightly]-->
|
||||
|
||||
Turn on Trace logs under Settings -> General and wait for the bug to occur again.
|
||||
**Upload the full log file here (or another site (e.g. pastebin) and link it). Issues will be closed, if they do not include this!**
|
||||
<!-- Trace logs are named Prowlarr.trace.txt or Prowlarr.trace.#.txt and will contain "trace" in them-->
|
||||
74
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
74
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
name: Bug Report
|
||||
title: "[BUG]: "
|
||||
description: 'Report a new bug, if you are not 100% certain this is a bug please go to our Reddit or Discord first'
|
||||
labels: ['Type: Bug', 'Status: Needs Triage']
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Is there an existing issue for this?
|
||||
description: Please search to see if an issue already exists for the bug you encountered.
|
||||
options:
|
||||
- label: I have searched the existing issues
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Current Behavior
|
||||
description: A concise description of what you're experiencing.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: A concise description of what you expected to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps To Reproduce
|
||||
description: Steps to reproduce the behavior.
|
||||
placeholder: |
|
||||
1. In this environment...
|
||||
2. With this config...
|
||||
3. Run '...'
|
||||
4. See error...
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Environment
|
||||
description: |
|
||||
examples:
|
||||
- **OS**: Ubuntu 20.04
|
||||
- **Prowlarr**: Prowlarr 0.1.0.650
|
||||
- **Docker Install**: Yes
|
||||
- **Using Reverse Proxy**: No
|
||||
- **Browser**: Firefox 90 (If UI related)
|
||||
value: |
|
||||
- OS:
|
||||
- Prowlarr:
|
||||
- Docker Install:
|
||||
- Using Reverse Proxy:
|
||||
- Browser:
|
||||
render: markdown
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: What branch are you running?
|
||||
options:
|
||||
- Master
|
||||
- Develop
|
||||
- Nightly
|
||||
- Other (This issue will be closed)
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Anything else?
|
||||
description: |
|
||||
Trace Logs (https://wiki.servarr.com/prowlarr/troubleshooting#logging-and-log-files)
|
||||
Links? References? Anything that will give us more context about the issue you are encountering!
|
||||
|
||||
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
|
||||
validations:
|
||||
required: true
|
||||
3
.github/ISSUE_TEMPLATE/config.yml
vendored
3
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Indexer Requests
|
||||
url: https://requests.prowlarr.com/
|
||||
about: Request new indexers to be added. Vote on existing requests.
|
||||
- name: Support via Discord
|
||||
url: https://prowlarr.com/discord
|
||||
about: Chat with users and devs on support and setup related topics.
|
||||
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: Feature Request
|
||||
about: Suggest an idea for Prowlarr
|
||||
title: ''
|
||||
labels: 'Type: Feature Request'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
|
||||
|
||||
**Describe the solution you'd like**
|
||||
<!-- A clear and concise description of what you want to happen. -->
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
|
||||
|
||||
**Additional context**
|
||||
<!-- Add any other context or screenshots about the feature request here. -->
|
||||
39
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
39
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
name: Feature Request
|
||||
title: "[FEAT]: "
|
||||
description: 'Suggest an idea for Prowlarr'
|
||||
labels: ['Type: Feature Request', 'Status: Needs Triage']
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Is there an existing issue for this?
|
||||
description: Please search to see if an issue already exists for the feature you are requesting.
|
||||
options:
|
||||
- label: I have searched the existing issues
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Is your feature request related to a problem? Please describe
|
||||
description: A clear and concise description of what the problem is.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the solution you'd like
|
||||
description: A clear and concise description of what you want to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe alternatives you've considered
|
||||
description: A clear and concise description of any alternative solutions or features you've considered.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Anything else?
|
||||
description: |
|
||||
Links? References? Mockups? Anything that will give us more context about the feature you are encountering!
|
||||
|
||||
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
|
||||
validations:
|
||||
required: true
|
||||
22
.github/ISSUE_TEMPLATE/indexer_request.md
vendored
22
.github/ISSUE_TEMPLATE/indexer_request.md
vendored
@@ -1,22 +0,0 @@
|
||||
---
|
||||
name: Indexer Request
|
||||
about: Request an indexer for Prowlarr. Check the pinned Jackett parity issue prior to submitting a request. Duplicated requests will be closed without warning. Please search GitHub prior to requesting.
|
||||
title: '(Indexer) '
|
||||
labels: 'Type: Indexer Request'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- Check the pinned Jackett parity issue prior to submitting a request. Duplicated requests or requests covered in existing GitHub Issues will be closed without warning. Please search GitHub prior to requesting.-->
|
||||
|
||||
**Type:** <Usenet|Torrents>
|
||||
|
||||
**Tracker:** <Indexer/Tracker Name>
|
||||
|
||||
**URL:** <Indexer/Tracker URL>
|
||||
|
||||
**In Jackett?:** <Yes|No>
|
||||
<!-- Check the pinned Jackett parity issue prior to submitting a request. Duplicated requests or requests covered in existing GitHub Issues will be closed without warning. Please search GitHub prior to requesting.-->
|
||||
|
||||
**Additional Context:**
|
||||
<!-- Add any other context or screenshots about the request here. -->
|
||||
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,14 +1,15 @@
|
||||
#### Database Migration
|
||||
YES | NO
|
||||
YES - XXXX | NO
|
||||
|
||||
#### Description
|
||||
A few sentences describing the overall goals of the pull request's commits.
|
||||
|
||||
#### Screenshot (if UI related)
|
||||
|
||||
#### Todos
|
||||
- [ ] Tests
|
||||
- [ ] Translation Keys
|
||||
- [ ] Wiki Updates
|
||||
- [ ] Translation Keys (./src/NzbDrone.Core/Localization/Core/en.json)
|
||||
- [ ] [Wiki Updates](https://wiki.servarr.com)
|
||||
|
||||
#### Issues Fixed or Closed by this PR
|
||||
|
||||
|
||||
41
.github/workflows/azuresync.yml
vendored
Normal file
41
.github/workflows/azuresync.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
name: Sync issue to Azure DevOps work item
|
||||
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
[opened, edited, deleted, closed, reopened, labeled, unlabeled, assigned]
|
||||
|
||||
concurrency: azuresync-${{ github.event.issue.number }}
|
||||
|
||||
jobs:
|
||||
alert:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: danhellem/github-actions-issue-to-work-item@master
|
||||
if: "${{ contains(github.event.issue.labels.*.name, 'Type: Bug') == true }}"
|
||||
env:
|
||||
ado_token: "${{ secrets.ADO_PERSONAL_ACCESS_TOKEN }}"
|
||||
github_token: "${{ github.token }}"
|
||||
ado_organization: "Servarr"
|
||||
ado_project: "Servarr"
|
||||
ado_area_path: "Servarr\\Prowlarr"
|
||||
ado_wit: "Bug"
|
||||
ado_new_state: "New"
|
||||
ado_active_state: "Active"
|
||||
ado_close_state: "Closed"
|
||||
ado_bypassrules: true
|
||||
log_level: 100
|
||||
- uses: danhellem/github-actions-issue-to-work-item@master
|
||||
if: "${{ contains(github.event.issue.labels.*.name, 'Type: Bug') == false }}"
|
||||
env:
|
||||
ado_token: "${{ secrets.ADO_PERSONAL_ACCESS_TOKEN }}"
|
||||
github_token: "${{ github.token }}"
|
||||
ado_organization: "Servarr"
|
||||
ado_project: "Servarr"
|
||||
ado_area_path: "Servarr\\Prowlarr"
|
||||
ado_wit: "User Story"
|
||||
ado_new_state: "New"
|
||||
ado_active_state: "Active"
|
||||
ado_close_state: "Closed"
|
||||
ado_bypassrules: true
|
||||
log_level: 100
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
We're always looking for people to help make Prowlarr even better, there are a number of ways to contribute.
|
||||
|
||||
This file is updated on an ad-hoc basis, for the latest details please see the [contributing wiki page](https://wiki.servarr.com/prowlarr/contributing).
|
||||
|
||||
## Documentation ##
|
||||
Setup guides, FAQ, the more information we have on the [wiki](https://wikijs.servarr.com/prowlarr) the better.
|
||||
Setup guides, FAQ, the more information we have on the [wiki](https://wiki.servarr.com/prowlarr) the better.
|
||||
|
||||
## Development ##
|
||||
|
||||
@@ -23,11 +25,11 @@ Setup guides, FAQ, the more information we have on the [wiki](https://wikijs.ser
|
||||
4. Start gulp to monitor your dev environment for any changes that need post processing using `yarn start` command.
|
||||
5. Build the project in Visual Studio, Setting startup project to `Prowlarr.Console` and framework to `net5.0`
|
||||
6. Debug the project in Visual Studio
|
||||
7. Open http://localhost:7878
|
||||
7. Open http://localhost:9696
|
||||
|
||||
### Contributing Code ###
|
||||
- If you're adding a new, already requested feature, please comment on [Github Issues](https://github.com/Prowlarr/Prowlarr/issues "Github Issues") so work is not duplicated (If you want to add something not already on there, please talk to us first)
|
||||
- Rebase from Radarr's develop branch, don't merge
|
||||
- Rebase from Prowlarr's develop branch, don't merge
|
||||
- Make meaningful commits, or squash them
|
||||
- Feel free to make a pull request before work is complete, this will let us see where its at and make comments/suggest improvements
|
||||
- Reach out to us on the discord if you have any questions
|
||||
|
||||
24
README.md
24
README.md
@@ -2,7 +2,7 @@
|
||||
|
||||
[](https://dev.azure.com/Prowlarr/Prowlarr/_build/latest?definitionId=1&branchName=develop)
|
||||
[](https://translate.servarr.com/engage/prowlarr/?utm_source=widget)
|
||||
[](https://wikijs.servarr.com/prowlarr/installation#docker)
|
||||
[](https://wiki.servarr.com/prowlarr/installation#docker)
|
||||

|
||||
[](#backers)
|
||||
[](#sponsors)
|
||||
@@ -10,12 +10,15 @@
|
||||
Prowlarr is a indexer manager/proxy built on the popular arr .net/reactjs base stack to integrate with your various PVR apps. Prowlarr supports both Torrent Trackers and Usenet Indexers. It integrates seamlessly with Sonarr, Radarr, Lidarr, and Readarr offering complete management of your indexers with no per app Indexer setup required (we do it all).
|
||||
|
||||
## Major Features Include:
|
||||
- Usenet support for any Newznab compatible indexer, including Headphones VIP
|
||||
- Torrent support 400+ trackers & more coming soon
|
||||
- Indexer Sync to Sonarr/Radarr/Readarr/Lidarr, so no manual configuration of the other applications are required
|
||||
- Indexer History and Statistics
|
||||
- Manual Searching of Trackers & Indexers at a category level
|
||||
- Usenet support for 24 indexers natively, including Headphones VIP, and 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
|
||||
- Indexer history and statistics
|
||||
- Manual searching of Trackers & Indexers at a category level
|
||||
- Support for pushing releases 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
|
||||
@@ -23,11 +26,14 @@ Note: Prowlarr is currently early in life, thus bugs should be expected
|
||||
[](https://prowlarr.com/discord)
|
||||
[](https://www.reddit.com/r/Prowlarr)
|
||||
[](https://github.com/Prowlarr/Prowlarr/issues)
|
||||
[](https://wikijs.servarr.com/prowlarr)
|
||||
[](https://wiki.servarr.com/prowlarr)
|
||||
|
||||
## Feature Requests
|
||||
## Indexers/Trackers
|
||||
|
||||
[Feature Requests](https://github.com/Prowlarr/Prowlarr/issues/new?assignees=&template=feature_request.md&Type%3A%20Feature%20Request&title=)
|
||||
[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
|
||||
|
||||
## Contributors & Developers
|
||||
This project exists thanks to all the people who contribute. [Contribute](CONTRIBUTING.md).
|
||||
|
||||
@@ -7,13 +7,13 @@ variables:
|
||||
outputFolder: './_output'
|
||||
artifactsFolder: './_artifacts'
|
||||
testsFolder: './_tests'
|
||||
majorVersion: '0.1.0'
|
||||
majorVersion: '0.1.1'
|
||||
minorVersion: $[counter('minorVersion', 1)]
|
||||
prowlarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
|
||||
sentryOrg: 'servarr'
|
||||
sentryUrl: 'https://sentry.servarr.com'
|
||||
dotnetVersion: '5.0.203'
|
||||
dotnetVersion: '5.0.400'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
|
||||
trigger:
|
||||
@@ -879,7 +879,7 @@ stages:
|
||||
artifactName: 'WindowsAutomationScreenshots'
|
||||
targetPath: $(Build.SourcesDirectory)
|
||||
- checkout: none
|
||||
- powershell: |
|
||||
- pwsh: |
|
||||
iex ((New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/Servarr/AzureDiscordNotify/master/DiscordNotify.ps1'))
|
||||
env:
|
||||
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
|
||||
|
||||
@@ -11,6 +11,7 @@ import ApplicationSettingsConnector from 'Settings/Applications/ApplicationSetti
|
||||
import DevelopmentSettingsConnector from 'Settings/Development/DevelopmentSettingsConnector';
|
||||
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
|
||||
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
|
||||
import IndexerSettings from 'Settings/Indexers/IndexerSettings';
|
||||
import NotificationSettings from 'Settings/Notifications/NotificationSettings';
|
||||
import Settings from 'Settings/Settings';
|
||||
import TagSettings from 'Settings/Tags/TagSettings';
|
||||
@@ -90,6 +91,11 @@ function AppRoutes(props) {
|
||||
component={Settings}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/settings/indexers"
|
||||
component={IndexerSettings}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/settings/applications"
|
||||
component={ApplicationSettingsConnector}
|
||||
|
||||
@@ -129,7 +129,7 @@ class FileBrowserModalContent extends Component {
|
||||
className={styles.mappedDrivesWarning}
|
||||
kind={kinds.WARNING}
|
||||
>
|
||||
<Link to="https://wikijs.servarr.com/prowlarr/faq#why-cant-prowlarr-see-my-files-on-a-remote-server">
|
||||
<Link to="https://wiki.servarr.com/prowlarr/faq#why-cant-prowlarr-see-my-files-on-a-remote-server">
|
||||
{translate('MappedDrivesRunningAsService')}
|
||||
</Link>
|
||||
</Alert>
|
||||
|
||||
@@ -11,8 +11,6 @@ class InfoInput extends Component {
|
||||
value
|
||||
} = this.props;
|
||||
|
||||
console.log(this.props);
|
||||
|
||||
return (
|
||||
<span dangerouslySetInnerHTML={{ __html: value }} />
|
||||
);
|
||||
|
||||
@@ -48,6 +48,10 @@ const links = [
|
||||
title: translate('Settings'),
|
||||
to: '/settings',
|
||||
children: [
|
||||
{
|
||||
title: translate('Indexers'),
|
||||
to: '/settings/indexers'
|
||||
},
|
||||
{
|
||||
title: translate('Apps'),
|
||||
to: '/settings/applications'
|
||||
|
||||
@@ -228,4 +228,4 @@ export const UNSAVED_SETTING = farDotCircle;
|
||||
export const VIEW = fasEye;
|
||||
export const WARNING = fasExclamationTriangle;
|
||||
export const WIKI = fasBookReader;
|
||||
export const BLACKLIST = fasBan;
|
||||
export const BLOCKLIST = fasBan;
|
||||
|
||||
@@ -13,6 +13,7 @@ import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './EditIndexerModalContent.css';
|
||||
|
||||
@@ -31,6 +32,7 @@ function EditIndexerModalContent(props) {
|
||||
onSavePress,
|
||||
onTestPress,
|
||||
onDeleteIndexerPress,
|
||||
onAdvancedSettingsPress,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
@@ -43,6 +45,7 @@ function EditIndexerModalContent(props) {
|
||||
supportsRss,
|
||||
supportsRedirect,
|
||||
appProfileId,
|
||||
tags,
|
||||
fields,
|
||||
priority
|
||||
} = item;
|
||||
@@ -87,7 +90,6 @@ function EditIndexerModalContent(props) {
|
||||
type={inputTypes.CHECK}
|
||||
name="enable"
|
||||
helpTextWarning={supportsRss.value ? undefined : translate('RSSIsNotSupportedWithThisIndexer')}
|
||||
isDisabled={!supportsRss.value}
|
||||
{...enable}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
@@ -150,6 +152,18 @@ function EditIndexerModalContent(props) {
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Tags')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TAG}
|
||||
name="tags"
|
||||
helpText="Use tags to specify default clients, specify Indexer Proxies, or just to organize your indexers."
|
||||
{...tags}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
}
|
||||
</ModalBody>
|
||||
@@ -165,6 +179,12 @@ function EditIndexerModalContent(props) {
|
||||
</Button>
|
||||
}
|
||||
|
||||
<AdvancedSettingsButton
|
||||
advancedSettings={advancedSettings}
|
||||
onAdvancedSettingsPress={onAdvancedSettingsPress}
|
||||
showLabel={false}
|
||||
/>
|
||||
|
||||
<SpinnerErrorButton
|
||||
isSpinning={isTesting}
|
||||
error={saveError}
|
||||
@@ -204,6 +224,7 @@ EditIndexerModalContent.propTypes = {
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onTestPress: PropTypes.func.isRequired,
|
||||
onAdvancedSettingsPress: PropTypes.func.isRequired,
|
||||
onDeleteIndexerPress: PropTypes.func
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { saveIndexer, setIndexerFieldValue, setIndexerValue, testIndexer } from 'Store/Actions/indexerActions';
|
||||
import { toggleAdvancedSettings } from 'Store/Actions/settingsActions';
|
||||
import createIndexerSchemaSelector from 'Store/Selectors/createIndexerSchemaSelector';
|
||||
import EditIndexerModalContent from './EditIndexerModalContent';
|
||||
|
||||
@@ -23,7 +24,8 @@ const mapDispatchToProps = {
|
||||
setIndexerValue,
|
||||
setIndexerFieldValue,
|
||||
saveIndexer,
|
||||
testIndexer
|
||||
testIndexer,
|
||||
toggleAdvancedSettings
|
||||
};
|
||||
|
||||
class EditIndexerModalContentConnector extends Component {
|
||||
@@ -56,6 +58,10 @@ class EditIndexerModalContentConnector extends Component {
|
||||
this.props.testIndexer({ id: this.props.id });
|
||||
}
|
||||
|
||||
onAdvancedSettingsPress = () => {
|
||||
this.props.toggleAdvancedSettings();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -65,6 +71,7 @@ class EditIndexerModalContentConnector extends Component {
|
||||
{...this.props}
|
||||
onSavePress={this.onSavePress}
|
||||
onTestPress={this.onTestPress}
|
||||
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
|
||||
onInputChange={this.onInputChange}
|
||||
onFieldChange={this.onFieldChange}
|
||||
/>
|
||||
@@ -80,6 +87,7 @@ EditIndexerModalContentConnector.propTypes = {
|
||||
item: PropTypes.object.isRequired,
|
||||
setIndexerValue: PropTypes.func.isRequired,
|
||||
setIndexerFieldValue: PropTypes.func.isRequired,
|
||||
toggleAdvancedSettings: PropTypes.func.isRequired,
|
||||
saveIndexer: PropTypes.func.isRequired,
|
||||
testIndexer: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
|
||||
@@ -71,7 +71,7 @@ class IndexerIndexRow extends Component {
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
baseUrl,
|
||||
indexerUrls,
|
||||
enable,
|
||||
redirect,
|
||||
tags,
|
||||
@@ -248,7 +248,7 @@ class IndexerIndexRow extends Component {
|
||||
className={styles.externalLink}
|
||||
name={icons.EXTERNAL_LINK}
|
||||
title={'Website'}
|
||||
to={baseUrl.replace('api.', '')}
|
||||
to={indexerUrls[0].replace('api.', '')}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
@@ -289,7 +289,7 @@ class IndexerIndexRow extends Component {
|
||||
|
||||
IndexerIndexRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
baseUrl: PropTypes.string.isRequired,
|
||||
indexerUrls: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
protocol: PropTypes.string.isRequired,
|
||||
privacy: PropTypes.string.isRequired,
|
||||
priority: PropTypes.number.isRequired,
|
||||
|
||||
@@ -31,7 +31,7 @@ function IndexerStatusCell(props) {
|
||||
<Icon
|
||||
className={styles.statusIcon}
|
||||
kind={enabled ? enableKind : kinds.DEFAULT}
|
||||
name={enabled ? enableIcon: icons.BLACKLIST}
|
||||
name={enabled ? enableIcon: icons.BLOCKLIST}
|
||||
title={enabled ? enableTitle : 'Indexer is Disabled'}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ function IndexerInfoModalContent(props) {
|
||||
description,
|
||||
encoding,
|
||||
language,
|
||||
baseUrl,
|
||||
indexerUrls,
|
||||
protocol,
|
||||
onModalClose
|
||||
} = props;
|
||||
@@ -54,10 +54,10 @@ function IndexerInfoModalContent(props) {
|
||||
|
||||
<DescriptionListItemTitle>Indexer Site</DescriptionListItemTitle>
|
||||
<DescriptionListItemDescription>
|
||||
<Link to={baseUrl}>{baseUrl}</Link>
|
||||
<Link to={indexerUrls[0]}>{indexerUrls[0]}</Link>
|
||||
</DescriptionListItemDescription>
|
||||
|
||||
<DescriptionListItemTitle>{protocol === 'usenet' ? 'Newznab' : 'Torznab'} Url</DescriptionListItemTitle>
|
||||
<DescriptionListItemTitle>{`${protocol === 'usenet' ? 'Newznab' : 'Torznab'} Url`}</DescriptionListItemTitle>
|
||||
<DescriptionListItemDescription>
|
||||
{`${window.location.origin}${window.Prowlarr.urlBase}/${id}/api`}
|
||||
</DescriptionListItemDescription>
|
||||
@@ -74,7 +74,7 @@ IndexerInfoModalContent.propTypes = {
|
||||
description: PropTypes.string.isRequired,
|
||||
encoding: PropTypes.string.isRequired,
|
||||
language: PropTypes.string.isRequired,
|
||||
baseUrl: PropTypes.string.isRequired,
|
||||
indexerUrls: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
protocol: PropTypes.string.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -10,7 +10,6 @@ function createMapStateToProps() {
|
||||
(state) => state.settings.advancedSettings,
|
||||
createIndexerSelector(),
|
||||
(advancedSettings, indexer) => {
|
||||
console.log(indexer);
|
||||
return {
|
||||
advancedSettings,
|
||||
...indexer
|
||||
|
||||
@@ -19,6 +19,10 @@ function getAverageResponseTimeData(indexerStats) {
|
||||
};
|
||||
});
|
||||
|
||||
data.sort((a, b) => {
|
||||
return b.value - a.value;
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ function SearchIndexSortMenu(props) {
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
Protocol
|
||||
{translate('Protocol')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
@@ -35,7 +35,7 @@ function SearchIndexSortMenu(props) {
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
Age
|
||||
{translate('Age')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
@@ -53,7 +53,7 @@ function SearchIndexSortMenu(props) {
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
Indexer
|
||||
{translate('Indexer')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
@@ -62,7 +62,7 @@ function SearchIndexSortMenu(props) {
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
Size
|
||||
{translate('Size')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
@@ -71,7 +71,7 @@ function SearchIndexSortMenu(props) {
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
Files
|
||||
{translate('Files')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
@@ -80,7 +80,7 @@ function SearchIndexSortMenu(props) {
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
Grabs
|
||||
{translate('Grabs')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
@@ -89,7 +89,7 @@ function SearchIndexSortMenu(props) {
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
Peers
|
||||
{translate('Peers')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
@@ -98,7 +98,7 @@ function SearchIndexSortMenu(props) {
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
Category
|
||||
{translate('Category')}
|
||||
</SortMenuItem>
|
||||
</MenuContent>
|
||||
</SortMenu>
|
||||
|
||||
@@ -19,7 +19,7 @@ function NoSearchResults(props) {
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.message}>
|
||||
No search results found, try performing a new search below.
|
||||
{translate('NoSearchResultsFound')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -7,6 +7,7 @@ import TextInput from 'Components/Form/TextInput';
|
||||
import keyboardShortcuts from 'Components/keyboardShortcuts';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import PageContentFooter from 'Components/Page/PageContentFooter';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import SearchFooterLabel from './SearchFooterLabel';
|
||||
import styles from './SearchFooter.css';
|
||||
|
||||
@@ -26,7 +27,7 @@ class SearchFooter extends Component {
|
||||
|
||||
this.state = {
|
||||
searchingReleases: false,
|
||||
searchQuery: defaultSearchQuery,
|
||||
searchQuery: defaultSearchQuery || '',
|
||||
searchIndexerIds: defaultIndexerIds,
|
||||
searchCategories: defaultCategories
|
||||
};
|
||||
@@ -51,14 +52,12 @@ class SearchFooter extends Component {
|
||||
isFetching,
|
||||
defaultIndexerIds,
|
||||
defaultCategories,
|
||||
defaultSearchQuery,
|
||||
searchError
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
searchIndexerIds,
|
||||
searchCategories,
|
||||
searchQuery
|
||||
searchCategories
|
||||
} = this.state;
|
||||
|
||||
const newState = {};
|
||||
@@ -71,10 +70,6 @@ class SearchFooter extends Component {
|
||||
newState.searchCategories = defaultCategories;
|
||||
}
|
||||
|
||||
if (searchQuery !== defaultSearchQuery) {
|
||||
newState.searchQuery = defaultSearchQuery;
|
||||
}
|
||||
|
||||
if (prevProps.isFetching && !isFetching && !searchError) {
|
||||
newState.searchingReleases = false;
|
||||
}
|
||||
@@ -91,6 +86,10 @@ class SearchFooter extends Component {
|
||||
this.props.onSearchPress(this.state.searchQuery, this.state.searchIndexerIds, this.state.searchCategories);
|
||||
}
|
||||
|
||||
onSearchInputChange = ({ value }) => {
|
||||
this.setState({ searchQuery: value });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -120,7 +119,7 @@ class SearchFooter extends Component {
|
||||
autoFocus={true}
|
||||
value={searchQuery}
|
||||
isDisabled={isFetching}
|
||||
onChange={onInputChange}
|
||||
onChange={this.onSearchInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -167,7 +166,7 @@ class SearchFooter extends Component {
|
||||
isDisabled={isFetching || !hasIndexers}
|
||||
onPress={this.onSearchPress}
|
||||
>
|
||||
Search
|
||||
{translate('Search')}
|
||||
</SpinnerButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Label from 'Components/Label';
|
||||
import { kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import Tooltip from '../../Components/Tooltip/Tooltip';
|
||||
|
||||
function CategoryLabel({ categories }) {
|
||||
const sortedCategories = categories.filter((cat) => cat.name !== undefined).sort((c) => c.id);
|
||||
|
||||
if (categories?.length === 0) {
|
||||
return (
|
||||
<Tooltip
|
||||
anchor={<Label kind={kinds.DANGER}>Unknown</Label>}
|
||||
tooltip="Please report this issue to the GitHub as this shouldn't be happening"
|
||||
position={tooltipPositions.LEFT}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span>
|
||||
{
|
||||
@@ -20,6 +32,10 @@ function CategoryLabel({ categories }) {
|
||||
);
|
||||
}
|
||||
|
||||
CategoryLabel.defaultProps = {
|
||||
categories: []
|
||||
};
|
||||
|
||||
CategoryLabel.propTypes = {
|
||||
categories: PropTypes.arrayOf(PropTypes.object).isRequired
|
||||
};
|
||||
|
||||
@@ -10,7 +10,8 @@ import styles from './AdvancedSettingsButton.css';
|
||||
function AdvancedSettingsButton(props) {
|
||||
const {
|
||||
advancedSettings,
|
||||
onAdvancedSettingsPress
|
||||
onAdvancedSettingsPress,
|
||||
showLabel
|
||||
} = props;
|
||||
|
||||
return (
|
||||
@@ -43,18 +44,27 @@ function AdvancedSettingsButton(props) {
|
||||
/>
|
||||
</span>
|
||||
|
||||
<div className={styles.labelContainer}>
|
||||
<div className={styles.label}>
|
||||
{advancedSettings ? translate('HideAdvanced') : translate('ShowAdvanced')}
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
showLabel &&
|
||||
<div className={styles.labelContainer}>
|
||||
<div className={styles.label}>
|
||||
{advancedSettings ? translate('HideAdvanced') : translate('ShowAdvanced')}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
AdvancedSettingsButton.propTypes = {
|
||||
advancedSettings: PropTypes.bool.isRequired,
|
||||
onAdvancedSettingsPress: PropTypes.func.isRequired
|
||||
onAdvancedSettingsPress: PropTypes.func.isRequired,
|
||||
showLabel: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
AdvancedSettingsButton.defaultProps = {
|
||||
showLabel: true
|
||||
};
|
||||
|
||||
export default AdvancedSettingsButton;
|
||||
|
||||
@@ -55,7 +55,7 @@ function UpdateSettings(props) {
|
||||
type={inputTypes.TEXT}
|
||||
name="branch"
|
||||
helpText={usingExternalUpdateMechanism ? translate('BranchUpdateMechanism') : translate('BranchUpdate')}
|
||||
helpLink="https://wikijs.servarr.com/prowlarr/settings#updates"
|
||||
helpLink="https://wiki.servarr.com/prowlarr/settings#updates"
|
||||
{...branch}
|
||||
onChange={onInputChange}
|
||||
readOnly={usingExternalUpdateMechanism}
|
||||
@@ -92,7 +92,7 @@ function UpdateSettings(props) {
|
||||
name="updateMechanism"
|
||||
values={updateOptions}
|
||||
helpText={translate('UpdateMechanismHelpText')}
|
||||
helpLink="https://wikijs.servarr.com/prowlarr/settings#updates"
|
||||
helpLink="https://wiki.servarr.com/prowlarr/settings#updates"
|
||||
onChange={onInputChange}
|
||||
{...updateMechanism}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
.indexerProxy {
|
||||
composes: card from '~Components/Card.css';
|
||||
|
||||
position: relative;
|
||||
width: 300px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.underlay {
|
||||
@add-mixin cover;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
@add-mixin linkOverlay;
|
||||
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.name {
|
||||
text-align: center;
|
||||
font-weight: lighter;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.presetsMenu {
|
||||
composes: menu from '~Components/Menu/Menu.css';
|
||||
|
||||
display: inline-block;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.presetsMenuButton {
|
||||
composes: button from '~Components/Link/Button.css';
|
||||
|
||||
&::after {
|
||||
margin-left: 5px;
|
||||
content: '\25BE';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Link from 'Components/Link/Link';
|
||||
import Menu from 'Components/Menu/Menu';
|
||||
import MenuContent from 'Components/Menu/MenuContent';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AddIndexerProxyPresetMenuItem from './AddIndexerProxyPresetMenuItem';
|
||||
import styles from './AddIndexerProxyItem.css';
|
||||
|
||||
class AddIndexerProxyItem extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onIndexerProxySelect = () => {
|
||||
const {
|
||||
implementation
|
||||
} = this.props;
|
||||
|
||||
this.props.onIndexerProxySelect({ implementation });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
implementation,
|
||||
implementationName,
|
||||
infoLink,
|
||||
presets,
|
||||
onIndexerProxySelect
|
||||
} = this.props;
|
||||
|
||||
const hasPresets = !!presets && !!presets.length;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.indexerProxy}
|
||||
>
|
||||
<Link
|
||||
className={styles.underlay}
|
||||
onPress={this.onIndexerProxySelect}
|
||||
/>
|
||||
|
||||
<div className={styles.overlay}>
|
||||
<div className={styles.name}>
|
||||
{implementationName}
|
||||
</div>
|
||||
|
||||
<div className={styles.actions}>
|
||||
{
|
||||
hasPresets &&
|
||||
<span>
|
||||
<Button
|
||||
size={sizes.SMALL}
|
||||
onPress={this.onIndexerProxySelect}
|
||||
>
|
||||
Custom
|
||||
</Button>
|
||||
|
||||
<Menu className={styles.presetsMenu}>
|
||||
<Button
|
||||
className={styles.presetsMenuButton}
|
||||
size={sizes.SMALL}
|
||||
>
|
||||
Presets
|
||||
</Button>
|
||||
|
||||
<MenuContent>
|
||||
{
|
||||
presets.map((preset) => {
|
||||
return (
|
||||
<AddIndexerProxyPresetMenuItem
|
||||
key={preset.name}
|
||||
name={preset.name}
|
||||
implementation={implementation}
|
||||
onPress={onIndexerProxySelect}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
</span>
|
||||
}
|
||||
|
||||
<Button
|
||||
to={infoLink}
|
||||
size={sizes.SMALL}
|
||||
>
|
||||
{translate('MoreInfo')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddIndexerProxyItem.propTypes = {
|
||||
implementation: PropTypes.string.isRequired,
|
||||
implementationName: PropTypes.string.isRequired,
|
||||
infoLink: PropTypes.string.isRequired,
|
||||
presets: PropTypes.arrayOf(PropTypes.object),
|
||||
onIndexerProxySelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AddIndexerProxyItem;
|
||||
@@ -0,0 +1,25 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import AddIndexerProxyModalContentConnector from './AddIndexerProxyModalContentConnector';
|
||||
|
||||
function AddIndexerProxyModal({ isOpen, onModalClose, ...otherProps }) {
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<AddIndexerProxyModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
AddIndexerProxyModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AddIndexerProxyModal;
|
||||
@@ -0,0 +1,5 @@
|
||||
.indexerProxies {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Button from 'Components/Link/Button';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AddIndexerProxyItem from './AddIndexerProxyItem';
|
||||
import styles from './AddIndexerProxyModalContent.css';
|
||||
|
||||
class AddIndexerProxyModalContent extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isSchemaFetching,
|
||||
isSchemaPopulated,
|
||||
schemaError,
|
||||
schema,
|
||||
onIndexerProxySelect,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{translate('AddIndexerProxy')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{
|
||||
isSchemaFetching &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isSchemaFetching && !!schemaError &&
|
||||
<div>
|
||||
{translate('UnableToAddANewIndexerProxyPleaseTryAgain')}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
isSchemaPopulated && !schemaError &&
|
||||
<div>
|
||||
<div className={styles.indexerProxies}>
|
||||
{
|
||||
schema.map((indexerProxy) => {
|
||||
return (
|
||||
<AddIndexerProxyItem
|
||||
key={indexerProxy.implementation}
|
||||
implementation={indexerProxy.implementation}
|
||||
{...indexerProxy}
|
||||
onIndexerProxySelect={onIndexerProxySelect}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
onPress={onModalClose}
|
||||
>
|
||||
{translate('Close')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddIndexerProxyModalContent.propTypes = {
|
||||
isSchemaFetching: PropTypes.bool.isRequired,
|
||||
isSchemaPopulated: PropTypes.bool.isRequired,
|
||||
schemaError: PropTypes.object,
|
||||
schema: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onIndexerProxySelect: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AddIndexerProxyModalContent;
|
||||
@@ -0,0 +1,70 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchIndexerProxySchema, selectIndexerProxySchema } from 'Store/Actions/settingsActions';
|
||||
import AddIndexerProxyModalContent from './AddIndexerProxyModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.indexerProxies,
|
||||
(indexerProxies) => {
|
||||
const {
|
||||
isSchemaFetching,
|
||||
isSchemaPopulated,
|
||||
schemaError,
|
||||
schema
|
||||
} = indexerProxies;
|
||||
|
||||
return {
|
||||
isSchemaFetching,
|
||||
isSchemaPopulated,
|
||||
schemaError,
|
||||
schema
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchIndexerProxySchema,
|
||||
selectIndexerProxySchema
|
||||
};
|
||||
|
||||
class AddIndexerProxyModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchIndexerProxySchema();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onIndexerProxySelect = ({ implementation, name }) => {
|
||||
this.props.selectIndexerProxySchema({ implementation, presetName: name });
|
||||
this.props.onModalClose({ indexerProxySelected: true });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<AddIndexerProxyModalContent
|
||||
{...this.props}
|
||||
onIndexerProxySelect={this.onIndexerProxySelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddIndexerProxyModalContentConnector.propTypes = {
|
||||
fetchIndexerProxySchema: PropTypes.func.isRequired,
|
||||
selectIndexerProxySchema: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(AddIndexerProxyModalContentConnector);
|
||||
@@ -0,0 +1,49 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import MenuItem from 'Components/Menu/MenuItem';
|
||||
|
||||
class AddIndexerProxyPresetMenuItem extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onPress = () => {
|
||||
const {
|
||||
name,
|
||||
implementation
|
||||
} = this.props;
|
||||
|
||||
this.props.onPress({
|
||||
name,
|
||||
implementation
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
name,
|
||||
implementation,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
{...otherProps}
|
||||
onPress={this.onPress}
|
||||
>
|
||||
{name}
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddIndexerProxyPresetMenuItem.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
implementation: PropTypes.string.isRequired,
|
||||
onPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AddIndexerProxyPresetMenuItem;
|
||||
@@ -0,0 +1,27 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import EditIndexerProxyModalContentConnector from './EditIndexerProxyModalContentConnector';
|
||||
|
||||
function EditIndexerProxyModal({ isOpen, onModalClose, ...otherProps }) {
|
||||
return (
|
||||
<Modal
|
||||
size={sizes.MEDIUM}
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<EditIndexerProxyModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
EditIndexerProxyModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default EditIndexerProxyModal;
|
||||
@@ -0,0 +1,65 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import { cancelSaveIndexerProxy, cancelTestIndexerProxy } from 'Store/Actions/settingsActions';
|
||||
import EditIndexerProxyModal from './EditIndexerProxyModal';
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
const section = 'settings.indexerProxies';
|
||||
|
||||
return {
|
||||
dispatchClearPendingChanges() {
|
||||
dispatch(clearPendingChanges({ section }));
|
||||
},
|
||||
|
||||
dispatchCancelTestIndexerProxy() {
|
||||
dispatch(cancelTestIndexerProxy({ section }));
|
||||
},
|
||||
|
||||
dispatchCancelSaveIndexerProxy() {
|
||||
dispatch(cancelSaveIndexerProxy({ section }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class EditIndexerProxyModalConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onModalClose = () => {
|
||||
this.props.dispatchClearPendingChanges();
|
||||
this.props.dispatchCancelTestIndexerProxy();
|
||||
this.props.dispatchCancelSaveIndexerProxy();
|
||||
this.props.onModalClose();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
dispatchClearPendingChanges,
|
||||
dispatchCancelTestIndexerProxy,
|
||||
dispatchCancelSaveIndexerProxy,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<EditIndexerProxyModal
|
||||
{...otherProps}
|
||||
onModalClose={this.onModalClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditIndexerProxyModalConnector.propTypes = {
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
dispatchClearPendingChanges: PropTypes.func.isRequired,
|
||||
dispatchCancelTestIndexerProxy: PropTypes.func.isRequired,
|
||||
dispatchCancelSaveIndexerProxy: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(null, createMapDispatchToProps)(EditIndexerProxyModalConnector);
|
||||
@@ -0,0 +1,11 @@
|
||||
.deleteButton {
|
||||
composes: button from '~Components/Link/Button.css';
|
||||
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.message {
|
||||
composes: alert from '~Components/Alert.css';
|
||||
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './EditIndexerProxyModalContent.css';
|
||||
|
||||
function EditIndexerProxyModalContent(props) {
|
||||
const {
|
||||
advancedSettings,
|
||||
isFetching,
|
||||
error,
|
||||
isSaving,
|
||||
isTesting,
|
||||
saveError,
|
||||
item,
|
||||
onInputChange,
|
||||
onFieldChange,
|
||||
onModalClose,
|
||||
onSavePress,
|
||||
onTestPress,
|
||||
onDeleteIndexerProxyPress,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const {
|
||||
id,
|
||||
implementationName,
|
||||
name,
|
||||
tags,
|
||||
fields,
|
||||
message
|
||||
} = item;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{`${id ? 'Edit' : 'Add'} Proxy - ${implementationName}`}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>
|
||||
{translate('UnableToAddANewIndexerProxyPleaseTryAgain')}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !error &&
|
||||
<Form {...otherProps}>
|
||||
{
|
||||
!!message &&
|
||||
<Alert
|
||||
className={styles.message}
|
||||
kind={message.value.type}
|
||||
>
|
||||
{message.value.message}
|
||||
</Alert>
|
||||
}
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Name')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="name"
|
||||
{...name}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Tags')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TAG}
|
||||
name="tags"
|
||||
helpText={translate('TagsHelpText')}
|
||||
{...tags}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
fields.map((field) => {
|
||||
return (
|
||||
<ProviderFieldFormGroup
|
||||
key={field.name}
|
||||
advancedSettings={advancedSettings}
|
||||
provider="indexerProxy"
|
||||
providerData={item}
|
||||
section="settings.indexerProxies"
|
||||
{...field}
|
||||
onChange={onFieldChange}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
</Form>
|
||||
}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
{
|
||||
id &&
|
||||
<Button
|
||||
className={styles.deleteButton}
|
||||
kind={kinds.DANGER}
|
||||
onPress={onDeleteIndexerProxyPress}
|
||||
>
|
||||
{translate('Delete')}
|
||||
</Button>
|
||||
}
|
||||
|
||||
<SpinnerErrorButton
|
||||
isSpinning={isTesting}
|
||||
error={saveError}
|
||||
onPress={onTestPress}
|
||||
>
|
||||
{translate('Test')}
|
||||
</SpinnerErrorButton>
|
||||
|
||||
<Button
|
||||
onPress={onModalClose}
|
||||
>
|
||||
{translate('Cancel')}
|
||||
</Button>
|
||||
|
||||
<SpinnerErrorButton
|
||||
isSpinning={isSaving}
|
||||
error={saveError}
|
||||
onPress={onSavePress}
|
||||
>
|
||||
{translate('Save')}
|
||||
</SpinnerErrorButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
EditIndexerProxyModalContent.propTypes = {
|
||||
advancedSettings: PropTypes.bool.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
isTesting: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
item: PropTypes.object.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onFieldChange: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onTestPress: PropTypes.func.isRequired,
|
||||
onDeleteIndexerProxyPress: PropTypes.func
|
||||
};
|
||||
|
||||
export default EditIndexerProxyModalContent;
|
||||
@@ -0,0 +1,88 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { saveIndexerProxy, setIndexerProxyFieldValue, setIndexerProxyValue, testIndexerProxy } from 'Store/Actions/settingsActions';
|
||||
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
||||
import EditIndexerProxyModalContent from './EditIndexerProxyModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.advancedSettings,
|
||||
createProviderSettingsSelector('indexerProxies'),
|
||||
(advancedSettings, indexerProxy) => {
|
||||
return {
|
||||
advancedSettings,
|
||||
...indexerProxy
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setIndexerProxyValue,
|
||||
setIndexerProxyFieldValue,
|
||||
saveIndexerProxy,
|
||||
testIndexerProxy
|
||||
};
|
||||
|
||||
class EditIndexerProxyModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
|
||||
this.props.onModalClose();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.setIndexerProxyValue({ name, value });
|
||||
}
|
||||
|
||||
onFieldChange = ({ name, value }) => {
|
||||
this.props.setIndexerProxyFieldValue({ name, value });
|
||||
}
|
||||
|
||||
onSavePress = () => {
|
||||
this.props.saveIndexerProxy({ id: this.props.id });
|
||||
}
|
||||
|
||||
onTestPress = () => {
|
||||
this.props.testIndexerProxy({ id: this.props.id });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EditIndexerProxyModalContent
|
||||
{...this.props}
|
||||
onSavePress={this.onSavePress}
|
||||
onTestPress={this.onTestPress}
|
||||
onInputChange={this.onInputChange}
|
||||
onFieldChange={this.onFieldChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditIndexerProxyModalContentConnector.propTypes = {
|
||||
id: PropTypes.number,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
item: PropTypes.object.isRequired,
|
||||
setIndexerProxyValue: PropTypes.func.isRequired,
|
||||
setIndexerProxyFieldValue: PropTypes.func.isRequired,
|
||||
saveIndexerProxy: PropTypes.func.isRequired,
|
||||
testIndexerProxy: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(EditIndexerProxyModalContentConnector);
|
||||
@@ -0,0 +1,20 @@
|
||||
.indexerProxies {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.addIndexerProxy {
|
||||
composes: indexerProxy from '~./IndexerProxy.css';
|
||||
|
||||
background-color: $cardAlternateBackgroundColor;
|
||||
color: $gray;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.center {
|
||||
display: inline-block;
|
||||
padding: 5px 20px 0;
|
||||
border: 1px solid $borderColor;
|
||||
border-radius: 4px;
|
||||
background-color: $white;
|
||||
}
|
||||
121
frontend/src/Settings/Indexers/IndexerProxies/IndexerProxies.js
Normal file
121
frontend/src/Settings/Indexers/IndexerProxies/IndexerProxies.js
Normal file
@@ -0,0 +1,121 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Card from 'Components/Card';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Icon from 'Components/Icon';
|
||||
import PageSectionContent from 'Components/Page/PageSectionContent';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AddIndexerProxyModal from './AddIndexerProxyModal';
|
||||
import EditIndexerProxyModalConnector from './EditIndexerProxyModalConnector';
|
||||
import IndexerProxy from './IndexerProxy';
|
||||
import styles from './IndexerProxies.css';
|
||||
|
||||
class IndexerProxies extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isAddIndexerProxyModalOpen: false,
|
||||
isEditIndexerProxyModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onAddIndexerProxyPress = () => {
|
||||
this.setState({ isAddIndexerProxyModalOpen: true });
|
||||
}
|
||||
|
||||
onAddIndexerProxyModalClose = ({ indexerProxySelected = false } = {}) => {
|
||||
this.setState({
|
||||
isAddIndexerProxyModalOpen: false,
|
||||
isEditIndexerProxyModalOpen: indexerProxySelected
|
||||
});
|
||||
}
|
||||
|
||||
onEditIndexerProxyModalClose = () => {
|
||||
this.setState({ isEditIndexerProxyModalOpen: false });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
items,
|
||||
tagList,
|
||||
indexerList,
|
||||
onConfirmDeleteIndexerProxy,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
isAddIndexerProxyModalOpen,
|
||||
isEditIndexerProxyModalOpen
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<FieldSet legend={translate('Indexer Proxies')}>
|
||||
<PageSectionContent
|
||||
errorMessage={translate('UnableToLoadIndexerProxies')}
|
||||
{...otherProps}
|
||||
>
|
||||
<div className={styles.indexerProxies}>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<IndexerProxy
|
||||
key={item.id}
|
||||
{...item}
|
||||
tagList={tagList}
|
||||
indexerList={indexerList}
|
||||
onConfirmDeleteIndexerProxy={onConfirmDeleteIndexerProxy}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
<Card
|
||||
className={styles.addIndexerProxy}
|
||||
onPress={this.onAddIndexerProxyPress}
|
||||
>
|
||||
<div className={styles.center}>
|
||||
<Icon
|
||||
name={icons.ADD}
|
||||
size={45}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<AddIndexerProxyModal
|
||||
isOpen={isAddIndexerProxyModalOpen}
|
||||
onModalClose={this.onAddIndexerProxyModalClose}
|
||||
/>
|
||||
|
||||
<EditIndexerProxyModalConnector
|
||||
isOpen={isEditIndexerProxyModalOpen}
|
||||
onModalClose={this.onEditIndexerProxyModalClose}
|
||||
/>
|
||||
</PageSectionContent>
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
IndexerProxies.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
indexerList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onConfirmDeleteIndexerProxy: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default IndexerProxies;
|
||||
@@ -0,0 +1,65 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { deleteIndexerProxy, fetchIndexerProxies } from 'Store/Actions/settingsActions';
|
||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import IndexerProxies from './IndexerProxies';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createSortedSectionSelector('settings.indexerProxies', sortByName),
|
||||
createSortedSectionSelector('indexers', sortByName),
|
||||
createTagsSelector(),
|
||||
(indexerProxies, indexers, tagList) => {
|
||||
return {
|
||||
...indexerProxies,
|
||||
indexerList: indexers.items,
|
||||
tagList
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchIndexerProxies,
|
||||
deleteIndexerProxy
|
||||
};
|
||||
|
||||
class IndexerProxiesConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchIndexerProxies();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onConfirmDeleteIndexerProxy = (id) => {
|
||||
this.props.deleteIndexerProxy({ id });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<IndexerProxies
|
||||
{...this.props}
|
||||
onConfirmDeleteIndexerProxy={this.onConfirmDeleteIndexerProxy}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
IndexerProxiesConnector.propTypes = {
|
||||
fetchIndexerProxies: PropTypes.func.isRequired,
|
||||
deleteIndexerProxy: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(IndexerProxiesConnector);
|
||||
@@ -0,0 +1,23 @@
|
||||
.indexerProxy {
|
||||
composes: card from '~Components/Card.css';
|
||||
|
||||
width: 290px;
|
||||
}
|
||||
|
||||
.name {
|
||||
@add-mixin truncate;
|
||||
|
||||
margin-bottom: 20px;
|
||||
font-weight: 300;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.indexers {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.enabled {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 5px;
|
||||
}
|
||||
144
frontend/src/Settings/Indexers/IndexerProxies/IndexerProxy.js
Normal file
144
frontend/src/Settings/Indexers/IndexerProxies/IndexerProxy.js
Normal file
@@ -0,0 +1,144 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Card from 'Components/Card';
|
||||
import Label from 'Components/Label';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import TagList from 'Components/TagList';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EditIndexerProxyModalConnector from './EditIndexerProxyModalConnector';
|
||||
import styles from './IndexerProxy.css';
|
||||
|
||||
class IndexerProxy extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isEditIndexerProxyModalOpen: false,
|
||||
isDeleteIndexerProxyModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onEditIndexerProxyPress = () => {
|
||||
this.setState({ isEditIndexerProxyModalOpen: true });
|
||||
}
|
||||
|
||||
onEditIndexerProxyModalClose = () => {
|
||||
this.setState({ isEditIndexerProxyModalOpen: false });
|
||||
}
|
||||
|
||||
onDeleteIndexerProxyPress = () => {
|
||||
this.setState({
|
||||
isEditIndexerProxyModalOpen: false,
|
||||
isDeleteIndexerProxyModalOpen: true
|
||||
});
|
||||
}
|
||||
|
||||
onDeleteIndexerProxyModalClose= () => {
|
||||
this.setState({ isDeleteIndexerProxyModalOpen: false });
|
||||
}
|
||||
|
||||
onConfirmDeleteIndexerProxy = () => {
|
||||
this.props.onConfirmDeleteIndexerProxy(this.props.id);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
tags,
|
||||
tagList,
|
||||
indexerList
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={styles.indexerProxy}
|
||||
overlayContent={true}
|
||||
onPress={this.onEditIndexerProxyPress}
|
||||
>
|
||||
<div className={styles.name}>
|
||||
{name}
|
||||
</div>
|
||||
|
||||
<TagList
|
||||
tags={tags}
|
||||
tagList={tagList}
|
||||
/>
|
||||
|
||||
<div className={styles.indexers}>
|
||||
{
|
||||
tags.map((t) => {
|
||||
const indexers = _.filter(indexerList, { tags: [t] });
|
||||
|
||||
if (!indexers || indexers.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return indexers.map((i) => {
|
||||
return (
|
||||
<Label
|
||||
key={i.name}
|
||||
kind={kinds.SUCCESS}
|
||||
>
|
||||
{i.name}
|
||||
</Label>
|
||||
);
|
||||
});
|
||||
})
|
||||
}
|
||||
</div>
|
||||
|
||||
{
|
||||
!tags || tags.length === 0 ?
|
||||
<Label
|
||||
kind={kinds.DISABLED}
|
||||
outline={true}
|
||||
>
|
||||
Disabled
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
|
||||
<EditIndexerProxyModalConnector
|
||||
id={id}
|
||||
isOpen={this.state.isEditIndexerProxyModalOpen}
|
||||
onModalClose={this.onEditIndexerProxyModalClose}
|
||||
onDeleteIndexerProxyPress={this.onDeleteIndexerProxyPress}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={this.state.isDeleteIndexerProxyModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('DeleteIndexerProxy')}
|
||||
message={translate('DeleteIndexerProxyMessageText', [name])}
|
||||
confirmLabel={translate('Delete')}
|
||||
onConfirm={this.onConfirmDeleteIndexerProxy}
|
||||
onCancel={this.onDeleteIndexerProxyModalClose}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
IndexerProxy.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
indexerList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onConfirmDeleteIndexerProxy: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default IndexerProxy;
|
||||
22
frontend/src/Settings/Indexers/IndexerSettings.js
Normal file
22
frontend/src/Settings/Indexers/IndexerSettings.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import IndexerProxiesConnector from './IndexerProxies/IndexerProxiesConnector';
|
||||
|
||||
function IndexerSettings() {
|
||||
return (
|
||||
<PageContent title={translate('Proxies')}>
|
||||
<SettingsToolbarConnector
|
||||
showSave={false}
|
||||
/>
|
||||
|
||||
<PageContentBody>
|
||||
<IndexerProxiesConnector />
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default IndexerSettings;
|
||||
@@ -26,7 +26,7 @@ function NotificationEventItems(props) {
|
||||
<div>
|
||||
<FormInputHelpText
|
||||
text={translate('NotificationTriggersHelpText')}
|
||||
link="https://wikijs.servarr.com/prowlarr/settings#connections"
|
||||
link="https://wiki.servarr.com/prowlarr/settings#connections"
|
||||
/>
|
||||
<div className={styles.events}>
|
||||
<div>
|
||||
|
||||
@@ -16,13 +16,24 @@ function Settings() {
|
||||
<PageContentBody>
|
||||
<Link
|
||||
className={styles.link}
|
||||
to="/settings/applications"
|
||||
to="/settings/indexers"
|
||||
>
|
||||
Applications
|
||||
{translate('Indexers')}
|
||||
</Link>
|
||||
|
||||
<div className={styles.summary}>
|
||||
Applications and settings to configure how prowlarr interacts with your PVR programs
|
||||
{translate('IndexerSettingsSummary')}
|
||||
</div>
|
||||
|
||||
<Link
|
||||
className={styles.link}
|
||||
to="/settings/applications"
|
||||
>
|
||||
{translate('Apps')}
|
||||
</Link>
|
||||
|
||||
<div className={styles.summary}>
|
||||
{translate('AppSettingsSummary')}
|
||||
</div>
|
||||
|
||||
<Link
|
||||
@@ -40,7 +51,7 @@ function Settings() {
|
||||
className={styles.link}
|
||||
to="/settings/connect"
|
||||
>
|
||||
Notifications
|
||||
{translate('Notifications')}
|
||||
</Link>
|
||||
|
||||
<div className={styles.summary}>
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import titleCase from 'Utilities/String/titleCase';
|
||||
|
||||
function TagDetailsDelayProfile(props) {
|
||||
const {
|
||||
preferredProtocol,
|
||||
enableUsenet,
|
||||
enableTorrent,
|
||||
usenetDelay,
|
||||
torrentDelay
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
Protocol: {titleCase(preferredProtocol)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{
|
||||
enableUsenet ?
|
||||
`Usenet Delay: ${usenetDelay}` :
|
||||
'Usenet disabled'
|
||||
}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{
|
||||
enableTorrent ?
|
||||
`Torrent Delay: ${torrentDelay}` :
|
||||
'Torrents disabled'
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
TagDetailsDelayProfile.propTypes = {
|
||||
preferredProtocol: PropTypes.string.isRequired,
|
||||
enableUsenet: PropTypes.bool.isRequired,
|
||||
enableTorrent: PropTypes.bool.isRequired,
|
||||
usenetDelay: PropTypes.number.isRequired,
|
||||
torrentDelay: PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
export default TagDetailsDelayProfile;
|
||||
@@ -1,26 +1,22 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Label from 'Components/Label';
|
||||
import Button from 'Components/Link/Button';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import split from 'Utilities/String/split';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import TagDetailsDelayProfile from './TagDetailsDelayProfile';
|
||||
import styles from './TagDetailsModalContent.css';
|
||||
|
||||
function TagDetailsModalContent(props) {
|
||||
const {
|
||||
label,
|
||||
isTagUsed,
|
||||
movies,
|
||||
delayProfiles,
|
||||
indexers,
|
||||
notifications,
|
||||
restrictions,
|
||||
indexerProxies,
|
||||
onModalClose,
|
||||
onDeleteTagPress
|
||||
} = props;
|
||||
@@ -40,13 +36,13 @@ function TagDetailsModalContent(props) {
|
||||
}
|
||||
|
||||
{
|
||||
!!movies.length &&
|
||||
<FieldSet legend={translate('Movies')}>
|
||||
!!indexers.length &&
|
||||
<FieldSet legend={translate('Indexers')}>
|
||||
{
|
||||
movies.map((item) => {
|
||||
indexers.map((item) => {
|
||||
return (
|
||||
<div key={item.id}>
|
||||
{item.title}
|
||||
{item.name}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
@@ -54,35 +50,6 @@ function TagDetailsModalContent(props) {
|
||||
</FieldSet>
|
||||
}
|
||||
|
||||
{
|
||||
!!delayProfiles.length &&
|
||||
<FieldSet legend={translate('DelayProfile')}>
|
||||
{
|
||||
delayProfiles.map((item) => {
|
||||
const {
|
||||
id,
|
||||
preferredProtocol,
|
||||
enableUsenet,
|
||||
enableTorrent,
|
||||
usenetDelay,
|
||||
torrentDelay
|
||||
} = item;
|
||||
|
||||
return (
|
||||
<TagDetailsDelayProfile
|
||||
key={id}
|
||||
preferredProtocol={preferredProtocol}
|
||||
enableUsenet={enableUsenet}
|
||||
enableTorrent={enableTorrent}
|
||||
usenetDelay={usenetDelay}
|
||||
torrentDelay={torrentDelay}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</FieldSet>
|
||||
}
|
||||
|
||||
{
|
||||
!!notifications.length &&
|
||||
<FieldSet legend={translate('Connections')}>
|
||||
@@ -99,44 +66,13 @@ function TagDetailsModalContent(props) {
|
||||
}
|
||||
|
||||
{
|
||||
!!restrictions.length &&
|
||||
<FieldSet legend={translate('Restrictions')}>
|
||||
!!indexerProxies.length &&
|
||||
<FieldSet legend={translate('Indexer Proxies')}>
|
||||
{
|
||||
restrictions.map((item) => {
|
||||
indexerProxies.map((item) => {
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className={styles.restriction}
|
||||
>
|
||||
<div>
|
||||
{
|
||||
split(item.required).map((r) => {
|
||||
return (
|
||||
<Label
|
||||
key={r}
|
||||
kind={kinds.SUCCESS}
|
||||
>
|
||||
{r}
|
||||
</Label>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{
|
||||
split(item.ignored).map((i) => {
|
||||
return (
|
||||
<Label
|
||||
key={i}
|
||||
kind={kinds.DANGER}
|
||||
>
|
||||
{i}
|
||||
</Label>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
<div key={item.id}>
|
||||
{item.name}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
@@ -171,10 +107,9 @@ function TagDetailsModalContent(props) {
|
||||
TagDetailsModalContent.propTypes = {
|
||||
label: PropTypes.string.isRequired,
|
||||
isTagUsed: PropTypes.bool.isRequired,
|
||||
movies: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
delayProfiles: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
indexers: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
notifications: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
restrictions: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
indexerProxies: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onDeleteTagPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createAllIndexersSelector from 'Store/Selectors/createAllIndexersSelector';
|
||||
import TagDetailsModalContent from './TagDetailsModalContent';
|
||||
|
||||
function findMatchingItems(ids, items) {
|
||||
@@ -9,35 +8,15 @@ function findMatchingItems(ids, items) {
|
||||
});
|
||||
}
|
||||
|
||||
function createUnorderedMatchingMoviesSelector() {
|
||||
function createMatchingIndexersSelector() {
|
||||
return createSelector(
|
||||
(state, { indexerIds }) => indexerIds,
|
||||
createAllIndexersSelector(),
|
||||
(state) => state.indexers.items,
|
||||
findMatchingItems
|
||||
);
|
||||
}
|
||||
|
||||
function createMatchingMoviesSelector() {
|
||||
return createSelector(
|
||||
createUnorderedMatchingMoviesSelector(),
|
||||
(movies) => {
|
||||
return movies.sort((movieA, movieB) => {
|
||||
const sortTitleA = movieA.sortTitle;
|
||||
const sortTitleB = movieB.sortTitle;
|
||||
|
||||
if (sortTitleA > sortTitleB) {
|
||||
return 1;
|
||||
} else if (sortTitleA < sortTitleB) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMatchingNotificationsSelector() {
|
||||
function createMatchingIndexerProxiesSelector() {
|
||||
return createSelector(
|
||||
(state, { notificationIds }) => notificationIds,
|
||||
(state) => state.settings.notifications.items,
|
||||
@@ -45,13 +24,23 @@ function createMatchingNotificationsSelector() {
|
||||
);
|
||||
}
|
||||
|
||||
function createMatchingNotificationsSelector() {
|
||||
return createSelector(
|
||||
(state, { indexerProxyIds }) => indexerProxyIds,
|
||||
(state) => state.settings.indexerProxies.items,
|
||||
findMatchingItems
|
||||
);
|
||||
}
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createMatchingMoviesSelector(),
|
||||
createMatchingIndexersSelector(),
|
||||
createMatchingIndexerProxiesSelector(),
|
||||
createMatchingNotificationsSelector(),
|
||||
(movies, notifications) => {
|
||||
(indexers, indexerProxies, notifications) => {
|
||||
return {
|
||||
movies,
|
||||
indexers,
|
||||
indexerProxies,
|
||||
notifications
|
||||
};
|
||||
}
|
||||
|
||||
@@ -53,11 +53,9 @@ class Tag extends Component {
|
||||
render() {
|
||||
const {
|
||||
label,
|
||||
delayProfileIds,
|
||||
notificationIds,
|
||||
restrictionIds,
|
||||
importListIds,
|
||||
movieIds
|
||||
indexerIds,
|
||||
indexerProxyIds
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@@ -66,11 +64,9 @@ class Tag extends Component {
|
||||
} = this.state;
|
||||
|
||||
const isTagUsed = !!(
|
||||
delayProfileIds.length ||
|
||||
indexerIds.length ||
|
||||
notificationIds.length ||
|
||||
restrictionIds.length ||
|
||||
importListIds.length ||
|
||||
movieIds.length
|
||||
indexerProxyIds.length
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -87,16 +83,9 @@ class Tag extends Component {
|
||||
isTagUsed &&
|
||||
<div>
|
||||
{
|
||||
!!movieIds.length &&
|
||||
!!indexerIds.length &&
|
||||
<div>
|
||||
{movieIds.length} movies
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
!!delayProfileIds.length &&
|
||||
<div>
|
||||
{delayProfileIds.length} delay profile{delayProfileIds.length > 1 && 's'}
|
||||
{indexerIds.length} indexer{indexerIds.length > 1 && 's'}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -108,16 +97,9 @@ class Tag extends Component {
|
||||
}
|
||||
|
||||
{
|
||||
!!restrictionIds.length &&
|
||||
!!indexerProxyIds.length &&
|
||||
<div>
|
||||
{restrictionIds.length} restriction{restrictionIds.length > 1 && 's'}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
!!importListIds.length &&
|
||||
<div>
|
||||
{importListIds.length} list{importListIds.length > 1 && 's'}
|
||||
{indexerProxyIds.length} indexerProxy{indexerProxyIds.length > 1 && 's'}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -133,11 +115,9 @@ class Tag extends Component {
|
||||
<TagDetailsModal
|
||||
label={label}
|
||||
isTagUsed={isTagUsed}
|
||||
movieIds={movieIds}
|
||||
delayProfileIds={delayProfileIds}
|
||||
indexerIds={indexerIds}
|
||||
notificationIds={notificationIds}
|
||||
restrictionIds={restrictionIds}
|
||||
importListIds={importListIds}
|
||||
indexerProxyIds={indexerProxyIds}
|
||||
isOpen={isDetailsModalOpen}
|
||||
onModalClose={this.onDetailsModalClose}
|
||||
onDeleteTagPress={this.onDeleteTagPress}
|
||||
@@ -160,20 +140,16 @@ class Tag extends Component {
|
||||
Tag.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
delayProfileIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
notificationIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
restrictionIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
importListIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
movieIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
indexerIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
indexerProxyIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
onConfirmDeleteTag: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
Tag.defaultProps = {
|
||||
delayProfileIds: [],
|
||||
indexerIds: [],
|
||||
notificationIds: [],
|
||||
restrictionIds: [],
|
||||
importListIds: [],
|
||||
movieIds: []
|
||||
indexerProxyIds: []
|
||||
};
|
||||
|
||||
export default Tag;
|
||||
|
||||
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchNotifications } from 'Store/Actions/settingsActions';
|
||||
import { fetchIndexerProxies, fetchNotifications } from 'Store/Actions/settingsActions';
|
||||
import { fetchTagDetails } from 'Store/Actions/tagActions';
|
||||
import Tags from './Tags';
|
||||
|
||||
@@ -26,7 +26,8 @@ function createMapStateToProps() {
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchFetchTagDetails: fetchTagDetails,
|
||||
dispatchFetchNotifications: fetchNotifications
|
||||
dispatchFetchNotifications: fetchNotifications,
|
||||
dispatchFetchIndexerProxies: fetchIndexerProxies
|
||||
};
|
||||
|
||||
class MetadatasConnector extends Component {
|
||||
@@ -37,11 +38,13 @@ class MetadatasConnector extends Component {
|
||||
componentDidMount() {
|
||||
const {
|
||||
dispatchFetchTagDetails,
|
||||
dispatchFetchNotifications
|
||||
dispatchFetchNotifications,
|
||||
dispatchFetchIndexerProxies
|
||||
} = this.props;
|
||||
|
||||
dispatchFetchTagDetails();
|
||||
dispatchFetchNotifications();
|
||||
dispatchFetchIndexerProxies();
|
||||
}
|
||||
|
||||
//
|
||||
@@ -58,7 +61,8 @@ class MetadatasConnector extends Component {
|
||||
|
||||
MetadatasConnector.propTypes = {
|
||||
dispatchFetchTagDetails: PropTypes.func.isRequired,
|
||||
dispatchFetchNotifications: PropTypes.func.isRequired
|
||||
dispatchFetchNotifications: PropTypes.func.isRequired,
|
||||
dispatchFetchIndexerProxies: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(MetadatasConnector);
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
import getSectionState from 'Utilities/State/getSectionState';
|
||||
import updateSectionState from 'Utilities/State/updateSectionState';
|
||||
|
||||
const blacklistedProperties = [
|
||||
const omittedProperties = [
|
||||
'section',
|
||||
'id'
|
||||
];
|
||||
@@ -31,7 +31,7 @@ export default function createHandleActions(handlers, defaultState, section) {
|
||||
|
||||
if (section === baseSection) {
|
||||
const newState = Object.assign(getSectionState(state, payloadSection),
|
||||
_.omit(payload, blacklistedProperties));
|
||||
_.omit(payload, omittedProperties));
|
||||
|
||||
return updateSectionState(state, payloadSection, newState);
|
||||
}
|
||||
|
||||
110
frontend/src/Store/Actions/Settings/indexerProxies.js
Normal file
110
frontend/src/Store/Actions/Settings/indexerProxies.js
Normal file
@@ -0,0 +1,110 @@
|
||||
import { createAction } from 'redux-actions';
|
||||
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
|
||||
import createFetchSchemaHandler from 'Store/Actions/Creators/createFetchSchemaHandler';
|
||||
import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler';
|
||||
import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler';
|
||||
import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler';
|
||||
import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
|
||||
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
||||
import { createThunk } from 'Store/thunks';
|
||||
import selectProviderSchema from 'Utilities/State/selectProviderSchema';
|
||||
|
||||
//
|
||||
// Variables
|
||||
|
||||
const section = 'settings.indexerProxies';
|
||||
|
||||
//
|
||||
// Actions Types
|
||||
|
||||
export const FETCH_INDEXER_PROXYS = 'settings/indexerProxies/fetchIndexerProxies';
|
||||
export const FETCH_INDEXER_PROXY_SCHEMA = 'settings/indexerProxies/fetchIndexerProxySchema';
|
||||
export const SELECT_INDEXER_PROXY_SCHEMA = 'settings/indexerProxies/selectIndexerProxySchema';
|
||||
export const SET_INDEXER_PROXY_VALUE = 'settings/indexerProxies/setIndexerProxyValue';
|
||||
export const SET_INDEXER_PROXY_FIELD_VALUE = 'settings/indexerProxies/setIndexerProxyFieldValue';
|
||||
export const SAVE_INDEXER_PROXY = 'settings/indexerProxies/saveIndexerProxy';
|
||||
export const CANCEL_SAVE_INDEXER_PROXY = 'settings/indexerProxies/cancelSaveIndexerProxy';
|
||||
export const DELETE_INDEXER_PROXY = 'settings/indexerProxies/deleteIndexerProxy';
|
||||
export const TEST_INDEXER_PROXY = 'settings/indexerProxies/testIndexerProxy';
|
||||
export const CANCEL_TEST_INDEXER_PROXY = 'settings/indexerProxies/cancelTestIndexerProxy';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
|
||||
export const fetchIndexerProxies = createThunk(FETCH_INDEXER_PROXYS);
|
||||
export const fetchIndexerProxySchema = createThunk(FETCH_INDEXER_PROXY_SCHEMA);
|
||||
export const selectIndexerProxySchema = createAction(SELECT_INDEXER_PROXY_SCHEMA);
|
||||
|
||||
export const saveIndexerProxy = createThunk(SAVE_INDEXER_PROXY);
|
||||
export const cancelSaveIndexerProxy = createThunk(CANCEL_SAVE_INDEXER_PROXY);
|
||||
export const deleteIndexerProxy = createThunk(DELETE_INDEXER_PROXY);
|
||||
export const testIndexerProxy = createThunk(TEST_INDEXER_PROXY);
|
||||
export const cancelTestIndexerProxy = createThunk(CANCEL_TEST_INDEXER_PROXY);
|
||||
|
||||
export const setIndexerProxyValue = createAction(SET_INDEXER_PROXY_VALUE, (payload) => {
|
||||
return {
|
||||
section,
|
||||
...payload
|
||||
};
|
||||
});
|
||||
|
||||
export const setIndexerProxyFieldValue = createAction(SET_INDEXER_PROXY_FIELD_VALUE, (payload) => {
|
||||
return {
|
||||
section,
|
||||
...payload
|
||||
};
|
||||
});
|
||||
|
||||
//
|
||||
// Details
|
||||
|
||||
export default {
|
||||
|
||||
//
|
||||
// State
|
||||
|
||||
defaultState: {
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
isSchemaFetching: false,
|
||||
isSchemaPopulated: false,
|
||||
schemaError: null,
|
||||
schema: [],
|
||||
selectedSchema: {},
|
||||
isSaving: false,
|
||||
saveError: null,
|
||||
isTesting: false,
|
||||
items: [],
|
||||
pendingChanges: {}
|
||||
},
|
||||
|
||||
//
|
||||
// Action Handlers
|
||||
|
||||
actionHandlers: {
|
||||
[FETCH_INDEXER_PROXYS]: createFetchHandler(section, '/indexerProxy'),
|
||||
[FETCH_INDEXER_PROXY_SCHEMA]: createFetchSchemaHandler(section, '/indexerProxy/schema'),
|
||||
|
||||
[SAVE_INDEXER_PROXY]: createSaveProviderHandler(section, '/indexerProxy'),
|
||||
[CANCEL_SAVE_INDEXER_PROXY]: createCancelSaveProviderHandler(section),
|
||||
[DELETE_INDEXER_PROXY]: createRemoveItemHandler(section, '/indexerProxy'),
|
||||
[TEST_INDEXER_PROXY]: createTestProviderHandler(section, '/indexerProxy'),
|
||||
[CANCEL_TEST_INDEXER_PROXY]: createCancelTestProviderHandler(section)
|
||||
},
|
||||
|
||||
//
|
||||
// Reducers
|
||||
|
||||
reducers: {
|
||||
[SET_INDEXER_PROXY_VALUE]: createSetSettingValueReducer(section),
|
||||
[SET_INDEXER_PROXY_FIELD_VALUE]: createSetProviderFieldValueReducer(section),
|
||||
|
||||
[SELECT_INDEXER_PROXY_SCHEMA]: (state, { payload }) => {
|
||||
return selectProviderSchema(state, section, payload, (selectedSchema) => {
|
||||
return selectedSchema;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
@@ -202,7 +202,8 @@ export const defaultState = {
|
||||
|
||||
export const persistState = [
|
||||
'releases.customFilters',
|
||||
'releases.selectedFilterKey'
|
||||
'releases.selectedFilterKey',
|
||||
'releases.columns'
|
||||
];
|
||||
|
||||
//
|
||||
|
||||
@@ -7,6 +7,7 @@ import development from './Settings/development';
|
||||
import downloadClients from './Settings/downloadClients';
|
||||
import general from './Settings/general';
|
||||
import indexerCategories from './Settings/indexerCategories';
|
||||
import indexerProxies from './Settings/indexerProxies';
|
||||
import languages from './Settings/languages';
|
||||
import notifications from './Settings/notifications';
|
||||
import ui from './Settings/ui';
|
||||
@@ -14,6 +15,7 @@ import ui from './Settings/ui';
|
||||
export * from './Settings/downloadClients';
|
||||
export * from './Settings/general';
|
||||
export * from './Settings/indexerCategories';
|
||||
export * from './Settings/indexerProxies';
|
||||
export * from './Settings/languages';
|
||||
export * from './Settings/notifications';
|
||||
export * from './Settings/applications';
|
||||
@@ -35,6 +37,7 @@ export const defaultState = {
|
||||
downloadClients: downloadClients.defaultState,
|
||||
general: general.defaultState,
|
||||
indexerCategories: indexerCategories.defaultState,
|
||||
indexerProxies: indexerProxies.defaultState,
|
||||
languages: languages.defaultState,
|
||||
notifications: notifications.defaultState,
|
||||
applications: applications.defaultState,
|
||||
@@ -64,6 +67,7 @@ export const actionHandlers = handleThunks({
|
||||
...downloadClients.actionHandlers,
|
||||
...general.actionHandlers,
|
||||
...indexerCategories.actionHandlers,
|
||||
...indexerProxies.actionHandlers,
|
||||
...languages.actionHandlers,
|
||||
...notifications.actionHandlers,
|
||||
...applications.actionHandlers,
|
||||
@@ -84,6 +88,7 @@ export const reducers = createHandleActions({
|
||||
...downloadClients.reducers,
|
||||
...general.reducers,
|
||||
...indexerCategories.reducers,
|
||||
...indexerProxies.reducers,
|
||||
...languages.reducers,
|
||||
...notifications.reducers,
|
||||
...applications.reducers,
|
||||
|
||||
@@ -13,7 +13,7 @@ function createHealthCheckSelector() {
|
||||
source: 'UI',
|
||||
type: 'warning',
|
||||
message: translate('CouldNotConnectSignalR'),
|
||||
wikiUrl: 'https://wikijs.servarr.com/prowlarr/system#could-not-connect-to-signalr'
|
||||
wikiUrl: 'https://wiki.servarr.com/prowlarr/system#could-not-connect-to-signalr'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ function getInternalLink(source) {
|
||||
function getTestLink(source, props) {
|
||||
switch (source) {
|
||||
case 'IndexerStatusCheck':
|
||||
case 'IndexerLongTermStatusCheck':
|
||||
return (
|
||||
<SpinnerIconButton
|
||||
name={icons.TEST}
|
||||
|
||||
@@ -22,7 +22,7 @@ class MoreInfo extends Component {
|
||||
|
||||
<DescriptionListItemTitle>{translate('Wiki')}</DescriptionListItemTitle>
|
||||
<DescriptionListItemDescription>
|
||||
<Link to="https://wikijs.servarr.com/prowlarr">wikijs.servarr.com/prowlarr</Link>
|
||||
<Link to="https://wiki.servarr.com/prowlarr">wiki.servarr.com/prowlarr</Link>
|
||||
</DescriptionListItemDescription>
|
||||
|
||||
<DescriptionListItemTitle>{translate('Reddit')}</DescriptionListItemTitle>
|
||||
|
||||
@@ -252,7 +252,7 @@
|
||||
</span>
|
||||
|
||||
<a
|
||||
href="https://wikijs.servarr.com/prowlarr/faq#help-i-have-locked-myself-out"
|
||||
href="https://wiki.servarr.com/prowlarr/faq#help-i-have-locked-myself-out"
|
||||
class="forgot-password"
|
||||
>Forgot your password?</a
|
||||
>
|
||||
|
||||
16
package.json
16
package.json
@@ -30,9 +30,9 @@
|
||||
"@fortawesome/free-regular-svg-icons": "5.15.3",
|
||||
"@fortawesome/free-solid-svg-icons": "5.15.3",
|
||||
"@fortawesome/react-fontawesome": "0.1.14",
|
||||
"@microsoft/signalr": "5.0.6",
|
||||
"@sentry/browser": "6.3.1",
|
||||
"@sentry/integrations": "6.3.1",
|
||||
"@microsoft/signalr": "5.0.9",
|
||||
"@sentry/browser": "6.10.0",
|
||||
"@sentry/integrations": "6.10.0",
|
||||
"chart.js": "3.2.0",
|
||||
"classnames": "2.3.1",
|
||||
"clipboard": "2.0.8",
|
||||
@@ -98,12 +98,12 @@
|
||||
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
|
||||
"core-js": "3.11.0",
|
||||
"css-loader": "5.2.4",
|
||||
"eslint": "7.25.0",
|
||||
"eslint": "7.31.0",
|
||||
"eslint-plugin-filenames": "1.3.2",
|
||||
"eslint-plugin-import": "2.22.1",
|
||||
"eslint-plugin-react": "7.23.2",
|
||||
"eslint-plugin-import": "2.23.4",
|
||||
"eslint-plugin-react": "7.24.0",
|
||||
"eslint-plugin-simple-import-sort": "7.0.0",
|
||||
"esprint": "2.0.0",
|
||||
"esprint": "3.1.0",
|
||||
"file-loader": "6.2.0",
|
||||
"filemanager-webpack-plugin": "5.0.0",
|
||||
"html-webpack-plugin": "5.3.1",
|
||||
@@ -125,7 +125,7 @@
|
||||
"webpack": "5.35.1",
|
||||
"webpack-cli": "4.6.0",
|
||||
"webpack-livereload-plugin": "3.0.1",
|
||||
"stylelint": "13.13.0",
|
||||
"stylelint": "13.13.1",
|
||||
"stylelint-order": "4.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
|
||||
<!-- Standard testing packages -->
|
||||
<ItemGroup Condition="'$(TestProject)'=='true'">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
|
||||
<PackageReference Include="NunitXml.TestLogger" Version="3.0.97" />
|
||||
|
||||
@@ -19,6 +19,9 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
||||
[TestCase(@"http://nzb.su/getnzb/2b51db35e1912ffc138825a12b9933d2.nzb&i=37292&r=2b51db35e1910123321025a12b9933d2")]
|
||||
[TestCase(@"https://horrorcharnel.org/takeloginhorror.php: username=mySecret&password=mySecret&use_sslvalue==&perm_ssl=1&submitme=X&use_ssl=1&returnto=%2F&captchaSelection=1230456")]
|
||||
[TestCase(@"https://torrentdb.net/login: _token=2b51db35e1912ffc138825a12b9933d2&username=mySecret&password=mySecret&remember=on")]
|
||||
[TestCase(@" var authkey = ""2b51db35e1910123321025a12b9933d2"";")]
|
||||
[TestCase(@"https://hd-space.org/index.php?page=login: uid=mySecret&pwd=mySecret")]
|
||||
[TestCase(@"https://beyond-hd.me/api/torrents/2b51db35e1912ffc138825a12b9933d2")]
|
||||
|
||||
// NzbGet
|
||||
[TestCase(@"{ ""Name"" : ""ControlUsername"", ""Value"" : ""mySecret"" }, { ""Name"" : ""ControlPassword"", ""Value"" : ""mySecret"" }, ")]
|
||||
@@ -90,5 +93,22 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
||||
|
||||
cleansedMessage.Should().Be(message);
|
||||
}
|
||||
|
||||
[TestCase(@"&useToken=2b51db35e1910123321025a12b9933d2")]
|
||||
[TestCase(@"&useToken=2b51db35e1910123321025a12b9933d2")]
|
||||
public void should_not_clean_usetoken(string message)
|
||||
{
|
||||
var cleansedMessage = CleanseLogMessage.Cleanse(message);
|
||||
|
||||
cleansedMessage.Should().Be(message);
|
||||
}
|
||||
|
||||
[TestCase(@"https://www.torrentleech.org/torrents/browse/list/imdbID/tt8005374/categories/29,2,26,27,32,44,7,34,35")]
|
||||
public void should_not_clean_url(string message)
|
||||
{
|
||||
var cleansedMessage = CleanseLogMessage.Cleanse(message);
|
||||
|
||||
cleansedMessage.Should().Be(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,16 @@ namespace NzbDrone.Common.Extensions
|
||||
return dateTime >= afterDateTime && dateTime <= beforeDateTime;
|
||||
}
|
||||
|
||||
public static DateTime EndOfDay(this DateTime date)
|
||||
{
|
||||
return new DateTime(date.Year, date.Month, date.Day, 23, 59, 59, 999);
|
||||
}
|
||||
|
||||
public static DateTime StartOfDay(this DateTime date)
|
||||
{
|
||||
return new DateTime(date.Year, date.Month, date.Day, 0, 0, 0, 0);
|
||||
}
|
||||
|
||||
public static DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,5 +194,21 @@ namespace NzbDrone.Common.Extensions
|
||||
var inputBytes = encoding.GetBytes(searchString);
|
||||
return encoding.GetString(WebUtility.UrlDecodeToBytes(inputBytes, 0, inputBytes.Length));
|
||||
}
|
||||
|
||||
public static string CleanFileName(this string name)
|
||||
{
|
||||
string result = name;
|
||||
string[] badCharacters = { "\\", "/", "<", ">", "?", "*", ":", "|", "\"" };
|
||||
string[] goodCharacters = { "+", "+", "", "", "!", "-", "-", "", "" };
|
||||
|
||||
result = result.Replace(": ", " - ");
|
||||
|
||||
for (int i = 0; i < badCharacters.Length; i++)
|
||||
{
|
||||
result = result.Replace(badCharacters[i], goodCharacters[i]);
|
||||
}
|
||||
|
||||
return result.TrimStart(' ', '.').TrimEnd(' ');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ namespace NzbDrone.Common.Http.Dispatchers
|
||||
webRequest.Timeout = (int)Math.Ceiling(request.RequestTimeout.TotalMilliseconds);
|
||||
}
|
||||
|
||||
webRequest.Proxy = GetProxy(request.Url);
|
||||
webRequest.Proxy = request.Proxy ?? GetProxy(request.Url);
|
||||
|
||||
if (request.Headers != null)
|
||||
{
|
||||
|
||||
@@ -61,7 +61,7 @@ namespace NzbDrone.Common.Http
|
||||
_cookieContainerCache = cacheManager.GetCache<CookieContainer>(typeof(HttpClient));
|
||||
}
|
||||
|
||||
public async Task<HttpResponse> ExecuteAsync(HttpRequest request)
|
||||
public virtual async Task<HttpResponse> ExecuteAsync(HttpRequest request)
|
||||
{
|
||||
var cookieContainer = InitializeRequestCookies(request);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
@@ -30,6 +31,8 @@ namespace NzbDrone.Common.Http
|
||||
public HttpUri Url { get; set; }
|
||||
public HttpMethod Method { get; set; }
|
||||
public HttpHeader Headers { get; set; }
|
||||
public Encoding Encoding { get; set; }
|
||||
public IWebProxy Proxy { get; set; }
|
||||
public byte[] ContentData { get; set; }
|
||||
public string ContentSummary { get; set; }
|
||||
public bool SuppressHttpError { get; set; }
|
||||
@@ -75,8 +78,28 @@ namespace NzbDrone.Common.Http
|
||||
|
||||
public void SetContent(string data)
|
||||
{
|
||||
var encoding = HttpHeader.GetEncodingFromContentType(Headers.ContentType);
|
||||
ContentData = encoding.GetBytes(data);
|
||||
if (Encoding != null)
|
||||
{
|
||||
ContentData = Encoding.GetBytes(data);
|
||||
}
|
||||
else
|
||||
{
|
||||
var encoding = HttpHeader.GetEncodingFromContentType(Headers.ContentType);
|
||||
ContentData = encoding.GetBytes(data);
|
||||
}
|
||||
}
|
||||
|
||||
public string GetContent()
|
||||
{
|
||||
if (Encoding != null)
|
||||
{
|
||||
return Encoding.GetString(ContentData);
|
||||
}
|
||||
else
|
||||
{
|
||||
var encoding = HttpHeader.GetEncodingFromContentType(Headers.ContentType);
|
||||
return encoding.GetString(ContentData);
|
||||
}
|
||||
}
|
||||
|
||||
public void AddBasicAuthentication(string username, string password)
|
||||
|
||||
@@ -47,7 +47,14 @@ namespace NzbDrone.Common.Http
|
||||
{
|
||||
if (_content == null)
|
||||
{
|
||||
_content = Headers.GetEncodingFromContentType().GetString(ResponseData);
|
||||
if (Request.Encoding != null)
|
||||
{
|
||||
_content = Request.Encoding.GetString(ResponseData);
|
||||
}
|
||||
else
|
||||
{
|
||||
_content = Headers.GetEncodingFromContentType().GetString(ResponseData);
|
||||
}
|
||||
}
|
||||
|
||||
return _content;
|
||||
|
||||
@@ -11,13 +11,15 @@ namespace NzbDrone.Common.Instrumentation
|
||||
private static readonly Regex[] CleansingRules = new[]
|
||||
{
|
||||
// Url
|
||||
new Regex(@"(?<=\?|&|: |;)(apikey|token|passkey|auth|authkey|user|uid|api|[a-z_]*apikey|account|passwd)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"(?<=\?|&| )[^=]*?(_?token|username|password)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"torrentleech\.org/(?!rss)(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"torrentleech\.org/rss/download/[0-9]+/(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"(?<=\?|&|: |;)(apikey|token|passkey|auth|authkey|user|uid|api|[a-z_]*apikey|account|passwd|pwd)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"(?<=\?|&| )[^=]*?(_?(?<!use)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),
|
||||
new Regex(@"iptorrents\.com/[/a-z0-9?&;]*?(?:[?&;](u|tp)=(?<secret>[^&=;]+?))+(?= |;|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"/fetch/[a-z0-9]{32}/(?<secret>[a-z0-9]{32})", RegexOptions.Compiled),
|
||||
new Regex(@"getnzb.*?(?<=\?|&)(r)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"(?<=authkey = "")(?<secret>[^&=]+?)(?="")", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"(?<=beyond-hd\.[a-z]+/api/torrents/)(?<secret>[^&=][a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
// Path
|
||||
new Regex(@"""C:\\Users\\(?<secret>[^\""]+?)(\\|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DotNet4.SocksProxy" Version="1.4.0.1" />
|
||||
<PackageReference Include="DryIoc.dll" Version="4.7.6" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
|
||||
<PackageReference Include="DryIoc.dll" Version="4.8.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.2" />
|
||||
<PackageReference Include="NLog.Extensions.Logging" Version="1.7.2" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
<PackageReference Include="NLog" Version="4.7.9" />
|
||||
<PackageReference Include="Sentry" Version="3.3.3" />
|
||||
<PackageReference Include="Sentry" Version="3.8.3" />
|
||||
<PackageReference Include="SharpZipLib" Version="1.3.1" />
|
||||
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
||||
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.113.0-0" />
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace NzbDrone.Core.Test.HealthCheck
|
||||
[TestFixture]
|
||||
public class HealthCheckFixture : CoreTest
|
||||
{
|
||||
private const string WikiRoot = "https://wikijs.servarr.com/";
|
||||
private const string WikiRoot = "https://wiki.servarr.com/";
|
||||
|
||||
[TestCase("I blew up because of some weird user mistake", null, WikiRoot + "prowlarr/system#i-blew-up-because-of-some-weird-user-mistake")]
|
||||
[TestCase("I blew up because of some weird user mistake", "#my-health-check", WikiRoot + "prowlarr/system#my-health-check")]
|
||||
|
||||
@@ -33,9 +33,9 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
|
||||
{
|
||||
var recentFeed = ReadAllText(@"Files/Indexers/PrivateHD/recentfeed.json");
|
||||
|
||||
Mocker.GetMock<IHttpClient>()
|
||||
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
|
||||
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed)));
|
||||
Mocker.GetMock<IIndexerHttpClient>()
|
||||
.Setup(o => o.ExecuteAsync(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;
|
||||
|
||||
|
||||
@@ -32,9 +32,9 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
|
||||
{
|
||||
var recentFeed = ReadAllText(@"Files/Indexers/FileList/recentfeed.json");
|
||||
|
||||
Mocker.GetMock<IHttpClient>()
|
||||
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
|
||||
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed)));
|
||||
Mocker.GetMock<IIndexerHttpClient>()
|
||||
.Setup(o => o.ExecuteAsync(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;
|
||||
|
||||
|
||||
@@ -19,7 +19,8 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
|
||||
Subject.Settings = new FileListSettings()
|
||||
{
|
||||
Passkey = "abcd",
|
||||
Username = "somename"
|
||||
Username = "somename",
|
||||
BaseUrl = "https://filelist.io"
|
||||
};
|
||||
|
||||
Subject.Capabilities = new IndexerCapabilities
|
||||
@@ -54,8 +55,6 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
|
||||
SearchTerm = "Star Wars",
|
||||
Categories = new int[] { 2000 }
|
||||
};
|
||||
|
||||
Subject.BaseUrl = "https://filelist.io";
|
||||
}
|
||||
|
||||
private void MovieWithoutIMDB()
|
||||
|
||||
@@ -44,9 +44,9 @@ namespace NzbDrone.Core.Test.IndexerTests.HDBitsTests
|
||||
{
|
||||
var responseJson = ReadAllText(fileName);
|
||||
|
||||
Mocker.GetMock<IHttpClient>()
|
||||
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.POST)))
|
||||
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), responseJson)));
|
||||
Mocker.GetMock<IIndexerHttpClient>()
|
||||
.Setup(o => o.ExecuteAsync(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;
|
||||
|
||||
@@ -73,9 +73,9 @@ namespace NzbDrone.Core.Test.IndexerTests.HDBitsTests
|
||||
{
|
||||
var responseJson = new { status = 5, message = "Invalid authentication credentials" }.ToJson();
|
||||
|
||||
Mocker.GetMock<IHttpClient>()
|
||||
.Setup(v => v.ExecuteAsync(It.IsAny<HttpRequest>()))
|
||||
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), Encoding.UTF8.GetBytes(responseJson))));
|
||||
Mocker.GetMock<IIndexerHttpClient>()
|
||||
.Setup(o => o.ExecuteAsync(It.IsAny<HttpRequest>(), Subject.Definition))
|
||||
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), Encoding.UTF8.GetBytes(responseJson))));
|
||||
|
||||
var torrents = (await Subject.Fetch(_movieSearchCriteria)).Releases;
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Indexers.Newznab;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
@@ -14,6 +16,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
public class NewznabCapabilitiesProviderFixture : CoreTest<NewznabCapabilitiesProvider>
|
||||
{
|
||||
private NewznabSettings _settings;
|
||||
private IndexerDefinition _definition;
|
||||
private string _caps;
|
||||
|
||||
[SetUp]
|
||||
@@ -24,14 +27,24 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
BaseUrl = "http://indxer.local"
|
||||
};
|
||||
|
||||
_definition = new IndexerDefinition()
|
||||
{
|
||||
Id = 5,
|
||||
Name = "Newznab",
|
||||
Settings = new NewznabSettings()
|
||||
{
|
||||
BaseUrl = "http://indexer.local/"
|
||||
}
|
||||
};
|
||||
|
||||
_caps = ReadAllText("Files/Indexers/Newznab/newznab_caps.xml");
|
||||
}
|
||||
|
||||
private void GivenCapsResponse(string caps)
|
||||
{
|
||||
Mocker.GetMock<IHttpClient>()
|
||||
.Setup(o => o.Get(It.IsAny<HttpRequest>()))
|
||||
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new CookieCollection(), caps));
|
||||
Mocker.GetMock<IIndexerHttpClient>()
|
||||
.Setup(o => o.Execute(It.IsAny<HttpRequest>(), It.IsAny<IndexerDefinition>()))
|
||||
.Returns<HttpRequest, IndexerDefinition>((r, d) => new HttpResponse(r, new HttpHeader(), new CookieCollection(), caps));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -39,11 +52,11 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
{
|
||||
GivenCapsResponse(_caps);
|
||||
|
||||
Subject.GetCapabilities(_settings);
|
||||
Subject.GetCapabilities(_settings);
|
||||
Subject.GetCapabilities(_settings, _definition);
|
||||
Subject.GetCapabilities(_settings, _definition);
|
||||
|
||||
Mocker.GetMock<IHttpClient>()
|
||||
.Verify(o => o.Get(It.IsAny<HttpRequest>()), Times.Once());
|
||||
Mocker.GetMock<IIndexerHttpClient>()
|
||||
.Verify(o => o.Execute(It.IsAny<HttpRequest>(), It.IsAny<IndexerDefinition>()), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -51,7 +64,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
{
|
||||
GivenCapsResponse(_caps);
|
||||
|
||||
var caps = Subject.GetCapabilities(_settings);
|
||||
var caps = Subject.GetCapabilities(_settings, _definition);
|
||||
|
||||
caps.LimitsDefault.Value.Should().Be(25);
|
||||
caps.LimitsMax.Value.Should().Be(60);
|
||||
@@ -62,7 +75,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
{
|
||||
GivenCapsResponse(_caps.Replace("<limits", "<abclimits"));
|
||||
|
||||
var caps = Subject.GetCapabilities(_settings);
|
||||
var caps = Subject.GetCapabilities(_settings, _definition);
|
||||
|
||||
caps.LimitsDefault.Value.Should().Be(100);
|
||||
caps.LimitsMax.Value.Should().Be(100);
|
||||
@@ -71,11 +84,11 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
[Test]
|
||||
public void should_throw_if_failed_to_get()
|
||||
{
|
||||
Mocker.GetMock<IHttpClient>()
|
||||
.Setup(o => o.Get(It.IsAny<HttpRequest>()))
|
||||
Mocker.GetMock<IIndexerHttpClient>()
|
||||
.Setup(o => o.Execute(It.IsAny<HttpRequest>(), It.IsAny<IndexerDefinition>()))
|
||||
.Throws<Exception>();
|
||||
|
||||
Assert.Throws<Exception>(() => Subject.GetCapabilities(_settings));
|
||||
Assert.Throws<Exception>(() => Subject.GetCapabilities(_settings, _definition));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -83,7 +96,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
{
|
||||
GivenCapsResponse(_caps.Replace("<limits", "<>"));
|
||||
|
||||
Assert.Throws<XmlException>(() => Subject.GetCapabilities(_settings));
|
||||
Assert.Throws<XmlException>(() => Subject.GetCapabilities(_settings, _definition));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -91,7 +104,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
{
|
||||
GivenCapsResponse(_caps.Replace("5030", "asdf"));
|
||||
|
||||
var result = Subject.GetCapabilities(_settings);
|
||||
var result = Subject.GetCapabilities(_settings, _definition);
|
||||
|
||||
result.Should().NotBeNull();
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
|
||||
_caps = new IndexerCapabilities();
|
||||
Mocker.GetMock<INewznabCapabilitiesProvider>()
|
||||
.Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>()))
|
||||
.Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>(), It.IsAny<IndexerDefinition>()))
|
||||
.Returns(_caps);
|
||||
}
|
||||
|
||||
@@ -42,9 +42,9 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
{
|
||||
var recentFeed = ReadAllText(@"Files/Indexers/Newznab/newznab_nzb_su.xml");
|
||||
|
||||
Mocker.GetMock<IHttpClient>()
|
||||
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
|
||||
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed)));
|
||||
Mocker.GetMock<IIndexerHttpClient>()
|
||||
.Setup(o => o.ExecuteAsync(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 }, Limit = 100, Offset = 0 })).Releases;
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
public class NewznabRequestGeneratorFixture : CoreTest<NewznabRequestGenerator>
|
||||
{
|
||||
private MovieSearchCriteria _movieSearchCriteria;
|
||||
private TvSearchCriteria _tvSearchCriteria;
|
||||
private IndexerCapabilities _capabilities;
|
||||
|
||||
[SetUp]
|
||||
@@ -30,10 +31,17 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
Categories = new int[] { 2000 }
|
||||
};
|
||||
|
||||
_tvSearchCriteria = new TvSearchCriteria
|
||||
{
|
||||
SearchTerm = "Star Wars",
|
||||
Categories = new int[] { 5000 },
|
||||
Season = 0
|
||||
};
|
||||
|
||||
_capabilities = new IndexerCapabilities();
|
||||
|
||||
Mocker.GetMock<INewznabCapabilitiesProvider>()
|
||||
.Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>()))
|
||||
.Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>(), It.IsAny<IndexerDefinition>()))
|
||||
.Returns(_capabilities);
|
||||
}
|
||||
|
||||
@@ -178,5 +186,18 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
pageTier2.Url.Query.Should().NotContain("imdbid=0076759");
|
||||
pageTier2.Url.Query.Should().Contain("q=");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_pad_seasons_for_tv_search()
|
||||
{
|
||||
_capabilities.TvSearchParams = new List<TvSearchParam> { TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep };
|
||||
|
||||
var results = Subject.GetSearchRequests(_tvSearchCriteria);
|
||||
results.Tiers.Should().Be(1);
|
||||
|
||||
var pageTier = results.GetTier(0).First().First();
|
||||
|
||||
pageTier.Url.Query.Should().Contain("season=00");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,13 +36,13 @@ namespace NzbDrone.Core.Test.IndexerTests.PTPTests
|
||||
Json.Serialize(authResponse, authStream);
|
||||
var responseJson = ReadAllText(fileName);
|
||||
|
||||
Mocker.GetMock<IHttpClient>()
|
||||
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.POST)))
|
||||
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), authStream.ToString())));
|
||||
Mocker.GetMock<IIndexerHttpClient>()
|
||||
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.POST), Subject.Definition))
|
||||
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), authStream.ToString())));
|
||||
|
||||
Mocker.GetMock<IHttpClient>()
|
||||
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
|
||||
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader { ContentType = HttpAccept.Json.Value }, new CookieCollection(), responseJson)));
|
||||
Mocker.GetMock<IIndexerHttpClient>()
|
||||
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET), Subject.Definition))
|
||||
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { ContentType = HttpAccept.Json.Value }, new CookieCollection(), responseJson)));
|
||||
|
||||
var torrents = (await Subject.Fetch(new MovieSearchCriteria())).Releases;
|
||||
|
||||
|
||||
@@ -37,9 +37,9 @@ namespace NzbDrone.Core.Test.IndexerTests.RarbgTests
|
||||
{
|
||||
var recentFeed = ReadAllText(@"Files/Indexers/Rarbg/RecentFeed_v2.json");
|
||||
|
||||
Mocker.GetMock<IHttpClient>()
|
||||
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
|
||||
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed)));
|
||||
Mocker.GetMock<IIndexerHttpClient>()
|
||||
.Setup(o => o.ExecuteAsync(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;
|
||||
|
||||
@@ -64,9 +64,9 @@ namespace NzbDrone.Core.Test.IndexerTests.RarbgTests
|
||||
[Test]
|
||||
public async Task should_parse_error_20_as_empty_results()
|
||||
{
|
||||
Mocker.GetMock<IHttpClient>()
|
||||
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
|
||||
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), "{ error_code: 20, error: \"some message\" }")));
|
||||
Mocker.GetMock<IIndexerHttpClient>()
|
||||
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET), Subject.Definition))
|
||||
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), "{ error_code: 20, error: \"some message\" }")));
|
||||
|
||||
var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new int[] { 2000 } })).Releases;
|
||||
|
||||
@@ -76,9 +76,9 @@ namespace NzbDrone.Core.Test.IndexerTests.RarbgTests
|
||||
[Test]
|
||||
public async Task should_warn_on_unknown_error()
|
||||
{
|
||||
Mocker.GetMock<IHttpClient>()
|
||||
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
|
||||
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), "{ error_code: 25, error: \"some message\" }")));
|
||||
Mocker.GetMock<IIndexerHttpClient>()
|
||||
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET), Subject.Definition))
|
||||
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), "{ error_code: 25, error: \"some message\" }")));
|
||||
|
||||
var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new int[] { 2000 } })).Releases;
|
||||
|
||||
|
||||
@@ -10,7 +10,8 @@ namespace NzbDrone.Core.Test.IndexerTests
|
||||
public class TestIndexer : UsenetIndexerBase<TestIndexerSettings>
|
||||
{
|
||||
public override string Name => "Test Indexer";
|
||||
public override string BaseUrl => "http://testindexer.com";
|
||||
public override string[] IndexerUrls => new string[] { "http://testindexer.com" };
|
||||
public override string Description => "";
|
||||
|
||||
public override DownloadProtocol Protocol => DownloadProtocol.Usenet;
|
||||
|
||||
@@ -19,7 +20,7 @@ namespace NzbDrone.Core.Test.IndexerTests
|
||||
public int _supportedPageSize;
|
||||
public override int PageSize => _supportedPageSize;
|
||||
|
||||
public TestIndexer(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, IValidateNzbs nzbValidationService, Logger logger)
|
||||
public TestIndexer(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, IValidateNzbs nzbValidationService, Logger logger)
|
||||
: base(httpClient, eventAggregator, indexerStatusService, configService, nzbValidationService, logger)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Test.IndexerTests
|
||||
{
|
||||
public class TestIndexerSettings : IProviderConfig
|
||||
public class TestIndexerSettings : IIndexerSettings
|
||||
{
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
@@ -14,5 +12,6 @@ namespace NzbDrone.Core.Test.IndexerTests
|
||||
}
|
||||
|
||||
public string BaseUrl { get; set; }
|
||||
public IndexerBaseSettings BaseSettings { get; set; } = new IndexerBaseSettings();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests
|
||||
|
||||
_caps = new IndexerCapabilities();
|
||||
Mocker.GetMock<INewznabCapabilitiesProvider>()
|
||||
.Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>()))
|
||||
.Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>(), It.IsAny<IndexerDefinition>()))
|
||||
.Returns(_caps);
|
||||
}
|
||||
|
||||
@@ -43,9 +43,9 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests
|
||||
{
|
||||
var recentFeed = ReadAllText(@"Files/Indexers/Torznab/torznab_hdaccess_net.xml");
|
||||
|
||||
Mocker.GetMock<IHttpClient>()
|
||||
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
|
||||
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed)));
|
||||
Mocker.GetMock<IIndexerHttpClient>()
|
||||
.Setup(o => o.ExecuteAsync(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())).Releases;
|
||||
|
||||
@@ -72,9 +72,9 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests
|
||||
{
|
||||
var recentFeed = ReadAllText(@"Files/Indexers/Torznab/torznab_tpb.xml");
|
||||
|
||||
Mocker.GetMock<IHttpClient>()
|
||||
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
|
||||
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed)));
|
||||
Mocker.GetMock<IIndexerHttpClient>()
|
||||
.Setup(o => o.ExecuteAsync(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())).Releases;
|
||||
|
||||
@@ -102,9 +102,9 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests
|
||||
{
|
||||
var recentFeed = ReadAllText(@"Files/Indexers/Torznab/torznab_animetosho.xml");
|
||||
|
||||
Mocker.GetMock<IHttpClient>()
|
||||
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
|
||||
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed)));
|
||||
Mocker.GetMock<IIndexerHttpClient>()
|
||||
.Setup(o => o.ExecuteAsync(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())).Releases;
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
<TargetFrameworks>net5.0</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dapper" Version="2.0.78" />
|
||||
<PackageReference Include="Dapper" Version="2.0.90" />
|
||||
<PackageReference Include="NBuilder" Version="6.1.0" />
|
||||
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.113.0-0" />
|
||||
<PackageReference Include="YamlDotNet" Version="11.1.1" />
|
||||
<PackageReference Include="YamlDotNet" Version="11.2.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\NzbDrone.Test.Common\Prowlarr.Test.Common.csproj" />
|
||||
|
||||
@@ -7,5 +7,6 @@ namespace NzbDrone.Core.Applications
|
||||
public int IndexerId { get; set; }
|
||||
public int AppId { get; set; }
|
||||
public int RemoteIndexerId { get; set; }
|
||||
public string RemoteIndexerName { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ namespace NzbDrone.Core.Applications
|
||||
{
|
||||
List<AppIndexerMap> GetMappingsForApp(int appId);
|
||||
AppIndexerMap Insert(AppIndexerMap appIndexerMap);
|
||||
AppIndexerMap Update(AppIndexerMap appIndexerMap);
|
||||
void Delete(int mappingId);
|
||||
void DeleteAllForApp(int appId);
|
||||
}
|
||||
@@ -41,6 +42,11 @@ namespace NzbDrone.Core.Applications
|
||||
return _appIndexerMapRepository.Insert(appIndexerMap);
|
||||
}
|
||||
|
||||
public AppIndexerMap Update(AppIndexerMap appIndexerMap)
|
||||
{
|
||||
return _appIndexerMapRepository.Update(appIndexerMap);
|
||||
}
|
||||
|
||||
public void Handle(ProviderDeletedEvent<IApplication> message)
|
||||
{
|
||||
_appIndexerMapRepository.DeleteAllForApp(message.ProviderId);
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace NzbDrone.Core.Applications
|
||||
protected readonly IAppIndexerMapService _appIndexerMapService;
|
||||
protected readonly Logger _logger;
|
||||
|
||||
protected static readonly Regex AppIndexerRegex = new Regex(@"(?<indexer>\d*)/api",
|
||||
protected static readonly Regex AppIndexerRegex = new Regex(@"\/(?<indexer>\d{1,3})(?:\/(?:api)?\/?)?$",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
public abstract string Name { get; }
|
||||
@@ -58,7 +58,7 @@ namespace NzbDrone.Core.Applications
|
||||
public abstract void AddIndexer(IndexerDefinition indexer);
|
||||
public abstract void UpdateIndexer(IndexerDefinition indexer);
|
||||
public abstract void RemoveIndexer(int indexerId);
|
||||
public abstract Dictionary<int, int> GetIndexerMappings();
|
||||
public abstract List<AppIndexerMap> GetIndexerMappings();
|
||||
|
||||
public virtual object RequestAction(string action, IDictionary<string, string> query)
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Configuration.Events;
|
||||
using NzbDrone.Core.Indexers;
|
||||
@@ -110,18 +111,27 @@ namespace NzbDrone.Core.Applications
|
||||
{
|
||||
var indexerMappings = _appIndexerMapService.GetMappingsForApp(app.Definition.Id);
|
||||
|
||||
//Remote-Local mappings currently stored by Prowlarr
|
||||
var prowlarrMappings = indexerMappings.ToDictionary(i => i.RemoteIndexerId, i => i.IndexerId);
|
||||
|
||||
//Get Dictionary of Remote Indexers point to Prowlarr and what they are mapped to
|
||||
var remoteMappings = app.GetIndexerMappings();
|
||||
var remoteMappings = ExecuteAction(a => a.GetIndexerMappings(), app);
|
||||
|
||||
if (remoteMappings == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
//Add mappings if not already in db, these were setup manually in the app or orphaned by a table wipe
|
||||
foreach (var mapping in remoteMappings)
|
||||
{
|
||||
if (!prowlarrMappings.ContainsKey(mapping.Key))
|
||||
if (!indexerMappings.Any(m => (m.RemoteIndexerId > 0 && m.RemoteIndexerId == mapping.RemoteIndexerId) || (m.RemoteIndexerName.IsNotNullOrWhiteSpace() && m.RemoteIndexerName == mapping.RemoteIndexerName)))
|
||||
{
|
||||
var addMapping = new AppIndexerMap { AppId = app.Definition.Id, RemoteIndexerId = mapping.Key, IndexerId = mapping.Value };
|
||||
var addMapping = new AppIndexerMap
|
||||
{
|
||||
AppId = app.Definition.Id,
|
||||
RemoteIndexerId = mapping.RemoteIndexerId,
|
||||
RemoteIndexerName = mapping.RemoteIndexerName,
|
||||
IndexerId = mapping.IndexerId
|
||||
};
|
||||
|
||||
_appIndexerMapService.Insert(addMapping);
|
||||
indexerMappings.Add(addMapping);
|
||||
}
|
||||
@@ -214,5 +224,64 @@ namespace NzbDrone.Core.Applications
|
||||
_logger.Error(ex, "An error occurred while talking to remote application.");
|
||||
}
|
||||
}
|
||||
|
||||
private TResult ExecuteAction<TResult>(Func<IApplication, TResult> applicationAction, IApplication application)
|
||||
{
|
||||
TResult result;
|
||||
|
||||
try
|
||||
{
|
||||
result = applicationAction(application);
|
||||
_applicationStatusService.RecordSuccess(application.Definition.Id);
|
||||
return result;
|
||||
}
|
||||
catch (WebException webException)
|
||||
{
|
||||
if (webException.Status == WebExceptionStatus.NameResolutionFailure ||
|
||||
webException.Status == WebExceptionStatus.ConnectFailure)
|
||||
{
|
||||
_applicationStatusService.RecordConnectionFailure(application.Definition.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
_applicationStatusService.RecordFailure(application.Definition.Id);
|
||||
}
|
||||
|
||||
if (webException.Message.Contains("502") || webException.Message.Contains("503") ||
|
||||
webException.Message.Contains("timed out"))
|
||||
{
|
||||
_logger.Warn("{0} server is currently unavailable. {1}", this, webException.Message);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Warn("{0} {1}", this, webException.Message);
|
||||
}
|
||||
}
|
||||
catch (TooManyRequestsException ex)
|
||||
{
|
||||
if (ex.RetryAfter != TimeSpan.Zero)
|
||||
{
|
||||
_applicationStatusService.RecordFailure(application.Definition.Id, ex.RetryAfter);
|
||||
}
|
||||
else
|
||||
{
|
||||
_applicationStatusService.RecordFailure(application.Definition.Id, TimeSpan.FromHours(1));
|
||||
}
|
||||
|
||||
_logger.Warn("API Request Limit reached for {0}", this);
|
||||
}
|
||||
catch (HttpException ex)
|
||||
{
|
||||
_applicationStatusService.RecordFailure(application.Definition.Id);
|
||||
_logger.Warn("{0} {1}", this, ex.Message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_applicationStatusService.RecordFailure(application.Definition.Id);
|
||||
_logger.Error(ex, "An error occurred while talking to remote application.");
|
||||
}
|
||||
|
||||
return default(TResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,6 @@ namespace NzbDrone.Core.Applications
|
||||
void AddIndexer(IndexerDefinition indexer);
|
||||
void UpdateIndexer(IndexerDefinition indexer);
|
||||
void RemoveIndexer(int indexerId);
|
||||
Dictionary<int, int> GetIndexerMappings();
|
||||
List<AppIndexerMap> GetIndexerMappings();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using FluentValidation.Results;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NLog;
|
||||
@@ -31,17 +32,35 @@ namespace NzbDrone.Core.Applications.Lidarr
|
||||
{
|
||||
var failures = new List<ValidationFailure>();
|
||||
|
||||
failures.AddIfNotNull(_lidarrV1Proxy.Test(Settings));
|
||||
var testIndexer = new IndexerDefinition
|
||||
{
|
||||
Id = 0,
|
||||
Name = "Test",
|
||||
Protocol = DownloadProtocol.Usenet,
|
||||
Capabilities = new IndexerCapabilities()
|
||||
};
|
||||
|
||||
testIndexer.Capabilities.Categories.AddCategoryMapping(1, NewznabStandardCategory.Audio);
|
||||
|
||||
try
|
||||
{
|
||||
failures.AddIfNotNull(_lidarrV1Proxy.TestConnection(BuildLidarrIndexer(testIndexer, DownloadProtocol.Usenet), Settings));
|
||||
}
|
||||
catch (WebException ex)
|
||||
{
|
||||
_logger.Error(ex, "Unable to send test message");
|
||||
failures.AddIfNotNull(new ValidationFailure("BaseUrl", "Unable to complete application test, cannot connect to Lidarr"));
|
||||
}
|
||||
|
||||
return new ValidationResult(failures);
|
||||
}
|
||||
|
||||
public override Dictionary<int, int> GetIndexerMappings()
|
||||
public override List<AppIndexerMap> GetIndexerMappings()
|
||||
{
|
||||
var indexers = _lidarrV1Proxy.GetIndexers(Settings)
|
||||
.Where(i => i.Implementation == "Newznab" || i.Implementation == "Torznab");
|
||||
|
||||
var mappings = new Dictionary<int, int>();
|
||||
var mappings = new List<AppIndexerMap>();
|
||||
|
||||
foreach (var indexer in indexers)
|
||||
{
|
||||
@@ -52,7 +71,7 @@ namespace NzbDrone.Core.Applications.Lidarr
|
||||
if (match.Groups["indexer"].Success && int.TryParse(match.Groups["indexer"].Value, out var indexerId))
|
||||
{
|
||||
//Add parsed mapping if it's mapped to a Indexer in this Prowlarr instance
|
||||
mappings.Add(indexer.Id, indexerId);
|
||||
mappings.Add(new AppIndexerMap { RemoteIndexerId = indexer.Id, IndexerId = indexerId });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace NzbDrone.Core.Applications.Lidarr
|
||||
|
||||
public IEnumerable<int> SyncCategories { get; set; }
|
||||
|
||||
[FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Lidarr sees it, including http(s):// and port if needed")]
|
||||
[FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Lidarr sees it, including http(s)://, port, and urlbase if needed")]
|
||||
public string ProwlarrUrl { get; set; }
|
||||
|
||||
[FieldDefinition(1, Label = "Lidarr Server", HelpText = "Lidarr server URL, including http(s):// and port if needed")]
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace NzbDrone.Core.Applications.Lidarr
|
||||
List<LidarrIndexer> GetIndexerSchema(LidarrSettings settings);
|
||||
void RemoveIndexer(int indexerId, LidarrSettings settings);
|
||||
LidarrIndexer UpdateIndexer(LidarrIndexer indexer, LidarrSettings settings);
|
||||
ValidationFailure Test(LidarrSettings settings);
|
||||
ValidationFailure TestConnection(LidarrIndexer indexer, LidarrSettings settings);
|
||||
}
|
||||
|
||||
public class LidarrV1Proxy : ILidarrV1Proxy
|
||||
@@ -91,11 +91,15 @@ namespace NzbDrone.Core.Applications.Lidarr
|
||||
return Execute<LidarrIndexer>(request);
|
||||
}
|
||||
|
||||
public ValidationFailure Test(LidarrSettings settings)
|
||||
public ValidationFailure TestConnection(LidarrIndexer indexer, LidarrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, $"/api/v1/indexer/test", HttpMethod.POST);
|
||||
|
||||
request.SetContent(indexer.ToJson());
|
||||
|
||||
try
|
||||
{
|
||||
GetStatus(settings);
|
||||
Execute<LidarrIndexer>(request);
|
||||
}
|
||||
catch (HttpException ex)
|
||||
{
|
||||
@@ -105,8 +109,14 @@ namespace NzbDrone.Core.Applications.Lidarr
|
||||
return new ValidationFailure("ApiKey", "API Key is invalid");
|
||||
}
|
||||
|
||||
if (ex.Response.StatusCode == HttpStatusCode.BadRequest)
|
||||
{
|
||||
_logger.Error(ex, "Prowlarr URL is invalid");
|
||||
return new ValidationFailure("ProwlarrUrl", "Prowlarr url is invalid, Lidarr cannot connect to Prowlarr");
|
||||
}
|
||||
|
||||
_logger.Error(ex, "Unable to send test message");
|
||||
return new ValidationFailure("ApiKey", "Unable to send test message");
|
||||
return new ValidationFailure("BaseUrl", "Unable to complete application test");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
154
src/NzbDrone.Core/Applications/Mylar/Mylar.cs
Normal file
154
src/NzbDrone.Core/Applications/Mylar/Mylar.cs
Normal file
@@ -0,0 +1,154 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using FluentValidation.Results;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Indexers;
|
||||
|
||||
namespace NzbDrone.Core.Applications.Mylar
|
||||
{
|
||||
public class Mylar : ApplicationBase<MylarSettings>
|
||||
{
|
||||
public override string Name => "Mylar";
|
||||
|
||||
private readonly IMylarV3Proxy _mylarV3Proxy;
|
||||
private readonly IConfigFileProvider _configFileProvider;
|
||||
|
||||
public Mylar(IMylarV3Proxy lidarrV1Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, Logger logger)
|
||||
: base(appIndexerMapService, logger)
|
||||
{
|
||||
_mylarV3Proxy = lidarrV1Proxy;
|
||||
_configFileProvider = configFileProvider;
|
||||
}
|
||||
|
||||
public override ValidationResult Test()
|
||||
{
|
||||
var failures = new List<ValidationFailure>();
|
||||
|
||||
try
|
||||
{
|
||||
failures.AddIfNotNull(_mylarV3Proxy.TestConnection(Settings));
|
||||
}
|
||||
catch (WebException ex)
|
||||
{
|
||||
_logger.Error(ex, "Unable to send test message");
|
||||
failures.AddIfNotNull(new ValidationFailure("BaseUrl", "Unable to complete application test, cannot connect to Mylar"));
|
||||
}
|
||||
|
||||
return new ValidationResult(failures);
|
||||
}
|
||||
|
||||
public override List<AppIndexerMap> GetIndexerMappings()
|
||||
{
|
||||
var indexers = _mylarV3Proxy.GetIndexers(Settings);
|
||||
|
||||
var mappings = new List<AppIndexerMap>();
|
||||
|
||||
foreach (var indexer in indexers)
|
||||
{
|
||||
if (indexer.Apikey == _configFileProvider.ApiKey)
|
||||
{
|
||||
var match = AppIndexerRegex.Match(indexer.Host);
|
||||
|
||||
if (match.Groups["indexer"].Success && int.TryParse(match.Groups["indexer"].Value, out var indexerId))
|
||||
{
|
||||
//Add parsed mapping if it's mapped to a Indexer in this Prowlarr instance
|
||||
mappings.Add(new AppIndexerMap { RemoteIndexerName = $"{indexer.Type},{indexer.Name}", IndexerId = indexerId });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mappings;
|
||||
}
|
||||
|
||||
public override void AddIndexer(IndexerDefinition indexer)
|
||||
{
|
||||
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
|
||||
{
|
||||
var lidarrIndexer = BuildMylarIndexer(indexer, indexer.Protocol);
|
||||
|
||||
var remoteIndexer = _mylarV3Proxy.AddIndexer(lidarrIndexer, Settings);
|
||||
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerName = $"{remoteIndexer.Type},{remoteIndexer.Name}" });
|
||||
}
|
||||
}
|
||||
|
||||
public override void RemoveIndexer(int indexerId)
|
||||
{
|
||||
var appMappings = _appIndexerMapService.GetMappingsForApp(Definition.Id);
|
||||
|
||||
var indexerMapping = appMappings.FirstOrDefault(m => m.IndexerId == indexerId);
|
||||
|
||||
if (indexerMapping != null)
|
||||
{
|
||||
//Remove Indexer remotely and then remove the mapping
|
||||
var indexerProps = indexerMapping.RemoteIndexerName.Split(",");
|
||||
_mylarV3Proxy.RemoveIndexer(indexerProps[1], (MylarProviderType)Enum.Parse(typeof(MylarProviderType), indexerProps[0]), Settings);
|
||||
_appIndexerMapService.Delete(indexerMapping.Id);
|
||||
}
|
||||
}
|
||||
|
||||
public override void UpdateIndexer(IndexerDefinition indexer)
|
||||
{
|
||||
_logger.Debug("Updating indexer {0} [{1}]", indexer.Name, indexer.Id);
|
||||
|
||||
var appMappings = _appIndexerMapService.GetMappingsForApp(Definition.Id);
|
||||
var indexerMapping = appMappings.FirstOrDefault(m => m.IndexerId == indexer.Id);
|
||||
var indexerProps = indexerMapping.RemoteIndexerName.Split(",");
|
||||
|
||||
var mylarIndexer = BuildMylarIndexer(indexer, indexer.Protocol, indexerProps[1]);
|
||||
|
||||
//Use the old remote id to find the indexer on Mylar incase the update was from a name change in Prowlarr
|
||||
var remoteIndexer = _mylarV3Proxy.GetIndexer(indexerProps[1], mylarIndexer.Type, Settings);
|
||||
|
||||
if (remoteIndexer != null)
|
||||
{
|
||||
_logger.Debug("Remote indexer found, syncing with current settings");
|
||||
|
||||
if (!mylarIndexer.Equals(remoteIndexer))
|
||||
{
|
||||
_mylarV3Proxy.UpdateIndexer(mylarIndexer, Settings);
|
||||
indexerMapping.RemoteIndexerName = $"{mylarIndexer.Type},{mylarIndexer.Altername}";
|
||||
_appIndexerMapService.Update(indexerMapping);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_appIndexerMapService.Delete(indexerMapping.Id);
|
||||
|
||||
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
|
||||
{
|
||||
_logger.Debug("Remote indexer not found, re-adding {0} to Mylar", indexer.Name);
|
||||
var newRemoteIndexer = _mylarV3Proxy.AddIndexer(mylarIndexer, Settings);
|
||||
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerName = $"{newRemoteIndexer.Type},{newRemoteIndexer.Name}" });
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Debug("Remote indexer not found for {0}, skipping re-add to Mylar due to indexer capabilities", indexer.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private MylarIndexer BuildMylarIndexer(IndexerDefinition indexer, DownloadProtocol protocol, string originalName = null)
|
||||
{
|
||||
var schema = protocol == DownloadProtocol.Usenet ? MylarProviderType.Newznab : MylarProviderType.Torznab;
|
||||
|
||||
var lidarrIndexer = new MylarIndexer
|
||||
{
|
||||
Name = originalName ?? $"{indexer.Name} (Prowlarr)",
|
||||
Altername = $"{indexer.Name} (Prowlarr)",
|
||||
Host = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/api",
|
||||
Apikey = _configFileProvider.ApiKey,
|
||||
Categories = string.Join(",", indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray())),
|
||||
Enabled = indexer.Enable,
|
||||
Type = schema,
|
||||
};
|
||||
|
||||
return lidarrIndexer;
|
||||
}
|
||||
}
|
||||
}
|
||||
14
src/NzbDrone.Core/Applications/Mylar/MylarError.cs
Normal file
14
src/NzbDrone.Core/Applications/Mylar/MylarError.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NzbDrone.Core.Applications.Mylar
|
||||
{
|
||||
public class MylarError
|
||||
{
|
||||
public int Code { get; set; }
|
||||
public string Message { get; set; }
|
||||
}
|
||||
}
|
||||
23
src/NzbDrone.Core/Applications/Mylar/MylarException.cs
Normal file
23
src/NzbDrone.Core/Applications/Mylar/MylarException.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using NzbDrone.Common.Exceptions;
|
||||
|
||||
namespace NzbDrone.Core.Applications.Mylar
|
||||
{
|
||||
public class MylarException : NzbDroneException
|
||||
{
|
||||
public MylarException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public MylarException(string message, params object[] args)
|
||||
: base(message, args)
|
||||
{
|
||||
}
|
||||
|
||||
public MylarException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
22
src/NzbDrone.Core/Applications/Mylar/MylarField.cs
Normal file
22
src/NzbDrone.Core/Applications/Mylar/MylarField.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
namespace NzbDrone.Core.Applications.Mylar
|
||||
{
|
||||
public class MylarField
|
||||
{
|
||||
public int Order { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Label { get; set; }
|
||||
public string Unit { get; set; }
|
||||
public string HelpText { get; set; }
|
||||
public string HelpLink { get; set; }
|
||||
public object Value { get; set; }
|
||||
public string Type { get; set; }
|
||||
public bool Advanced { get; set; }
|
||||
public string Section { get; set; }
|
||||
public string Hidden { get; set; }
|
||||
|
||||
public MylarField Clone()
|
||||
{
|
||||
return (MylarField)MemberwiseClone();
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user