mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2026-04-17 21:44:48 -04:00
Compare commits
317 Commits
v0.1.0.447
...
v0.1.3.111
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8a42a7998 | ||
|
|
a3fd37b27e | ||
|
|
29dcbadfca | ||
|
|
d8c9225d09 | ||
|
|
17008ace5c | ||
|
|
470b751f44 | ||
|
|
a31971472d | ||
|
|
4c4614cfab | ||
|
|
f5cb81951a | ||
|
|
c35e257a82 | ||
|
|
cb7cbb74e0 | ||
|
|
c263c6f365 | ||
|
|
701c7d349a | ||
|
|
4a8131e00f | ||
|
|
39fd9bea89 | ||
|
|
89a1b0f534 | ||
|
|
1aa9149866 | ||
|
|
eccd72304c | ||
|
|
0fb19160f4 | ||
|
|
3e4ac89a4c | ||
|
|
9331d8e013 | ||
|
|
e94c8839f6 | ||
|
|
1abd14ee86 | ||
|
|
6200c9e496 | ||
|
|
2c0c6aa158 | ||
|
|
93deb56e8e | ||
|
|
971c755738 | ||
|
|
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 |
@@ -260,7 +260,7 @@ dotnet_diagnostic.CA5392.severity = suggestion
|
||||
dotnet_diagnostic.CA5394.severity = suggestion
|
||||
dotnet_diagnostic.CA5397.severity = suggestion
|
||||
|
||||
|
||||
dotnet_diagnostic.SYSLIB0014.severity = none
|
||||
|
||||
[*.{js,html,js,hbs,less,css}]
|
||||
charset = utf-8
|
||||
|
||||
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.3'
|
||||
minorVersion: $[counter('minorVersion', 1)]
|
||||
prowlarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
|
||||
sentryOrg: 'servarr'
|
||||
sentryUrl: 'https://sentry.servarr.com'
|
||||
dotnetVersion: '5.0.203'
|
||||
dotnetVersion: '6.0.100'
|
||||
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'
|
||||
@@ -111,23 +111,23 @@ stages:
|
||||
artifact: '$(osName)Backend'
|
||||
displayName: Publish Backend
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
- publish: '$(testsFolder)/net5.0/win-x64/publish'
|
||||
- publish: '$(testsFolder)/net6.0/win-x64/publish'
|
||||
artifact: WindowsCoreTests
|
||||
displayName: Publish Windows Test Package
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
- publish: '$(testsFolder)/net5.0/linux-x64/publish'
|
||||
- publish: '$(testsFolder)/net6.0/linux-x64/publish'
|
||||
artifact: LinuxCoreTests
|
||||
displayName: Publish Linux Test Package
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
- publish: '$(testsFolder)/net5.0/linux-musl-x64/publish'
|
||||
- publish: '$(testsFolder)/net6.0/linux-musl-x64/publish'
|
||||
artifact: LinuxMuslCoreTests
|
||||
displayName: Publish Linux Musl Test Package
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
- publish: '$(testsFolder)/net5.0/freebsd-x64/publish'
|
||||
- publish: '$(testsFolder)/net6.0/freebsd-x64/publish'
|
||||
artifact: FreebsdCoreTests
|
||||
displayName: Publish FreeBSD Test Package
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
- publish: '$(testsFolder)/net5.0/osx-x64/publish'
|
||||
- publish: '$(testsFolder)/net6.0/osx-x64/publish'
|
||||
artifact: MacCoreTests
|
||||
displayName: Publish MacOS Test Package
|
||||
condition: and(succeeded(), eq(variables['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'
|
||||
@@ -203,12 +203,12 @@ stages:
|
||||
- bash: ./build.sh --packages
|
||||
displayName: Create Packages
|
||||
- bash: |
|
||||
distribution/windows/setup/inno/ISCC.exe distribution/windows/setup/prowlarr.iss //DFramework=net5.0 //DRuntime=win-x86
|
||||
cp distribution/windows/setup/output/Prowlarr.*windows.net5.0.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Prowlarr.${BUILDNAME}.windows-core-x86-installer.exe
|
||||
distribution/windows/setup/inno/ISCC.exe distribution/windows/setup/prowlarr.iss //DFramework=net6.0 //DRuntime=win-x86
|
||||
cp distribution/windows/setup/output/Prowlarr.*windows.net6.0.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Prowlarr.${BUILDNAME}.windows-core-x86-installer.exe
|
||||
displayName: Create x86 .NET Core Windows installer
|
||||
- bash: |
|
||||
distribution/windows/setup/inno/ISCC.exe distribution/windows/setup/prowlarr.iss //DFramework=net5.0 //DRuntime=win-x64
|
||||
cp distribution/windows/setup/output/Prowlarr.*windows.net5.0.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Prowlarr.${BUILDNAME}.windows-core-x64-installer.exe
|
||||
distribution/windows/setup/inno/ISCC.exe distribution/windows/setup/prowlarr.iss //DFramework=net6.0 //DRuntime=win-x64
|
||||
cp distribution/windows/setup/output/Prowlarr.*windows.net6.0.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Prowlarr.${BUILDNAME}.windows-core-x64-installer.exe
|
||||
displayName: Create x64 .NET Core Windows installer
|
||||
- publish: $(Build.ArtifactStagingDirectory)
|
||||
artifact: 'WindowsInstaller'
|
||||
@@ -250,29 +250,44 @@ stages:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).windows-core-x64.zip'
|
||||
archiveType: 'zip'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/win-x64/net5.0
|
||||
rootFolderOrFile: $(artifactsFolder)/win-x64/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create Windows x86 Core zip
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).windows-core-x86.zip'
|
||||
archiveType: 'zip'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/win-x86/net5.0
|
||||
rootFolderOrFile: $(artifactsFolder)/win-x86/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create MacOS Core app
|
||||
displayName: Create MacOS x64 Core app
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).osx-app-core-x64.zip'
|
||||
archiveType: 'zip'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/macos-app/net5.0
|
||||
rootFolderOrFile: $(artifactsFolder)/osx-x64-app/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create MacOS Core tar
|
||||
displayName: Create MacOS x64 Core tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).osx-core-x64.tar.gz'
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/macos/net5.0
|
||||
rootFolderOrFile: $(artifactsFolder)/osx-x64/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create MacOS arm64 Core app
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).osx-app-core-arm64.zip'
|
||||
archiveType: 'zip'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/osx-arm64-app/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create MacOS arm64 Core tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).osx-core-arm64.tar.gz'
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/osx-arm64/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create Linux Core tar
|
||||
inputs:
|
||||
@@ -280,7 +295,7 @@ stages:
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-x64/net5.0
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-x64/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create Linux Musl Core tar
|
||||
inputs:
|
||||
@@ -288,7 +303,7 @@ stages:
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/net5.0
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create ARM32 Linux Core tar
|
||||
inputs:
|
||||
@@ -296,7 +311,15 @@ stages:
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-arm/net5.0
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-arm/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create ARM32 Linux Musl Core tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-musl-core-arm.tar.gz'
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create ARM64 Linux Core tar
|
||||
inputs:
|
||||
@@ -304,7 +327,7 @@ stages:
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-arm64/net5.0
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-arm64/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create ARM64 Linux Musl Core tar
|
||||
inputs:
|
||||
@@ -312,7 +335,7 @@ stages:
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm64/net5.0
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm64/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create FreeBSD Core Core tar
|
||||
inputs:
|
||||
@@ -320,7 +343,7 @@ stages:
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/freebsd-x64/net5.0
|
||||
rootFolderOrFile: $(artifactsFolder)/freebsd-x64/net6.0
|
||||
- publish: $(Build.ArtifactStagingDirectory)
|
||||
artifact: 'Packages'
|
||||
displayName: Publish Packages
|
||||
@@ -383,7 +406,7 @@ stages:
|
||||
osName: 'Mac'
|
||||
testName: 'MacCore'
|
||||
poolName: 'Azure Pipelines'
|
||||
imageName: 'macos-10.14'
|
||||
imageName: 'macos-10.15'
|
||||
WindowsCore:
|
||||
osName: 'Windows'
|
||||
testName: 'WindowsCore'
|
||||
@@ -510,7 +533,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 +709,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:
|
||||
@@ -838,8 +861,8 @@ stages:
|
||||
sonar.cs.opencover.reportsPaths=$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml
|
||||
sonar.cs.nunit.reportsPaths=$(Build.SourcesDirectory)/TestResult.xml
|
||||
- bash: |
|
||||
./build.sh --backend -f net5.0 -r win-x64
|
||||
TEST_DIR=_tests/net5.0/win-x64/publish/ ./test.sh Windows Unit Coverage
|
||||
./build.sh --backend -f net6.0 -r win-x64
|
||||
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
|
||||
displayName: Coverage Unit Tests
|
||||
- task: SonarCloudAnalyze@1
|
||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||
@@ -879,7 +902,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)
|
||||
|
||||
54
build.sh
54
build.sh
@@ -130,7 +130,7 @@ PackageLinux()
|
||||
|
||||
echo "Adding Prowlarr.Mono to UpdatePackage"
|
||||
cp $folder/Prowlarr.Mono.* $folder/Prowlarr.Update
|
||||
if [ "$framework" = "net5.0" ]; then
|
||||
if [ "$framework" = "net6.0" ]; then
|
||||
cp $folder/Mono.Posix.NETStandard.* $folder/Prowlarr.Update
|
||||
cp $folder/libMonoPosixHelper.* $folder/Prowlarr.Update
|
||||
fi
|
||||
@@ -141,12 +141,13 @@ PackageLinux()
|
||||
PackageMacOS()
|
||||
{
|
||||
local framework="$1"
|
||||
local runtime="$2"
|
||||
|
||||
ProgressStart "Creating MacOS Package for $framework"
|
||||
ProgressStart "Creating MacOS Package for $framework $runtime"
|
||||
|
||||
local folder=$artifactsFolder/macos/$framework/Prowlarr
|
||||
local folder=$artifactsFolder/$runtime/$framework/Prowlarr
|
||||
|
||||
PackageFiles "$folder" "$framework" "osx-x64"
|
||||
PackageFiles "$folder" "$framework" "$runtime"
|
||||
|
||||
echo "Removing Service helpers"
|
||||
rm -f $folder/ServiceUninstall.*
|
||||
@@ -157,7 +158,7 @@ PackageMacOS()
|
||||
|
||||
echo "Adding Prowlarr.Mono to UpdatePackage"
|
||||
cp $folder/Prowlarr.Mono.* $folder/Prowlarr.Update
|
||||
if [ "$framework" = "net5.0" ]; then
|
||||
if [ "$framework" = "net6.0" ]; then
|
||||
cp $folder/Mono.Posix.NETStandard.* $folder/Prowlarr.Update
|
||||
cp $folder/libMonoPosixHelper.* $folder/Prowlarr.Update
|
||||
fi
|
||||
@@ -168,10 +169,11 @@ PackageMacOS()
|
||||
PackageMacOSApp()
|
||||
{
|
||||
local framework="$1"
|
||||
local runtime="$2"
|
||||
|
||||
ProgressStart "Creating macOS App Package for $framework"
|
||||
ProgressStart "Creating macOS App Package for $framework $runtime"
|
||||
|
||||
local folder=$artifactsFolder/macos-app/$framework
|
||||
local folder="$artifactsFolder/$runtime-app/$framework"
|
||||
|
||||
rm -rf $folder
|
||||
mkdir -p $folder
|
||||
@@ -179,7 +181,7 @@ PackageMacOSApp()
|
||||
mkdir -p $folder/Prowlarr.app/Contents/MacOS
|
||||
|
||||
echo "Copying Binaries"
|
||||
cp -r $artifactsFolder/macos/$framework/Prowlarr/* $folder/Prowlarr.app/Contents/MacOS
|
||||
cp -r $artifactsFolder/$runtime/$framework/Prowlarr/* $folder/Prowlarr.app/Contents/MacOS
|
||||
|
||||
echo "Removing Update Folder"
|
||||
rm -r $folder/Prowlarr.app/Contents/MacOS/Prowlarr.Update
|
||||
@@ -226,8 +228,8 @@ Package()
|
||||
PackageWindows "$framework" "$runtime"
|
||||
;;
|
||||
osx)
|
||||
PackageMacOS "$framework"
|
||||
PackageMacOSApp "$framework"
|
||||
PackageMacOS "$framework" "$runtime"
|
||||
PackageMacOSApp "$framework" "$runtime"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
@@ -327,14 +329,14 @@ then
|
||||
Build
|
||||
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
|
||||
then
|
||||
PackageTests "net5.0" "win-x64"
|
||||
PackageTests "net5.0" "win-x86"
|
||||
PackageTests "net5.0" "linux-x64"
|
||||
PackageTests "net5.0" "linux-musl-x64"
|
||||
PackageTests "net5.0" "osx-x64"
|
||||
PackageTests "net6.0" "win-x64"
|
||||
PackageTests "net6.0" "win-x86"
|
||||
PackageTests "net6.0" "linux-x64"
|
||||
PackageTests "net6.0" "linux-musl-x64"
|
||||
PackageTests "net6.0" "osx-x64"
|
||||
if [ "$ENABLE_BSD" = "YES" ];
|
||||
then
|
||||
PackageTests "net5.0" "freebsd-x64"
|
||||
PackageTests "net6.0" "freebsd-x64"
|
||||
fi
|
||||
else
|
||||
PackageTests "$FRAMEWORK" "$RID"
|
||||
@@ -363,17 +365,19 @@ then
|
||||
|
||||
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
|
||||
then
|
||||
Package "net5.0" "win-x64"
|
||||
Package "net5.0" "win-x86"
|
||||
Package "net5.0" "linux-x64"
|
||||
Package "net5.0" "linux-musl-x64"
|
||||
Package "net5.0" "linux-arm64"
|
||||
Package "net5.0" "linux-musl-arm64"
|
||||
Package "net5.0" "linux-arm"
|
||||
Package "net5.0" "osx-x64"
|
||||
Package "net6.0" "win-x64"
|
||||
Package "net6.0" "win-x86"
|
||||
Package "net6.0" "linux-x64"
|
||||
Package "net6.0" "linux-musl-x64"
|
||||
Package "net6.0" "linux-arm64"
|
||||
Package "net6.0" "linux-musl-arm64"
|
||||
Package "net6.0" "linux-arm"
|
||||
Package "net6.0" "linux-musl-arm"
|
||||
Package "net6.0" "osx-x64"
|
||||
Package "net6.0" "osx-arm64"
|
||||
if [ "$ENABLE_BSD" = "YES" ];
|
||||
then
|
||||
Package "net5.0" "freebsd-x64"
|
||||
Package "net6.0" "freebsd-x64"
|
||||
fi
|
||||
else
|
||||
Package "$FRAMEWORK" "$RID"
|
||||
|
||||
@@ -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": "6.0.0",
|
||||
"@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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
<!-- Common to all Prowlarr Projects -->
|
||||
<PropertyGroup>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
|
||||
<ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles>
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
|
||||
<RuntimeIdentifiers>win-x64;win-x86;osx-x64;linux-x64;linux-musl-x64;linux-arm;linux-arm64;linux-musl-arm64</RuntimeIdentifiers>
|
||||
<RuntimeIdentifiers>win-x64;win-x86;osx-x64;osx-arm64;linux-x64;linux-musl-x64;linux-musl-arm;linux-arm;linux-arm64;linux-musl-arm64</RuntimeIdentifiers>
|
||||
|
||||
<ProwlarrRootDir>$(MSBuildThisFileDirectory)..\</ProwlarrRootDir>
|
||||
|
||||
@@ -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="17.0.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
|
||||
<PackageReference Include="NunitXml.TestLogger" Version="3.0.97" />
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net5.0</TargetFrameworks>
|
||||
<TargetFrameworks>net6.0</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Selenium.Support" Version="3.141.0" />
|
||||
|
||||
@@ -728,7 +728,7 @@ namespace NzbDrone.Common.Test.Http
|
||||
{
|
||||
try
|
||||
{
|
||||
string url = $"https://{_httpBinHost}/response-headers?Set-Cookie={Uri.EscapeUriString(malformedCookie)}";
|
||||
string url = $"https://{_httpBinHost}/response-headers?Set-Cookie={Uri.EscapeDataString(malformedCookie)}";
|
||||
|
||||
var requestSet = new HttpRequest(url);
|
||||
requestSet.AllowAutoRedirect = false;
|
||||
|
||||
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,23 @@ 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")]
|
||||
|
||||
//Indexer Responses
|
||||
|
||||
// avistaz response
|
||||
[TestCase(@"""download"":""https:\/\/avistaz.to\/rss\/download\/2b51db35e1910123321025a12b9933d2\/tb51db35e1910123321025a12b9933d2.torrent"",")]
|
||||
[TestCase(@",""info_hash"":""2b51db35e1910123321025a12b9933d2"",")]
|
||||
|
||||
// danish bytes response
|
||||
[TestCase(@",""rsskey"":""2b51db35e1910123321025a12b9933d2"",")]
|
||||
[TestCase(@",""passkey"":""2b51db35e1910123321025a12b9933d2"",")]
|
||||
|
||||
// nzbgeek & usenet response
|
||||
[TestCase(@"<guid isPermaLink=""true"">https://api.nzbgeek.info/api?t=details&id=2b51db35e1910123321025a12b9933d2&apikey=2b51db35e1910123321025a12b9933d2</guid>")]
|
||||
|
||||
// NzbGet
|
||||
[TestCase(@"{ ""Name"" : ""ControlUsername"", ""Value"" : ""mySecret"" }, { ""Name"" : ""ControlPassword"", ""Value"" : ""mySecret"" }, ")]
|
||||
@@ -58,6 +75,7 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
||||
|
||||
// Notifiarr
|
||||
[TestCase("https://notifiarr.com/notifier.php: api=1234530f-422f-4aac-b6b3-01233210aaaa&radarr_health_issue_message=Download")]
|
||||
[TestCase("/readarr/signalr/messages/negotiate?access_token=1234530f422f4aacb6b301233210aaaa&negotiateVersion=1")]
|
||||
|
||||
// RSS
|
||||
[TestCase(@"<atom:link href = ""https://api.nzb.su/api?t=search&extended=1&cat=3030&apikey=mySecret&q=Diggers"" rel=""self"" type=""application/rss+xml"" />")]
|
||||
@@ -90,5 +108,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net5.0</TargetFrameworks>
|
||||
<TargetFrameworks>net6.0</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\NzbDrone.Host\Prowlarr.Host.csproj" />
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace NzbDrone.Common.EnvironmentInfo
|
||||
serviceProvider.ServiceExist(ServiceProvider.SERVICE_NAME) &&
|
||||
serviceProvider.GetStatus(ServiceProvider.SERVICE_NAME) == ServiceControllerStatus.StartPending;
|
||||
|
||||
// net5.0 will return Radarr.dll for entry assembly, we need the actual
|
||||
// net6.0 will return Radarr.dll for entry assembly, we need the actual
|
||||
// executable name (Radarr on linux). On mono this will return the location of
|
||||
// the mono executable itself, which is not what we want.
|
||||
var entry = Process.GetCurrentProcess().MainModule;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,13 +6,6 @@ namespace NzbDrone.Common.Extensions
|
||||
{
|
||||
public static class EnumerableExtensions
|
||||
{
|
||||
public static IEnumerable<TSource> DistinctBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
|
||||
{
|
||||
var knownKeys = new HashSet<TKey>();
|
||||
|
||||
return source.Where(element => knownKeys.Add(keySelector(element)));
|
||||
}
|
||||
|
||||
public static IEnumerable<TFirst> IntersectBy<TFirst, TSecond, TKey>(this IEnumerable<TFirst> first,
|
||||
Func<TFirst, TKey> firstKeySelector,
|
||||
IEnumerable<TSecond> second,
|
||||
@@ -160,5 +153,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;
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using com.LandonKey.SocksWebProxy;
|
||||
using com.LandonKey.SocksWebProxy.Proxy;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common.Extensions;
|
||||
|
||||
@@ -33,54 +29,37 @@ namespace NzbDrone.Common.Http.Proxy
|
||||
}
|
||||
|
||||
private IWebProxy CreateWebProxy(HttpProxySettings proxySettings)
|
||||
{
|
||||
var uri = GetProxyUri(proxySettings);
|
||||
|
||||
if (uri == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (proxySettings.Username.IsNotNullOrWhiteSpace() && proxySettings.Password.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
return new WebProxy(uri, proxySettings.BypassLocalAddress, proxySettings.BypassListAsArray, new NetworkCredential(proxySettings.Username, proxySettings.Password));
|
||||
}
|
||||
else
|
||||
{
|
||||
return new WebProxy(uri, proxySettings.BypassLocalAddress, proxySettings.BypassListAsArray);
|
||||
}
|
||||
}
|
||||
|
||||
private Uri GetProxyUri(HttpProxySettings proxySettings)
|
||||
{
|
||||
switch (proxySettings.Type)
|
||||
{
|
||||
case ProxyType.Http:
|
||||
if (proxySettings.Username.IsNotNullOrWhiteSpace() && proxySettings.Password.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
return new WebProxy(proxySettings.Host + ":" + proxySettings.Port, proxySettings.BypassLocalAddress, proxySettings.BypassListAsArray, new NetworkCredential(proxySettings.Username, proxySettings.Password));
|
||||
}
|
||||
else
|
||||
{
|
||||
return new WebProxy(proxySettings.Host + ":" + proxySettings.Port, proxySettings.BypassLocalAddress, proxySettings.BypassListAsArray);
|
||||
}
|
||||
|
||||
return new Uri("http://" + proxySettings.Host + ":" + proxySettings.Port);
|
||||
case ProxyType.Socks4:
|
||||
return new SocksWebProxy(new ProxyConfig(IPAddress.Loopback, GetNextFreePort(), GetProxyIpAddress(proxySettings.Host), proxySettings.Port, ProxyConfig.SocksVersion.Four, proxySettings.Username, proxySettings.Password), false);
|
||||
return new Uri("socks4://" + proxySettings.Host + ":" + proxySettings.Port);
|
||||
case ProxyType.Socks5:
|
||||
return new SocksWebProxy(new ProxyConfig(IPAddress.Loopback, GetNextFreePort(), GetProxyIpAddress(proxySettings.Host), proxySettings.Port, ProxyConfig.SocksVersion.Five, proxySettings.Username, proxySettings.Password), false);
|
||||
return new Uri("socks5://" + proxySettings.Host + ":" + proxySettings.Port);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static IPAddress GetProxyIpAddress(string host)
|
||||
{
|
||||
IPAddress ipAddress;
|
||||
if (!IPAddress.TryParse(host, out ipAddress))
|
||||
{
|
||||
try
|
||||
{
|
||||
ipAddress = Dns.GetHostEntry(host).AddressList.OrderByDescending(a => a.AddressFamily == AddressFamily.InterNetwork).First();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new InvalidOperationException(string.Format("Unable to resolve proxy hostname '{0}' to a valid IP address.", host), e);
|
||||
}
|
||||
}
|
||||
|
||||
return ipAddress;
|
||||
}
|
||||
|
||||
private static int GetNextFreePort()
|
||||
{
|
||||
var listener = new TcpListener(IPAddress.Loopback, 0);
|
||||
listener.Start();
|
||||
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
|
||||
listener.Stop();
|
||||
|
||||
return port;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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|(?:access[-_]?)?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,13 @@ 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),
|
||||
|
||||
// Indexer Responses
|
||||
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),
|
||||
new Regex(@",""pass[- _]?key"":""(?<secret>[^&=]+?)"",", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@",""rss[- _]?key"":""(?<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);
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net5.0</TargetFrameworks>
|
||||
<TargetFrameworks>net6.0</TargetFrameworks>
|
||||
<DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants>
|
||||
</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="6.0.0" />
|
||||
<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" />
|
||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="5.0.0" />
|
||||
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-12" />
|
||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.0" />
|
||||
<PackageReference Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
|
||||
<PackageReference Include="System.Runtime.Loader" Version="4.3.0" />
|
||||
<PackageReference Include="System.ServiceProcess.ServiceController" Version="5.0.0" />
|
||||
<PackageReference Include="System.ServiceProcess.ServiceController" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net5.0</TargetFrameworks>
|
||||
<TargetFrameworks>net6.0</TargetFrameworks>
|
||||
|
||||
<ApplicationIcon>..\NzbDrone.Host\Prowlarr.ico</ApplicationIcon>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
|
||||
4872
src/NzbDrone.Core.Test/Files/Indexers/Avistaz/recentfeed.json
Normal file
4872
src/NzbDrone.Core.Test/Files/Indexers/Avistaz/recentfeed.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Indexers.Definitions;
|
||||
using NzbDrone.Core.Indexers.Definitions.Avistaz;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class AvistazFixture : CoreTest<AvistaZ>
|
||||
{
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
Subject.Definition = new IndexerDefinition()
|
||||
{
|
||||
Name = "AvistaZ",
|
||||
Settings = new AvistazSettings() { Username = "someuser", Password = "somepass", Pid = "somepid" }
|
||||
};
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task should_parse_recent_feed_from_AvistaZ()
|
||||
{
|
||||
var recentFeed = ReadAllText(@"Files/Indexers/Avistaz/recentfeed.json");
|
||||
|
||||
Mocker.GetMock<IIndexerHttpClient>()
|
||||
.Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET), Subject.Definition))
|
||||
.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;
|
||||
|
||||
releases.Should().HaveCount(100);
|
||||
releases.First().Should().BeOfType<TorrentInfo>();
|
||||
|
||||
var torrentInfo = releases.First() as TorrentInfo;
|
||||
|
||||
torrentInfo.Title.Should().Be("JAPAN SINKS: People of Hope 2021 S01E05 720p NF WEB-DL DDP2.0 x264-SEIKEL");
|
||||
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
||||
torrentInfo.DownloadUrl.Should().Be("https://avistaz.to/rss/download/(removed)/(removed).torrent");
|
||||
torrentInfo.InfoUrl.Should().Be("https://avistaz.to/torrent/187240-japan-sinks-people-of-hope-2021-s01e05-720p-nf-web-dl-ddp20-x264-seikel");
|
||||
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-11-14 23:26:21"));
|
||||
torrentInfo.Size.Should().Be(935127615);
|
||||
torrentInfo.InfoHash.Should().Be("a879261d4e6e792402f92401141a21de70d51bf2");
|
||||
torrentInfo.MagnetUrl.Should().Be(null);
|
||||
torrentInfo.Peers.Should().Be(20);
|
||||
torrentInfo.Seeders.Should().Be(20);
|
||||
torrentInfo.ImdbId.Should().Be(15569106);
|
||||
torrentInfo.TmdbId.Should().Be(135144);
|
||||
torrentInfo.TvdbId.Should().Be(410548);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user