mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2026-04-16 21:35:04 -04:00
Compare commits
290 Commits
v0.1.0.447
...
v0.1.2.106
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51df1be144 | ||
|
|
11fad915d5 | ||
|
|
49793a3af0 | ||
|
|
87d6cbd813 | ||
|
|
5ca8148d3b | ||
|
|
8ebec7c7e1 | ||
|
|
61cff12206 | ||
|
|
22412981bb | ||
|
|
8e43ea4bbc | ||
|
|
f38d6c5b42 | ||
|
|
2054dcc127 | ||
|
|
8d856b2edb | ||
|
|
e07ad14e83 | ||
|
|
d585ab5677 | ||
|
|
6a6697c2c2 | ||
|
|
e859bedef1 | ||
|
|
e688dac040 | ||
|
|
b309582d91 | ||
|
|
998e214171 | ||
|
|
8b0760296a | ||
|
|
44aad1b943 | ||
|
|
9ec8990a21 | ||
|
|
8ac721a30b | ||
|
|
3bbadb516d | ||
|
|
f5f0dd6fae | ||
|
|
0cfb7da411 | ||
|
|
b65f4205fc | ||
|
|
3e243eafdd | ||
|
|
327fd08059 | ||
|
|
827741db17 | ||
|
|
c21e323992 | ||
|
|
e49d03ab7b | ||
|
|
4347e1cf7a | ||
|
|
d7e1043b79 | ||
|
|
d4bdb73b7c | ||
|
|
eeebf3ecf0 | ||
|
|
76d73aa6a9 | ||
|
|
5dfe530cf3 | ||
|
|
8b8b5ba1c8 | ||
|
|
c5caf22375 | ||
|
|
293b32ea0e | ||
|
|
25bb10d62b | ||
|
|
9eba50d9db | ||
|
|
234995cbaf | ||
|
|
918071903b | ||
|
|
dcfa3ad48e | ||
|
|
5dd6cde61a | ||
|
|
d18ddcaa50 | ||
|
|
0cca9525a1 | ||
|
|
3455f3c92a | ||
|
|
af03c17892 | ||
|
|
6b39fa5ce6 | ||
|
|
1ee79f16fc | ||
|
|
e73f2466cc | ||
|
|
2e56b7681e | ||
|
|
87650c83c6 | ||
|
|
34a09af01e | ||
|
|
5a3d429d52 | ||
|
|
40c49bce9b | ||
|
|
063083a1f1 | ||
|
|
dbbc913809 | ||
|
|
f0f2c88c4a | ||
|
|
1bfcb99f31 | ||
|
|
4ea0e6c016 | ||
|
|
1de845c8f5 | ||
|
|
f3a33cf817 | ||
|
|
593a0e9658 | ||
|
|
a854ce6f4e | ||
|
|
043b1a0e46 | ||
|
|
4c7c7e8a62 | ||
|
|
89a4c03dd2 | ||
|
|
baed2960b6 | ||
|
|
e4ef1c3af0 | ||
|
|
b4f8fb733f | ||
|
|
1a6ea21b9f | ||
|
|
16834e0f24 | ||
|
|
658724b315 | ||
|
|
a2c8cec27e | ||
|
|
3c9fbeabaa | ||
|
|
04e84f3a90 | ||
|
|
77a76fe5a1 | ||
|
|
1d20b9d429 | ||
|
|
46e1cce632 | ||
|
|
03f821f484 | ||
|
|
c72222a696 | ||
|
|
f4cee1d5f4 | ||
|
|
ab7bc85368 | ||
|
|
d50e1d7cc0 | ||
|
|
ab1545e834 | ||
|
|
c8cc48229c | ||
|
|
b513fac2f7 | ||
|
|
368e0755a0 | ||
|
|
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
|
||||
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!
|
||||
***Generally speaking, all bug reports must have trace logs provided.***
|
||||
|
||||
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. -->
|
||||
38
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
38
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
name: Feature Request
|
||||
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
|
||||
@@ -1,53 +1,13 @@
|
||||
# How to Contribute #
|
||||
# How to Contribute
|
||||
|
||||
We're always looking for people to help make Prowlarr even better, there are a number of ways to contribute.
|
||||
|
||||
## Documentation ##
|
||||
Setup guides, FAQ, the more information we have on the [wiki](https://wikijs.servarr.com/prowlarr) the better.
|
||||
This file has been moved to the wiki for the latest details please see the [contributing wiki page](https://wiki.servarr.com/prowlarr/contributing).
|
||||
|
||||
## Development ##
|
||||
## Documentation
|
||||
|
||||
### Tools required ###
|
||||
- Visual Studio 2019 or higher (https://www.visualstudio.com/vs/). The community version is free and works (https://www.visualstudio.com/downloads/).
|
||||
- HTML/Javascript editor of choice (VS Code/Sublime Text/Webstorm/Atom/etc)
|
||||
- [Git](https://git-scm.com/downloads)
|
||||
- [NodeJS](https://nodejs.org/en/download/) (Node 12.X.X or higher)
|
||||
- [Yarn](https://yarnpkg.com/)
|
||||
- .NET Core 5.0.
|
||||
Setup guides, [FAQ](https://wiki.servarr.com/prowlarr/faq), the more information we have on the [wiki](https://wiki.servarr.com/prowlarr) the better.
|
||||
|
||||
### Getting started ###
|
||||
## Development
|
||||
|
||||
1. Fork Prowlarr
|
||||
2. Clone the repository into your development machine. [*info*](https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/cloning-a-repository-from-github)
|
||||
3. Install the required Node Packages `yarn install`
|
||||
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
|
||||
|
||||
### 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
|
||||
- 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
|
||||
- Add tests (unit/integration)
|
||||
- Commit with *nix line endings for consistency (We checkout Windows and commit *nix)
|
||||
- One feature/bug fix per pull request to keep things clean and easy to understand
|
||||
- Use 4 spaces instead of tabs, this is the default for VS 2019 and WebStorm (to my knowledge)
|
||||
|
||||
### Contributing Indexers ###
|
||||
- If you're contributing an indexer please phrase your commit as something like: `New: (Indexer) {Indexer Name}`, `New: (Indexer) {Usenet|Torrent} {Indexer Name}`, `New: (Indexer) {Torznab|Newznab} {Indexer Name}`
|
||||
- If you're updating an indexer please phrase your commit as something like: `Fixed: (Indexer) {Indexer Name} {changes}` e.g. `Fixed: (Indexer) Changed BHD to use API`
|
||||
|
||||
### Pull Requesting ###
|
||||
- Only make pull requests to develop, never master, if you make a PR to master we'll comment on it and close it
|
||||
- You're probably going to get some comments or questions from us, they will be to ensure consistency and maintainability
|
||||
- We'll try to respond to pull requests as soon as possible, if its been a day or two, please reach out to us, we may have missed it
|
||||
- Each PR should come from its own [feature branch](http://martinfowler.com/bliki/FeatureBranch.html) not develop in your fork, it should have a meaningful branch name (what is being added/fixed)
|
||||
- new-feature (Good)
|
||||
- fix-bug (Good)
|
||||
- patch (Bad)
|
||||
- develop (Bad)
|
||||
|
||||
If you have any questions about any of this, please let us know.
|
||||
See the [Wiki Page](https://wiki.servarr.com/prowlarr/contributing)
|
||||
|
||||
34
README.md
34
README.md
@@ -2,20 +2,23 @@
|
||||
|
||||
[](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)
|
||||
|
||||
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).
|
||||
Prowlarr is an indexer manager/proxy built on the popular arr .net/reactjs base stack to integrate with your various PVR apps. Prowlarr supports management of both Torrent Trackers and Usenet Indexers. It integrates seamlessly with Lidarr, Mylar3, Radarr, Readarr, and Sonarr offering complete management of your indexers with no per app Indexer setup required (we do it all).
|
||||
|
||||
## Major Features Include:
|
||||
- Usenet support for 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,16 +26,23 @@ 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).
|
||||
<a href="https://github.com/Prowlarr/Prowlarr/graphs/contributors"><img src="https://opencollective.com/Prowlarr/contributors.svg?width=890&button=false" /></a>
|
||||
|
||||
- [Contribute (GitHub)](CONTRIBUTING.md)
|
||||
- [Contribution (Wiki Article)](https://wiki.servarr.com/prowlarr/contributing)
|
||||
- [YML Indexer Defintion (Wiki Article)](https://wiki.servarr.com/prowlarr/cardigann-yml-definition)
|
||||
|
||||
This project exists thanks to all the people who contribute.
|
||||
<a href="https://github.com/Prowlarr/Prowlarr/graphs/contributors"><img src="https://opencollective.com/Prowlarr/contributors.svg?width=890&button=false" /></a>
|
||||
|
||||
## Backers
|
||||
|
||||
|
||||
@@ -7,13 +7,13 @@ variables:
|
||||
outputFolder: './_output'
|
||||
artifactsFolder: './_artifacts'
|
||||
testsFolder: './_tests'
|
||||
majorVersion: '0.1.0'
|
||||
majorVersion: '0.1.2'
|
||||
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:
|
||||
@@ -67,7 +67,7 @@ stages:
|
||||
enableAnalysis: 'true'
|
||||
Mac:
|
||||
osName: 'Mac'
|
||||
imageName: 'macos-10.14'
|
||||
imageName: 'macos-10.15'
|
||||
enableAnalysis: 'false'
|
||||
Windows:
|
||||
osName: 'Windows'
|
||||
@@ -144,7 +144,7 @@ stages:
|
||||
imageName: 'ubuntu-18.04'
|
||||
Mac:
|
||||
osName: 'Mac'
|
||||
imageName: 'macos-10.14'
|
||||
imageName: 'macos-10.15'
|
||||
Windows:
|
||||
osName: 'Windows'
|
||||
imageName: 'windows-2019'
|
||||
@@ -383,7 +383,7 @@ stages:
|
||||
osName: 'Mac'
|
||||
testName: 'MacCore'
|
||||
poolName: 'Azure Pipelines'
|
||||
imageName: 'macos-10.14'
|
||||
imageName: 'macos-10.15'
|
||||
WindowsCore:
|
||||
osName: 'Windows'
|
||||
testName: 'WindowsCore'
|
||||
@@ -510,7 +510,7 @@ stages:
|
||||
MacCore:
|
||||
osName: 'Mac'
|
||||
testName: 'MacCore'
|
||||
imageName: 'macos-10.14'
|
||||
imageName: 'macos-10.15'
|
||||
pattern: 'Prowlarr.*.osx-core-x64.tar.gz'
|
||||
WindowsCore:
|
||||
osName: 'Windows'
|
||||
@@ -686,7 +686,7 @@ stages:
|
||||
failBuild: false
|
||||
Mac:
|
||||
osName: 'Mac'
|
||||
imageName: 'macos-10.14'
|
||||
imageName: 'macos-10.15'
|
||||
pattern: 'Prowlarr.*.osx-core-x64.tar.gz'
|
||||
failBuild: false
|
||||
Windows:
|
||||
@@ -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>
|
||||
|
||||
@@ -16,7 +16,6 @@ import FormInputHelpText from './FormInputHelpText';
|
||||
import IndexerFlagsSelectInputConnector from './IndexerFlagsSelectInputConnector';
|
||||
import InfoInput from './InfoInput';
|
||||
import KeyValueListInput from './KeyValueListInput';
|
||||
import MovieMonitoredSelectInput from './MovieMonitoredSelectInput';
|
||||
import NumberInput from './NumberInput';
|
||||
import OAuthInputConnector from './OAuthInputConnector';
|
||||
import PasswordInput from './PasswordInput';
|
||||
@@ -69,9 +68,6 @@ function getComponent(type) {
|
||||
case inputTypes.PATH:
|
||||
return PathInputConnector;
|
||||
|
||||
case inputTypes.MOVIE_MONITORED_SELECT:
|
||||
return MovieMonitoredSelectInput;
|
||||
|
||||
case inputTypes.INDEXER_FLAGS_SELECT:
|
||||
return IndexerFlagsSelectInputConnector;
|
||||
|
||||
|
||||
@@ -11,8 +11,6 @@ class InfoInput extends Component {
|
||||
value
|
||||
} = this.props;
|
||||
|
||||
console.log(this.props);
|
||||
|
||||
return (
|
||||
<span dangerouslySetInnerHTML={{ __html: value }} />
|
||||
);
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import SelectInput from './SelectInput';
|
||||
|
||||
const monitorTypesOptions = [
|
||||
{ key: 'true', value: 'True' },
|
||||
{ key: 'false', value: 'False' }
|
||||
];
|
||||
|
||||
function MovieMonitoredSelectInput(props) {
|
||||
const values = [...monitorTypesOptions];
|
||||
|
||||
const {
|
||||
includeNoChange,
|
||||
includeMixed
|
||||
} = props;
|
||||
|
||||
if (includeNoChange) {
|
||||
values.unshift({
|
||||
key: 'noChange',
|
||||
value: 'No Change',
|
||||
disabled: true
|
||||
});
|
||||
}
|
||||
|
||||
if (includeMixed) {
|
||||
values.unshift({
|
||||
key: 'mixed',
|
||||
value: '(Mixed)',
|
||||
disabled: true
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<SelectInput
|
||||
{...props}
|
||||
values={values}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
MovieMonitoredSelectInput.propTypes = {
|
||||
includeNoChange: PropTypes.bool.isRequired,
|
||||
includeMixed: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
MovieMonitoredSelectInput.defaultProps = {
|
||||
includeNoChange: false,
|
||||
includeMixed: false
|
||||
};
|
||||
|
||||
export default MovieMonitoredSelectInput;
|
||||
@@ -53,7 +53,8 @@ function getSelectValues(selectOptions) {
|
||||
result.push({
|
||||
key: option.value,
|
||||
value: option.name,
|
||||
hint: option.hint
|
||||
hint: option.hint,
|
||||
parentKey: option.parentValue
|
||||
});
|
||||
|
||||
return result;
|
||||
|
||||
@@ -60,7 +60,7 @@ function createMapStateToProps() {
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onGoToAddNewMovie(query) {
|
||||
dispatch(setSearchDefault({ searchQuery: query, searchIndexerIds: [-1, -2] }));
|
||||
dispatch(setSearchDefault({ searchQuery: query }));
|
||||
dispatch(push(`${window.Prowlarr.urlBase}/search`));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -48,6 +48,10 @@ const links = [
|
||||
title: translate('Settings'),
|
||||
to: '/settings',
|
||||
children: [
|
||||
{
|
||||
title: translate('Indexers'),
|
||||
to: '/settings/indexers'
|
||||
},
|
||||
{
|
||||
title: translate('Apps'),
|
||||
to: '/settings/applications'
|
||||
|
||||
@@ -168,9 +168,9 @@ class SignalRConnector extends Component {
|
||||
this.props.dispatchFetchIndexerStatus();
|
||||
}
|
||||
|
||||
handleMovie = (body) => {
|
||||
handleIndexer = (body) => {
|
||||
const action = body.action;
|
||||
const section = 'movies';
|
||||
const section = 'indexers';
|
||||
|
||||
if (action === 'updated') {
|
||||
this.props.dispatchUpdateItem({ section, ...body.resource });
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -270,6 +270,7 @@ class IndexerIndex extends Component {
|
||||
isSaving,
|
||||
saveError,
|
||||
isDeleting,
|
||||
isTestingAll,
|
||||
deleteError,
|
||||
onScroll,
|
||||
onSortSelect,
|
||||
@@ -310,7 +311,7 @@ class IndexerIndex extends Component {
|
||||
<PageToolbarButton
|
||||
label={'Test All Indexers'}
|
||||
iconName={icons.TEST}
|
||||
spinningName={icons.TEST}
|
||||
isSpinning={isTestingAll}
|
||||
isDisabled={hasNoIndexer}
|
||||
onPress={this.props.onTestAllPress}
|
||||
/>
|
||||
@@ -489,6 +490,7 @@ IndexerIndex.propTypes = {
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
isDeleting: PropTypes.bool.isRequired,
|
||||
isTestingAll: PropTypes.bool.isRequired,
|
||||
deleteError: PropTypes.object,
|
||||
onSortSelect: PropTypes.func.isRequired,
|
||||
onFilterSelect: 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
|
||||
};
|
||||
@@ -57,12 +58,15 @@ class SearchFooter extends Component {
|
||||
|
||||
const {
|
||||
searchIndexerIds,
|
||||
searchCategories,
|
||||
searchQuery
|
||||
searchCategories
|
||||
} = this.state;
|
||||
|
||||
const newState = {};
|
||||
|
||||
if (defaultSearchQuery && defaultSearchQuery !== prevProps.defaultSearchQuery) {
|
||||
newState.searchQuery = defaultSearchQuery;
|
||||
}
|
||||
|
||||
if (searchIndexerIds !== defaultIndexerIds) {
|
||||
newState.searchIndexerIds = defaultIndexerIds;
|
||||
}
|
||||
@@ -71,10 +75,6 @@ class SearchFooter extends Component {
|
||||
newState.searchCategories = defaultCategories;
|
||||
}
|
||||
|
||||
if (searchQuery !== defaultSearchQuery) {
|
||||
newState.searchQuery = defaultSearchQuery;
|
||||
}
|
||||
|
||||
if (prevProps.isFetching && !isFetching && !searchError) {
|
||||
newState.searchingReleases = false;
|
||||
}
|
||||
@@ -91,6 +91,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 +124,7 @@ class SearchFooter extends Component {
|
||||
autoFocus={true}
|
||||
value={searchQuery}
|
||||
isDisabled={isFetching}
|
||||
onChange={onInputChange}
|
||||
onChange={this.onSearchInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -167,7 +171,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,37 +83,23 @@ 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} {indexerIds.length > 1 ? translate('Indexers') : translate('Indexer')}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
!!notificationIds.length &&
|
||||
<div>
|
||||
{notificationIds.length} connection{notificationIds.length > 1 && 's'}
|
||||
{notificationIds.length} {notificationIds.length > 1 ? translate('Notifications') : translate('Notification')}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
!!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} {indexerProxyIds.length > 1 ? translate('IndexerProxies') : translate('IndexerProxy')}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -126,18 +108,16 @@ class Tag extends Component {
|
||||
{
|
||||
!isTagUsed &&
|
||||
<div>
|
||||
No links
|
||||
{translate('NoLinks')}
|
||||
</div>
|
||||
}
|
||||
|
||||
<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,
|
||||
|
||||
@@ -87,6 +87,13 @@ export const defaultState = {
|
||||
isVisible: true,
|
||||
isModifiable: false
|
||||
},
|
||||
{
|
||||
name: 'time',
|
||||
label: translate('Time'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
isModifiable: false
|
||||
},
|
||||
{
|
||||
name: 'logger',
|
||||
label: translate('Component'),
|
||||
@@ -100,13 +107,6 @@ export const defaultState = {
|
||||
isVisible: true,
|
||||
isModifiable: false
|
||||
},
|
||||
{
|
||||
name: 'time',
|
||||
label: translate('Time'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
isModifiable: false
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
columnLabel: translate('Actions'),
|
||||
|
||||
@@ -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'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -58,9 +58,9 @@ class LogsTableRow extends Component {
|
||||
render() {
|
||||
const {
|
||||
level,
|
||||
time,
|
||||
logger,
|
||||
message,
|
||||
time,
|
||||
exception,
|
||||
columns
|
||||
} = this.props;
|
||||
@@ -96,6 +96,15 @@ class LogsTableRow extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'time') {
|
||||
return (
|
||||
<RelativeDateCellConnector
|
||||
key={name}
|
||||
date={time}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'logger') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
@@ -112,15 +121,6 @@ class LogsTableRow extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'time') {
|
||||
return (
|
||||
<RelativeDateCellConnector
|
||||
key={name}
|
||||
date={time}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'actions') {
|
||||
return (
|
||||
<TableRowCell
|
||||
@@ -148,9 +148,9 @@ class LogsTableRow extends Component {
|
||||
|
||||
LogsTableRow.propTypes = {
|
||||
level: PropTypes.string.isRequired,
|
||||
time: PropTypes.string.isRequired,
|
||||
logger: PropTypes.string.isRequired,
|
||||
message: PropTypes.string.isRequired,
|
||||
time: PropTypes.string.isRequired,
|
||||
exception: PropTypes.string,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -7,18 +7,6 @@ function isRelative(ajaxOptions) {
|
||||
return !absUrlRegex.test(ajaxOptions.url);
|
||||
}
|
||||
|
||||
function moveBodyToQuery(ajaxOptions) {
|
||||
if (ajaxOptions.data && ajaxOptions.type === 'DELETE') {
|
||||
if (ajaxOptions.url.contains('?')) {
|
||||
ajaxOptions.url += '&';
|
||||
} else {
|
||||
ajaxOptions.url += '?';
|
||||
}
|
||||
ajaxOptions.url += $.param(ajaxOptions.data);
|
||||
delete ajaxOptions.data;
|
||||
}
|
||||
}
|
||||
|
||||
function addRootUrl(ajaxOptions) {
|
||||
ajaxOptions.url = apiRoot + ajaxOptions.url;
|
||||
}
|
||||
@@ -32,7 +20,7 @@ function addContentType(ajaxOptions) {
|
||||
if (
|
||||
ajaxOptions.contentType == null &&
|
||||
ajaxOptions.dataType === 'json' &&
|
||||
(ajaxOptions.method === 'PUT' || ajaxOptions.method === 'POST')) {
|
||||
(ajaxOptions.method === 'PUT' || ajaxOptions.method === 'POST' || ajaxOptions.method === 'DELETE')) {
|
||||
ajaxOptions.contentType = 'application/json';
|
||||
}
|
||||
}
|
||||
@@ -52,7 +40,6 @@ export default function createAjaxRequest(originalAjaxOptions) {
|
||||
const ajaxOptions = { dataType: 'json', ...originalAjaxOptions };
|
||||
|
||||
if (isRelative(ajaxOptions)) {
|
||||
moveBodyToQuery(ajaxOptions);
|
||||
addRootUrl(ajaxOptions);
|
||||
addApiKey(ajaxOptions);
|
||||
addContentType(ajaxOptions);
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<packageSources>
|
||||
<clear />
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||
<add key="FluentMigrator" value="https://pkgs.dev.azure.com/fluentmigrator/fluentmigrator/_packaging/fluentmigrator/nuget/v3/index.json" />
|
||||
<add key="dotnet-bsd-crossbuild" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/dotnet-bsd-crossbuild/nuget/v3/index.json" />
|
||||
<add key="Mono.Posix.NETStandard" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/Mono.Posix.NETStandard/nuget/v3/index.json" />
|
||||
<add key="SQLite" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/SQLite/nuget/v3/index.json" />
|
||||
|
||||
24
src/NzbDrone.Common.Test/Http/UserAgentParserFixture.cs
Normal file
24
src/NzbDrone.Common.Test/Http/UserAgentParserFixture.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Test.Common;
|
||||
|
||||
namespace NzbDrone.Common.Test.Http
|
||||
{
|
||||
[TestFixture]
|
||||
public class UserAgentParserFixture : TestBase
|
||||
{
|
||||
// Ref *Arr `_userAgent = $"{BuildInfo.AppName}/{BuildInfo.Version} ({osName} {osVersion})";`
|
||||
// Ref Mylar `Mylar3/' +str(hash) +'(' +vers +') +http://www.github.com/mylar3/mylar3/`
|
||||
[TestCase("Mylar3/ 3ee23rh23irqfq (13123123) http://www.github.com/mylar3/mylar3/", "Mylar3")]
|
||||
[TestCase("Lidarr/1.0.0.2300 (ubuntu 20.04)", "Lidarr")]
|
||||
[TestCase("Radarr/1.0.0.2300 (ubuntu 20.04)", "Radarr")]
|
||||
[TestCase("Readarr/1.0.0.2300 (ubuntu 20.04)", "Readarr")]
|
||||
[TestCase("Sonarr/3.0.6.9999 (ubuntu 20.04)", "Sonarr")]
|
||||
[TestCase("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36", "Other")]
|
||||
public void should_parse_user_agent(string userAgent, string parsedAgent)
|
||||
{
|
||||
UserAgentParser.ParseSource(userAgent).Should().Be(parsedAgent);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,14 @@ 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")]
|
||||
[TestCase(@"Req: [POST] https://www3.yggtorrent.nz/user/login: id=mySecret&pass=mySecret&ci_csrf_token=2b51db35e1912ffc138825a12b9933d2")]
|
||||
|
||||
// avistaz response
|
||||
[TestCase(@"""download"":""https:\/\/avistaz.to\/rss\/download\/2b51db35e1910123321025a12b9933d2\/tb51db35e1910123321025a12b9933d2.torrent"",")]
|
||||
[TestCase(@",""info_hash"":""2b51db35e1910123321025a12b9933d2"",")]
|
||||
|
||||
// NzbGet
|
||||
[TestCase(@"{ ""Name"" : ""ControlUsername"", ""Value"" : ""mySecret"" }, { ""Name"" : ""ControlPassword"", ""Value"" : ""mySecret"" }, ")]
|
||||
@@ -90,5 +98,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,5 +160,16 @@ namespace NzbDrone.Common.Extensions
|
||||
{
|
||||
return new HashSet<T>(source, comparer);
|
||||
}
|
||||
|
||||
public static T FirstIfSingleOrDefault<T>(this IEnumerable<T> source, T replace = default)
|
||||
{
|
||||
if (source is ICollection<T> collection)
|
||||
{
|
||||
return collection.Count == 1 ? collection.First() : replace;
|
||||
}
|
||||
|
||||
var test = source.Take(2).ToList();
|
||||
return test.Count == 1 ? test[0] : replace;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace NzbDrone.Common.Http
|
||||
{
|
||||
public static class UserAgentParser
|
||||
{
|
||||
private static readonly Regex AppSourceRegex = new Regex(@"(?<agent>.*)\/.*(\(.*\))?",
|
||||
private static readonly Regex AppSourceRegex = new Regex(@"(?<agent>[a-z0-9]*)\/.*(?:\(.*\))?",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
public static string SimplifyUserAgent(string userAgent)
|
||||
|
||||
@@ -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|pass(?:key|wd)?|auth|authkey|user|u?id|api|[a-z_]*apikey|account|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),
|
||||
@@ -43,7 +45,11 @@ namespace NzbDrone.Common.Instrumentation
|
||||
new Regex(@"(?<=\?|&)(authkey|torrent_pass)=(?<secret>[^&=]+?)(?=""|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
// Plex
|
||||
new Regex(@"(?<=\?|&)(X-Plex-Client-Identifier|X-Plex-Token)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase)
|
||||
new Regex(@"(?<=\?|&)(X-Plex-Client-Identifier|X-Plex-Token)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
//avistaz
|
||||
new Regex(@"avistaz\.[a-z]{2,3}\\\/rss\\\/download\\\/(?<secret>[^&=]+?)\\\/(?<secret>[^&=]+?)\.torrent", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@",""info_hash"":""(?<secret>[^&=]+?)"",", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
};
|
||||
|
||||
private static readonly Regex CleanseRemoteIPRegex = new Regex(@"(?:Auth-\w+(?<!Failure|Unauthorized) ip|from) (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})", RegexOptions.Compiled);
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -19,6 +19,10 @@ namespace NzbDrone.Core.Test.HealthCheck
|
||||
|
||||
Mocker.SetConstant<IEnumerable<IProvideHealthCheck>>(new[] { _healthCheck });
|
||||
Mocker.SetConstant<ICacheManager>(Mocker.Resolve<CacheManager>());
|
||||
|
||||
Mocker.GetMock<IServerSideNotificationService>()
|
||||
.Setup(v => v.GetServerChecks())
|
||||
.Returns(new List<Core.HealthCheck.HealthCheck>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -2,6 +2,8 @@ using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Housekeeping.Housekeepers;
|
||||
using NzbDrone.Core.IndexerProxies;
|
||||
using NzbDrone.Core.IndexerProxies.Http;
|
||||
using NzbDrone.Core.Tags;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
@@ -22,5 +24,30 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
|
||||
Subject.Clean();
|
||||
AllStoredModels.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_delete_used_tags()
|
||||
{
|
||||
var tags = Builder<Tag>
|
||||
.CreateListOfSize(2)
|
||||
.All()
|
||||
.With(x => x.Id = 0)
|
||||
.BuildList();
|
||||
|
||||
Db.InsertMany(tags);
|
||||
|
||||
var settings = Builder<HttpSettings>.CreateNew().Build();
|
||||
|
||||
var restrictions = Builder<IndexerProxyDefinition>.CreateListOfSize(2)
|
||||
.All()
|
||||
.With(x => x.Id = 0)
|
||||
.With(x => x.Settings = settings)
|
||||
.With(v => v.Tags.Add(tags[0].Id))
|
||||
.BuildList();
|
||||
Db.InsertMany(restrictions);
|
||||
|
||||
Subject.Clean();
|
||||
AllStoredModels.Should().HaveCount(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET), Subject.Definition))
|
||||
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed)));
|
||||
|
||||
var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new int[] { 2000 } })).Releases;
|
||||
|
||||
|
||||
@@ -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.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET), Subject.Definition))
|
||||
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed)));
|
||||
|
||||
var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new int[] { 2000 } })).Releases;
|
||||
|
||||
|
||||
@@ -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.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.POST), Subject.Definition))
|
||||
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), responseJson)));
|
||||
|
||||
var torrents = (await Subject.Fetch(_movieSearchCriteria)).Releases;
|
||||
|
||||
@@ -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.ExecuteProxiedAsync(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.ExecuteProxied(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.ExecuteProxied(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.ExecuteProxied(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.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET), Subject.Definition))
|
||||
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed)));
|
||||
|
||||
var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new int[] { 2000 }, 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.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.POST), Subject.Definition))
|
||||
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), 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.ExecuteProxiedAsync(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.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET), Subject.Definition))
|
||||
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed)));
|
||||
|
||||
var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new int[] { 2000 } })).Releases;
|
||||
|
||||
@@ -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.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET), Subject.Definition))
|
||||
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), "{ 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.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET), Subject.Definition))
|
||||
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), "{ 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)
|
||||
{
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user