mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2026-04-18 21:55:12 -04:00
Compare commits
329 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 99bc56efb6 | |||
| 04276eb587 | |||
| 34c560fd3a | |||
| caa8bb05a7 | |||
| 773e8ff1f4 | |||
| 0984976378 | |||
| fcb3c96455 | |||
| acf7a425b5 | |||
| da898fe958 | |||
| 5bb3ea0806 | |||
| b41cb80e33 | |||
| a39341be4b | |||
| 27b3d8618a | |||
| 550b9b58df | |||
| 035ad33b72 | |||
| 85f8e0c451 | |||
| ea2061a7d3 | |||
| ea6d01a49b | |||
| 252cd97e35 | |||
| a8ea05af07 | |||
| 24d6a0cb06 | |||
| 8e1771b5a9 | |||
| d767a82e84 | |||
| 76bfd29f23 | |||
| c923982711 | |||
| f03a64f9ac | |||
| e713e58e83 | |||
| 4fb5d3432b | |||
| a31b107a90 | |||
| f91ffb8328 | |||
| a3ba070296 | |||
| bccb0bd5c8 | |||
| 4517f271c4 | |||
| 2ae2a0b184 | |||
| b5e43e7a1a | |||
| 3a52048dc2 | |||
| 8b898733ab | |||
| f99a2e1164 | |||
| 306209fcc2 | |||
| 5d09c2b5fa | |||
| 41a9d2d732 | |||
| 49b120ba55 | |||
| a88fc34a78 | |||
| c46b7c5e4b | |||
| 94c45541ae | |||
| f8082047a5 | |||
| 011fd57f7d | |||
| 6c35c3fc6f | |||
| 5da02c49eb | |||
| 1a339b9ab2 | |||
| 94edd7538e | |||
| 9b2274805e | |||
| dbf86efb0a | |||
| 529fbfd9bd | |||
| 0ed5bfe0d0 | |||
| 6a43eb0031 | |||
| a12001a5ef | |||
| b57014762d | |||
| a51a8bf921 | |||
| e8dc5b3206 | |||
| d4f22f3596 | |||
| b6018a4cd7 | |||
| ec389987df | |||
| 6b62504916 | |||
| 626d777d3c | |||
| 234707b291 | |||
| 15734ca0da | |||
| 19913e5b01 | |||
| 156f6505be | |||
| e383287972 | |||
| 0c0cbdac2f | |||
| 0685c2eb04 | |||
| e8c132e908 | |||
| bea9bd39ff | |||
| 077e4727f2 | |||
| 5f7bc82eb5 | |||
| 0dd5c56175 | |||
| 409a218379 | |||
| 07cc1e03c8 | |||
| 560cda8ba0 | |||
| 934f566359 | |||
| 89ae5ceaa6 | |||
| c7d5889e59 | |||
| bea3c051b9 | |||
| c0b1675627 | |||
| 906d09e162 | |||
| 8cd9ad01c2 | |||
| ce2f322478 | |||
| 0487309ee8 | |||
| 9862584611 | |||
| 6a00e0db90 | |||
| c93831dd8b | |||
| 6546ba773c | |||
| 4c3484a898 | |||
| 8561b862f9 | |||
| e1032fb0f5 | |||
| 4063219430 | |||
| e008be8581 | |||
| d6b379df64 | |||
| 27094ccf62 | |||
| edf9473e9a | |||
| a0d11e7e33 | |||
| 7729eb398a | |||
| 989564dbce | |||
| c1f917f1ac | |||
| 4b7e47c397 | |||
| 1529527af9 | |||
| a11bd1c3c7 | |||
| 915b320a4a | |||
| 155f72cc45 | |||
| 3f73fec5c3 | |||
| 8515623ceb | |||
| 963cddb582 | |||
| ede323b8ed | |||
| 07d7fc98b0 | |||
| 1b78fd38db | |||
| 5a9d4d6280 | |||
| 70685de5d2 | |||
| 9860183433 | |||
| 50331c61ae | |||
| bd3408f170 | |||
| c043bf8da9 | |||
| ea3fa6f28d | |||
| 8917347c0b | |||
| 2cebdf4a06 | |||
| 985110cfb9 | |||
| de876247a3 | |||
| bad6c301f8 | |||
| fc3b23394a | |||
| 92c3656bad | |||
| 1acbee2a57 | |||
| c28f9b6bcd | |||
| aa8048968c | |||
| 6646734510 | |||
| 71dd8b6d04 | |||
| 6d87bd9f8c | |||
| 551d969680 | |||
| 57dac6afdd | |||
| 3dfbfd07dd | |||
| 842df6913c | |||
| 599eeb4c61 | |||
| da371dd921 | |||
| fc25ba7ac0 | |||
| 6e1bef13e2 | |||
| 72ee413411 | |||
| e87b45b47e | |||
| cc841fe3d1 | |||
| 264ffdcc26 | |||
| 5cc044aa8f | |||
| de2fd92b6f | |||
| eff09c1f72 | |||
| 9db888c9a3 | |||
| bf78396164 | |||
| 0e7eaa9221 | |||
| 5b82decc31 | |||
| 38ab533272 | |||
| 4914fcd5df | |||
| 858415b037 | |||
| 43f4899324 | |||
| c60a94adfb | |||
| f386ddb806 | |||
| 4175c2577e | |||
| 6ce9e5ceb9 | |||
| c15643be39 | |||
| a58380031d | |||
| 73af5c9a72 | |||
| d556545e7f | |||
| affde5d7b7 | |||
| 518c85dee2 | |||
| ba3a240707 | |||
| 587a73f3d6 | |||
| ae8f017ca8 | |||
| d9098b612e | |||
| 29e7cc06a1 | |||
| 387fb0bd15 | |||
| 2d33560d89 | |||
| 94a797fc1e | |||
| 2e851b0588 | |||
| 7303cdf555 | |||
| 6636cbc4ae | |||
| a5a4f62f25 | |||
| 05a7465a07 | |||
| c35f1212fb | |||
| ad95d73e9d | |||
| 30f53c20ed | |||
| 0199a37a0c | |||
| e9764820c0 | |||
| d285cbb021 | |||
| 8afaa3386d | |||
| c94beb6814 | |||
| c7eb08a0f0 | |||
| 2a2e859420 | |||
| 31f0e8212e | |||
| 1cbb9b1724 | |||
| 45dbcc6b89 | |||
| 3b26613394 | |||
| 6bb8c09fcf | |||
| 810b3612aa | |||
| 57dcd861a9 | |||
| dfe132cda2 | |||
| a635820b48 | |||
| d959e81efb | |||
| ac89cd636f | |||
| 50616f5c9e | |||
| 3f9cb2c6ea | |||
| b5aa85a548 | |||
| 0fa5127c83 | |||
| 4d137886bc | |||
| 9dde041c99 | |||
| a8234c9ce0 | |||
| 9227efdb65 | |||
| fa923e658f | |||
| 364a5564ae | |||
| 9efd0b391e | |||
| 320161e051 | |||
| 38ba810ae8 | |||
| 4e3f460a24 | |||
| 0d918a0aa9 | |||
| a110412665 | |||
| 6c97f1b6ee | |||
| 470779ead2 | |||
| b371f2d913 | |||
| 3ff3452e2d | |||
| df13537e29 | |||
| 5d2fefde8f | |||
| ffb3f83324 | |||
| 1c125733b2 | |||
| 2af7fac15e | |||
| f172d17ecc | |||
| c69843931e | |||
| cd3e99ad87 | |||
| 1cce39b404 | |||
| 9b46ab73e4 | |||
| a352c053ab | |||
| b33e45d266 | |||
| 817d61de91 | |||
| c7e5cc6462 | |||
| 25596fc2e8 | |||
| 9ff0b90626 | |||
| 4f4c011436 | |||
| bd0115931f | |||
| a0d18c546e | |||
| d935b0df82 | |||
| 9e37f69224 | |||
| 2805c4f18b | |||
| dae21f22b9 | |||
| 7ddbe09eca | |||
| 90e3c809c3 | |||
| ec8cf5f57a | |||
| f4bbf2f8af | |||
| ea98d41472 | |||
| b8cb0fd291 | |||
| d3dfa620ac | |||
| 049668f307 | |||
| c400575aac | |||
| 6f122fb2e4 | |||
| a9c210f8e7 | |||
| 1068ba8915 | |||
| 635335d876 | |||
| 2ed51cd933 | |||
| b74c46c554 | |||
| 7029e0d6ee | |||
| 438ea380f5 | |||
| 4eec675d61 | |||
| 0a9bd8287f | |||
| b583ac3a97 | |||
| 4be41ff3fb | |||
| b911f8cc08 | |||
| 22face385f | |||
| 3e700b63c2 | |||
| df0b8fc660 | |||
| f96dbbfc21 | |||
| 4a75f92cb5 | |||
| dd05a9dbd4 | |||
| e78b8d5346 | |||
| 74a1d95ab7 | |||
| f929a7e62f | |||
| e9e4248af4 | |||
| 9e3b43ef12 | |||
| 738a690aac | |||
| 3b7c59e9bb | |||
| b8ca28d955 | |||
| 8797bb7d1c | |||
| be430732f5 | |||
| e7b1380b85 | |||
| c29735741c | |||
| f56a13a375 | |||
| 148d8ee249 | |||
| 3547028b96 | |||
| e4ffa1873e | |||
| 2e85a21576 | |||
| 0a111e7572 | |||
| 25217c0ee8 | |||
| 791592927c | |||
| 4137193a60 | |||
| 99816bfd36 | |||
| 59e5b5bd52 | |||
| 7fa0a2b33c | |||
| 0593ca6b9e | |||
| 06a26b5c87 | |||
| dcae6dc151 | |||
| 04e3ed0ffe | |||
| 1ed5ed9179 | |||
| d292d086ee | |||
| f68915c5dd | |||
| 01e970e1a7 | |||
| 68df439498 | |||
| 33de7ca7ab | |||
| ae2d9b795b | |||
| eadea745f8 | |||
| f958c4aefa | |||
| 4cf9fb0e79 | |||
| bfa68347e6 | |||
| f97b35403d | |||
| bf2e057247 | |||
| 5a278f4e9d | |||
| 232a6efd0d | |||
| 7e01c93b2c | |||
| d58f6551e6 | |||
| 6446528022 | |||
| 7f63757e06 | |||
| b5d789df3a | |||
| 4473551182 | |||
| fd88f44865 | |||
| 69b8be5b67 | |||
| fbde3fe2cd | |||
| f9e2c5b673 | |||
| 5c5dfbb66b | |||
| 2db24d454e |
+7
-7
@@ -117,7 +117,6 @@ dotnet_diagnostic.CA1003.severity = suggestion
|
|||||||
dotnet_diagnostic.CA1008.severity = suggestion
|
dotnet_diagnostic.CA1008.severity = suggestion
|
||||||
dotnet_diagnostic.CA1010.severity = suggestion
|
dotnet_diagnostic.CA1010.severity = suggestion
|
||||||
dotnet_diagnostic.CA1012.severity = suggestion
|
dotnet_diagnostic.CA1012.severity = suggestion
|
||||||
dotnet_diagnostic.CA1014.severity = suggestion
|
|
||||||
dotnet_diagnostic.CA1016.severity = suggestion
|
dotnet_diagnostic.CA1016.severity = suggestion
|
||||||
dotnet_diagnostic.CA1017.severity = suggestion
|
dotnet_diagnostic.CA1017.severity = suggestion
|
||||||
dotnet_diagnostic.CA1018.severity = suggestion
|
dotnet_diagnostic.CA1018.severity = suggestion
|
||||||
@@ -163,6 +162,7 @@ dotnet_diagnostic.CA1309.severity = suggestion
|
|||||||
dotnet_diagnostic.CA1310.severity = suggestion
|
dotnet_diagnostic.CA1310.severity = suggestion
|
||||||
dotnet_diagnostic.CA1401.severity = suggestion
|
dotnet_diagnostic.CA1401.severity = suggestion
|
||||||
dotnet_diagnostic.CA1416.severity = suggestion
|
dotnet_diagnostic.CA1416.severity = suggestion
|
||||||
|
dotnet_diagnostic.CA1419.severity = suggestion
|
||||||
dotnet_diagnostic.CA1507.severity = suggestion
|
dotnet_diagnostic.CA1507.severity = suggestion
|
||||||
dotnet_diagnostic.CA1508.severity = suggestion
|
dotnet_diagnostic.CA1508.severity = suggestion
|
||||||
dotnet_diagnostic.CA1707.severity = suggestion
|
dotnet_diagnostic.CA1707.severity = suggestion
|
||||||
@@ -178,9 +178,6 @@ dotnet_diagnostic.CA1720.severity = suggestion
|
|||||||
dotnet_diagnostic.CA1721.severity = suggestion
|
dotnet_diagnostic.CA1721.severity = suggestion
|
||||||
dotnet_diagnostic.CA1724.severity = suggestion
|
dotnet_diagnostic.CA1724.severity = suggestion
|
||||||
dotnet_diagnostic.CA1725.severity = suggestion
|
dotnet_diagnostic.CA1725.severity = suggestion
|
||||||
dotnet_diagnostic.CA1801.severity = suggestion
|
|
||||||
dotnet_diagnostic.CA1802.severity = suggestion
|
|
||||||
dotnet_diagnostic.CA1805.severity = suggestion
|
|
||||||
dotnet_diagnostic.CA1806.severity = suggestion
|
dotnet_diagnostic.CA1806.severity = suggestion
|
||||||
dotnet_diagnostic.CA1810.severity = suggestion
|
dotnet_diagnostic.CA1810.severity = suggestion
|
||||||
dotnet_diagnostic.CA1812.severity = suggestion
|
dotnet_diagnostic.CA1812.severity = suggestion
|
||||||
@@ -192,13 +189,14 @@ dotnet_diagnostic.CA1819.severity = suggestion
|
|||||||
dotnet_diagnostic.CA1822.severity = suggestion
|
dotnet_diagnostic.CA1822.severity = suggestion
|
||||||
dotnet_diagnostic.CA1823.severity = suggestion
|
dotnet_diagnostic.CA1823.severity = suggestion
|
||||||
dotnet_diagnostic.CA1824.severity = suggestion
|
dotnet_diagnostic.CA1824.severity = suggestion
|
||||||
|
dotnet_diagnostic.CA1835.severity = suggestion
|
||||||
|
dotnet_diagnostic.CA1845.severity = suggestion
|
||||||
|
dotnet_diagnostic.CA1848.severity = suggestion
|
||||||
|
dotnet_diagnostic.CA1849.severity = suggestion
|
||||||
dotnet_diagnostic.CA2000.severity = suggestion
|
dotnet_diagnostic.CA2000.severity = suggestion
|
||||||
dotnet_diagnostic.CA2002.severity = suggestion
|
dotnet_diagnostic.CA2002.severity = suggestion
|
||||||
dotnet_diagnostic.CA2007.severity = suggestion
|
dotnet_diagnostic.CA2007.severity = suggestion
|
||||||
dotnet_diagnostic.CA2008.severity = suggestion
|
dotnet_diagnostic.CA2008.severity = suggestion
|
||||||
dotnet_diagnostic.CA2009.severity = suggestion
|
|
||||||
dotnet_diagnostic.CA2010.severity = suggestion
|
|
||||||
dotnet_diagnostic.CA2011.severity = suggestion
|
|
||||||
dotnet_diagnostic.CA2012.severity = suggestion
|
dotnet_diagnostic.CA2012.severity = suggestion
|
||||||
dotnet_diagnostic.CA2013.severity = suggestion
|
dotnet_diagnostic.CA2013.severity = suggestion
|
||||||
dotnet_diagnostic.CA2100.severity = suggestion
|
dotnet_diagnostic.CA2100.severity = suggestion
|
||||||
@@ -229,6 +227,7 @@ dotnet_diagnostic.CA2243.severity = suggestion
|
|||||||
dotnet_diagnostic.CA2244.severity = suggestion
|
dotnet_diagnostic.CA2244.severity = suggestion
|
||||||
dotnet_diagnostic.CA2245.severity = suggestion
|
dotnet_diagnostic.CA2245.severity = suggestion
|
||||||
dotnet_diagnostic.CA2246.severity = suggestion
|
dotnet_diagnostic.CA2246.severity = suggestion
|
||||||
|
dotnet_diagnostic.CA2254.severity = suggestion
|
||||||
dotnet_diagnostic.CA3061.severity = suggestion
|
dotnet_diagnostic.CA3061.severity = suggestion
|
||||||
dotnet_diagnostic.CA3075.severity = suggestion
|
dotnet_diagnostic.CA3075.severity = suggestion
|
||||||
dotnet_diagnostic.CA3076.severity = suggestion
|
dotnet_diagnostic.CA3076.severity = suggestion
|
||||||
@@ -255,6 +254,7 @@ dotnet_diagnostic.CA5385.severity = suggestion
|
|||||||
dotnet_diagnostic.CA5392.severity = suggestion
|
dotnet_diagnostic.CA5392.severity = suggestion
|
||||||
dotnet_diagnostic.CA5394.severity = suggestion
|
dotnet_diagnostic.CA5394.severity = suggestion
|
||||||
dotnet_diagnostic.CA5397.severity = suggestion
|
dotnet_diagnostic.CA5397.severity = suggestion
|
||||||
|
dotnet_diagnostic.CA5401.severity = suggestion
|
||||||
|
|
||||||
dotnet_diagnostic.SYSLIB0014.severity = none
|
dotnet_diagnostic.SYSLIB0014.severity = none
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ body:
|
|||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
attributes:
|
attributes:
|
||||||
label: Is there an existing issue for this?
|
label: Is there an existing issue for this?
|
||||||
description: Please search to see if an issue already exists for the bug you encountered.
|
description: Please search to see if an open or closed issue already exists for the bug you encountered. If a bug exists and is closed note that it may only be fixed in an unstable branch.
|
||||||
options:
|
options:
|
||||||
- label: I have searched the existing issues
|
- label: I have searched the existing open and closed issues
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ body:
|
|||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
attributes:
|
attributes:
|
||||||
label: Is there an existing issue for this?
|
label: Is there an existing issue for this?
|
||||||
description: Please search to see if an issue already exists for the feature you are requesting.
|
description: Please search to see if an open or closed issue already exists for the feature you are requesting. If a request exists and is closed note that it may only be fixed in an unstable branch.
|
||||||
options:
|
options:
|
||||||
- label: I have searched the existing issues
|
- label: I have searched the existing open and closed issues
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
|
nationality, personal appearance, race, caste, color, religion, or sexual
|
||||||
|
identity and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
|
diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to a positive environment for our
|
||||||
|
community include:
|
||||||
|
|
||||||
|
* Demonstrating empathy and kindness toward other people
|
||||||
|
* Being respectful of differing opinions, viewpoints, and experiences
|
||||||
|
* Giving and gracefully accepting constructive feedback
|
||||||
|
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
|
and learning from the experience
|
||||||
|
* Focusing on what is best not just for us as individuals, but for the overall
|
||||||
|
community
|
||||||
|
|
||||||
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery, and sexual attention or advances of
|
||||||
|
any kind
|
||||||
|
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or email address,
|
||||||
|
without their explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
|
Community leaders are responsible for clarifying and enforcing our standards of
|
||||||
|
acceptable behavior and will take appropriate and fair corrective action in
|
||||||
|
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||||
|
or harmful.
|
||||||
|
|
||||||
|
Community leaders have the right and responsibility to remove, edit, or reject
|
||||||
|
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||||
|
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||||
|
decisions when appropriate.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all community spaces, and also applies when
|
||||||
|
an individual is officially representing the community in public spaces.
|
||||||
|
Examples of representing our community include using an official e-mail address,
|
||||||
|
posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported to the community leaders responsible for enforcement at
|
||||||
|
<development@prowlarr.com>.
|
||||||
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
|
reporter of any incident.
|
||||||
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Community leaders will follow these Community Impact Guidelines in determining
|
||||||
|
the consequences for any action they deem in violation of this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||||
|
unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning from community leaders, providing
|
||||||
|
clarity around the nature of the violation and an explanation of why the
|
||||||
|
behavior was inappropriate. A public apology may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Community Impact**: A violation through a single incident or series of
|
||||||
|
actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued behavior. No
|
||||||
|
interaction with the people involved, including unsolicited interaction with
|
||||||
|
those enforcing the Code of Conduct, for a specified period of time. This
|
||||||
|
includes avoiding interactions in community spaces as well as external channels
|
||||||
|
like social media. Violating these terms may lead to a temporary or permanent
|
||||||
|
ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Community Impact**: A serious violation of community standards, including
|
||||||
|
sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or public
|
||||||
|
communication with the community for a specified period of time. No public or
|
||||||
|
private interaction with the people involved, including unsolicited interaction
|
||||||
|
with those enforcing the Code of Conduct, is allowed during this period.
|
||||||
|
Violating these terms may lead to a permanent ban.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction within the
|
||||||
|
community.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
|
version 2.1, available at
|
||||||
|
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||||
|
|
||||||
|
Community Impact Guidelines were inspired by
|
||||||
|
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
|
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
|
||||||
|
[https://www.contributor-covenant.org/translations][translations].
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||||
|
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||||
|
[FAQ]: https://www.contributor-covenant.org/faq
|
||||||
|
[translations]: https://www.contributor-covenant.org/translations
|
||||||
+6
-5
@@ -9,13 +9,13 @@ variables:
|
|||||||
testsFolder: './_tests'
|
testsFolder: './_tests'
|
||||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||||
majorVersion: '0.4.4'
|
majorVersion: '1.2.2'
|
||||||
minorVersion: $[counter('minorVersion', 1)]
|
minorVersion: $[counter('minorVersion', 1)]
|
||||||
prowlarrVersion: '$(majorVersion).$(minorVersion)'
|
prowlarrVersion: '$(majorVersion).$(minorVersion)'
|
||||||
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
|
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
|
||||||
sentryOrg: 'servarr'
|
sentryOrg: 'servarr'
|
||||||
sentryUrl: 'https://sentry.servarr.com'
|
sentryUrl: 'https://sentry.servarr.com'
|
||||||
dotnetVersion: '6.0.301'
|
dotnetVersion: '6.0.405'
|
||||||
innoVersion: '6.2.0'
|
innoVersion: '6.2.0'
|
||||||
nodeVersion: '16.x'
|
nodeVersion: '16.x'
|
||||||
windowsImage: 'windows-2022'
|
windowsImage: 'windows-2022'
|
||||||
@@ -541,7 +541,7 @@ stages:
|
|||||||
Prowlarr__Postgres__Password: 'prowlarr'
|
Prowlarr__Postgres__Password: 'prowlarr'
|
||||||
|
|
||||||
pool:
|
pool:
|
||||||
vmImage: 'ubuntu-18.04'
|
vmImage: ${{ variables.linuxImage }}
|
||||||
|
|
||||||
timeoutInMinutes: 10
|
timeoutInMinutes: 10
|
||||||
|
|
||||||
@@ -675,7 +675,7 @@ stages:
|
|||||||
Prowlarr__Postgres__Password: 'prowlarr'
|
Prowlarr__Postgres__Password: 'prowlarr'
|
||||||
|
|
||||||
pool:
|
pool:
|
||||||
vmImage: 'ubuntu-18.04'
|
vmImage: ${{ variables.linuxImage }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- task: UseDotNet@2
|
- task: UseDotNet@2
|
||||||
@@ -748,7 +748,7 @@ stages:
|
|||||||
inputs:
|
inputs:
|
||||||
buildType: 'current'
|
buildType: 'current'
|
||||||
artifactName: Packages
|
artifactName: Packages
|
||||||
itemPattern: '/$(pattern)'
|
itemPattern: '**/$(pattern)'
|
||||||
targetPath: $(Build.ArtifactStagingDirectory)
|
targetPath: $(Build.ArtifactStagingDirectory)
|
||||||
- bash: |
|
- bash: |
|
||||||
mkdir -p ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin
|
mkdir -p ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin
|
||||||
@@ -1108,4 +1108,5 @@ stages:
|
|||||||
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
|
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
|
||||||
DISCORDCHANNELID: $(discordChannelId)
|
DISCORDCHANNELID: $(discordChannelId)
|
||||||
DISCORDWEBHOOKKEY: $(discordWebhookKey)
|
DISCORDWEBHOOKKEY: $(discordWebhookKey)
|
||||||
|
DISCORDTHREADID: $(discordThreadId)
|
||||||
|
|
||||||
|
|||||||
@@ -142,8 +142,8 @@ module.exports = (env) => {
|
|||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
test: /\.js?$/,
|
test: /\.jsx?$/,
|
||||||
exclude: /(node_modules|JsLibraries)/,
|
exclude: /[\\/]node_modules[\\/](?!(@sentry\/browser|@sentry\/integrations|chart.js|filesize|normalize.css)[\\/])/,
|
||||||
use: [
|
use: [
|
||||||
{
|
{
|
||||||
loader: 'babel-loader',
|
loader: 'babel-loader',
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ function CustomFiltersModalContent(props) {
|
|||||||
|
|
||||||
<div className={styles.addButtonContainer}>
|
<div className={styles.addButtonContainer}>
|
||||||
<Button onPress={onAddCustomFilter}>
|
<Button onPress={onAddCustomFilter}>
|
||||||
Add Custom Filter
|
{translate('AddCustomFilter')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import FormInputHelpText from './FormInputHelpText';
|
|||||||
import IndexerFlagsSelectInputConnector from './IndexerFlagsSelectInputConnector';
|
import IndexerFlagsSelectInputConnector from './IndexerFlagsSelectInputConnector';
|
||||||
import InfoInput from './InfoInput';
|
import InfoInput from './InfoInput';
|
||||||
import KeyValueListInput from './KeyValueListInput';
|
import KeyValueListInput from './KeyValueListInput';
|
||||||
|
import NewznabCategorySelectInputConnector from './NewznabCategorySelectInputConnector';
|
||||||
import NumberInput from './NumberInput';
|
import NumberInput from './NumberInput';
|
||||||
import OAuthInputConnector from './OAuthInputConnector';
|
import OAuthInputConnector from './OAuthInputConnector';
|
||||||
import PasswordInput from './PasswordInput';
|
import PasswordInput from './PasswordInput';
|
||||||
@@ -68,6 +69,9 @@ function getComponent(type) {
|
|||||||
case inputTypes.PATH:
|
case inputTypes.PATH:
|
||||||
return PathInputConnector;
|
return PathInputConnector;
|
||||||
|
|
||||||
|
case inputTypes.CATEGORY_SELECT:
|
||||||
|
return NewznabCategorySelectInputConnector;
|
||||||
|
|
||||||
case inputTypes.INDEXER_FLAGS_SELECT:
|
case inputTypes.INDEXER_FLAGS_SELECT:
|
||||||
return IndexerFlagsSelectInputConnector;
|
return IndexerFlagsSelectInputConnector;
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ function createMapStateToProps() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
value,
|
value: value || [],
|
||||||
values
|
values
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ class TagInputInput extends Component {
|
|||||||
<div
|
<div
|
||||||
ref={forwardedRef}
|
ref={forwardedRef}
|
||||||
className={className}
|
className={className}
|
||||||
component="div"
|
|
||||||
onMouseDown={this.onMouseDown}
|
onMouseDown={this.onMouseDown}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -36,5 +36,5 @@
|
|||||||
/** Outline **/
|
/** Outline **/
|
||||||
|
|
||||||
.outline {
|
.outline {
|
||||||
background-color: var(--white);
|
background-color: var(--cardBackgroundColor);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,5 +108,5 @@
|
|||||||
/** Outline **/
|
/** Outline **/
|
||||||
|
|
||||||
.outline {
|
.outline {
|
||||||
background-color: var(--white);
|
background-color: var(--cardBackgroundColor);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--toobarButtonHoverColor);
|
color: #515253;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import AppUpdatedModalConnector from 'App/AppUpdatedModalConnector';
|
|||||||
import ColorImpairedContext from 'App/ColorImpairedContext';
|
import ColorImpairedContext from 'App/ColorImpairedContext';
|
||||||
import ConnectionLostModalConnector from 'App/ConnectionLostModalConnector';
|
import ConnectionLostModalConnector from 'App/ConnectionLostModalConnector';
|
||||||
import SignalRConnector from 'Components/SignalRConnector';
|
import SignalRConnector from 'Components/SignalRConnector';
|
||||||
|
import AuthenticationRequiredModal from 'FirstRun/AuthenticationRequiredModal';
|
||||||
import locationShape from 'Helpers/Props/Shapes/locationShape';
|
import locationShape from 'Helpers/Props/Shapes/locationShape';
|
||||||
import PageHeader from './Header/PageHeader';
|
import PageHeader from './Header/PageHeader';
|
||||||
import PageSidebar from './Sidebar/PageSidebar';
|
import PageSidebar from './Sidebar/PageSidebar';
|
||||||
@@ -75,6 +76,7 @@ class Page extends Component {
|
|||||||
isSmallScreen,
|
isSmallScreen,
|
||||||
isSidebarVisible,
|
isSidebarVisible,
|
||||||
enableColorImpairedMode,
|
enableColorImpairedMode,
|
||||||
|
authenticationEnabled,
|
||||||
onSidebarToggle,
|
onSidebarToggle,
|
||||||
onSidebarVisibleChange
|
onSidebarVisibleChange
|
||||||
} = this.props;
|
} = this.props;
|
||||||
@@ -109,6 +111,10 @@ class Page extends Component {
|
|||||||
isOpen={this.state.isConnectionLostModalOpen}
|
isOpen={this.state.isConnectionLostModalOpen}
|
||||||
onModalClose={this.onConnectionLostModalClose}
|
onModalClose={this.onConnectionLostModalClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<AuthenticationRequiredModal
|
||||||
|
isOpen={!authenticationEnabled}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ColorImpairedContext.Provider>
|
</ColorImpairedContext.Provider>
|
||||||
);
|
);
|
||||||
@@ -124,6 +130,7 @@ Page.propTypes = {
|
|||||||
isUpdated: PropTypes.bool.isRequired,
|
isUpdated: PropTypes.bool.isRequired,
|
||||||
isDisconnected: PropTypes.bool.isRequired,
|
isDisconnected: PropTypes.bool.isRequired,
|
||||||
enableColorImpairedMode: PropTypes.bool.isRequired,
|
enableColorImpairedMode: PropTypes.bool.isRequired,
|
||||||
|
authenticationEnabled: PropTypes.bool.isRequired,
|
||||||
onResize: PropTypes.func.isRequired,
|
onResize: PropTypes.func.isRequired,
|
||||||
onSidebarToggle: PropTypes.func.isRequired,
|
onSidebarToggle: PropTypes.func.isRequired,
|
||||||
onSidebarVisibleChange: PropTypes.func.isRequired
|
onSidebarVisibleChange: PropTypes.func.isRequired
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { fetchAppProfiles, fetchGeneralSettings, fetchIndexerCategories, fetchUI
|
|||||||
import { fetchStatus } from 'Store/Actions/systemActions';
|
import { fetchStatus } from 'Store/Actions/systemActions';
|
||||||
import { fetchTags } from 'Store/Actions/tagActions';
|
import { fetchTags } from 'Store/Actions/tagActions';
|
||||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
|
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
||||||
import ErrorPage from './ErrorPage';
|
import ErrorPage from './ErrorPage';
|
||||||
import LoadingPage from './LoadingPage';
|
import LoadingPage from './LoadingPage';
|
||||||
import Page from './Page';
|
import Page from './Page';
|
||||||
@@ -133,18 +134,21 @@ function createMapStateToProps() {
|
|||||||
selectErrors,
|
selectErrors,
|
||||||
selectAppProps,
|
selectAppProps,
|
||||||
createDimensionsSelector(),
|
createDimensionsSelector(),
|
||||||
|
createSystemStatusSelector(),
|
||||||
(
|
(
|
||||||
enableColorImpairedMode,
|
enableColorImpairedMode,
|
||||||
isPopulated,
|
isPopulated,
|
||||||
errors,
|
errors,
|
||||||
app,
|
app,
|
||||||
dimensions
|
dimensions,
|
||||||
|
systemStatus
|
||||||
) => {
|
) => {
|
||||||
return {
|
return {
|
||||||
...app,
|
...app,
|
||||||
...errors,
|
...errors,
|
||||||
isPopulated,
|
isPopulated,
|
||||||
isSmallScreen: dimensions.isSmallScreen,
|
isSmallScreen: dimensions.isSmallScreen,
|
||||||
|
authenticationEnabled: systemStatus.authentication !== 'none',
|
||||||
enableColorImpairedMode
|
enableColorImpairedMode
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import { sizes } from 'Helpers/Props';
|
||||||
|
import AuthenticationRequiredModalContentConnector from './AuthenticationRequiredModalContentConnector';
|
||||||
|
|
||||||
|
function onModalClose() {
|
||||||
|
// No-op
|
||||||
|
}
|
||||||
|
|
||||||
|
function AuthenticationRequiredModal(props) {
|
||||||
|
const {
|
||||||
|
isOpen
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
size={sizes.MEDIUM}
|
||||||
|
isOpen={isOpen}
|
||||||
|
closeOnBackgroundClick={false}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
>
|
||||||
|
<AuthenticationRequiredModalContentConnector
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthenticationRequiredModal.propTypes = {
|
||||||
|
isOpen: PropTypes.bool.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AuthenticationRequiredModal;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
.authRequiredAlert {
|
||||||
|
composes: alert from '~Components/Alert.css';
|
||||||
|
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { useEffect, useRef } from 'react';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import { inputTypes, kinds } from 'Helpers/Props';
|
||||||
|
import { authenticationMethodOptions, authenticationRequiredOptions, authenticationRequiredWarning } from 'Settings/General/SecuritySettings';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './AuthenticationRequiredModalContent.css';
|
||||||
|
|
||||||
|
function onModalClose() {
|
||||||
|
// No-op
|
||||||
|
}
|
||||||
|
|
||||||
|
function AuthenticationRequiredModalContent(props) {
|
||||||
|
const {
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
isSaving,
|
||||||
|
settings,
|
||||||
|
onInputChange,
|
||||||
|
onSavePress,
|
||||||
|
dispatchFetchStatus
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
authenticationMethod,
|
||||||
|
authenticationRequired,
|
||||||
|
username,
|
||||||
|
password
|
||||||
|
} = settings;
|
||||||
|
|
||||||
|
const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none';
|
||||||
|
|
||||||
|
const didMount = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isSaving && didMount.current) {
|
||||||
|
dispatchFetchStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
didMount.current = true;
|
||||||
|
}, [isSaving, dispatchFetchStatus]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent
|
||||||
|
showCloseButton={false}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
>
|
||||||
|
<ModalHeader>
|
||||||
|
{translate('AuthenticationRequired')}
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<Alert
|
||||||
|
className={styles.authRequiredAlert}
|
||||||
|
kind={kinds.WARNING}
|
||||||
|
>
|
||||||
|
{authenticationRequiredWarning}
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
{
|
||||||
|
isPopulated && !error ?
|
||||||
|
<div>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('Authentication')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="authenticationMethod"
|
||||||
|
values={authenticationMethodOptions}
|
||||||
|
helpText={translate('AuthenticationMethodHelpText')}
|
||||||
|
onChange={onInputChange}
|
||||||
|
{...authenticationMethod}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
{
|
||||||
|
authenticationEnabled ?
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('AuthenticationRequired')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="authenticationRequired"
|
||||||
|
values={authenticationRequiredOptions}
|
||||||
|
helpText={translate('AuthenticationRequiredHelpText')}
|
||||||
|
onChange={onInputChange}
|
||||||
|
{...authenticationRequired}
|
||||||
|
/>
|
||||||
|
</FormGroup> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
authenticationEnabled ?
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('Username')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.TEXT}
|
||||||
|
name="username"
|
||||||
|
onChange={onInputChange}
|
||||||
|
{...username}
|
||||||
|
/>
|
||||||
|
</FormGroup> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
authenticationEnabled ?
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('Password')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.PASSWORD}
|
||||||
|
name="password"
|
||||||
|
onChange={onInputChange}
|
||||||
|
{...password}
|
||||||
|
/>
|
||||||
|
</FormGroup> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
</div> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!isPopulated && !error ? <LoadingIndicator /> : null
|
||||||
|
}
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<SpinnerButton
|
||||||
|
kind={kinds.PRIMARY}
|
||||||
|
isSpinning={isSaving}
|
||||||
|
isDisabled={!authenticationEnabled}
|
||||||
|
onPress={onSavePress}
|
||||||
|
>
|
||||||
|
{translate('Save')}
|
||||||
|
</SpinnerButton>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthenticationRequiredModalContent.propTypes = {
|
||||||
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
|
error: PropTypes.object,
|
||||||
|
isSaving: PropTypes.bool.isRequired,
|
||||||
|
saveError: PropTypes.object,
|
||||||
|
settings: PropTypes.object.isRequired,
|
||||||
|
onInputChange: PropTypes.func.isRequired,
|
||||||
|
onSavePress: PropTypes.func.isRequired,
|
||||||
|
dispatchFetchStatus: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AuthenticationRequiredModalContent;
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||||
|
import { fetchGeneralSettings, saveGeneralSettings, setGeneralSettingsValue } from 'Store/Actions/settingsActions';
|
||||||
|
import { fetchStatus } from 'Store/Actions/systemActions';
|
||||||
|
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
|
||||||
|
import AuthenticationRequiredModalContent from './AuthenticationRequiredModalContent';
|
||||||
|
|
||||||
|
const SECTION = 'general';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
createSettingsSectionSelector(SECTION),
|
||||||
|
(sectionSettings) => {
|
||||||
|
return {
|
||||||
|
...sectionSettings
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
dispatchClearPendingChanges: clearPendingChanges,
|
||||||
|
dispatchSetGeneralSettingsValue: setGeneralSettingsValue,
|
||||||
|
dispatchSaveGeneralSettings: saveGeneralSettings,
|
||||||
|
dispatchFetchGeneralSettings: fetchGeneralSettings,
|
||||||
|
dispatchFetchStatus: fetchStatus
|
||||||
|
};
|
||||||
|
|
||||||
|
class AuthenticationRequiredModalContentConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.dispatchFetchGeneralSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.props.dispatchClearPendingChanges({ section: `settings.${SECTION}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onInputChange = ({ name, value }) => {
|
||||||
|
this.props.dispatchSetGeneralSettingsValue({ name, value });
|
||||||
|
};
|
||||||
|
|
||||||
|
onSavePress = () => {
|
||||||
|
this.props.dispatchSaveGeneralSettings();
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
dispatchClearPendingChanges,
|
||||||
|
dispatchFetchGeneralSettings,
|
||||||
|
dispatchSetGeneralSettingsValue,
|
||||||
|
dispatchSaveGeneralSettings,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthenticationRequiredModalContent
|
||||||
|
{...otherProps}
|
||||||
|
onInputChange={this.onInputChange}
|
||||||
|
onSavePress={this.onSavePress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthenticationRequiredModalContentConnector.propTypes = {
|
||||||
|
dispatchClearPendingChanges: PropTypes.func.isRequired,
|
||||||
|
dispatchFetchGeneralSettings: PropTypes.func.isRequired,
|
||||||
|
dispatchSetGeneralSettingsValue: PropTypes.func.isRequired,
|
||||||
|
dispatchSaveGeneralSettings: PropTypes.func.isRequired,
|
||||||
|
dispatchFetchStatus: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(AuthenticationRequiredModalContentConnector);
|
||||||
@@ -8,6 +8,7 @@ export const DEVICE = 'device';
|
|||||||
export const KEY_VALUE_LIST = 'keyValueList';
|
export const KEY_VALUE_LIST = 'keyValueList';
|
||||||
export const INFO = 'info';
|
export const INFO = 'info';
|
||||||
export const MOVIE_MONITORED_SELECT = 'movieMonitoredSelect';
|
export const MOVIE_MONITORED_SELECT = 'movieMonitoredSelect';
|
||||||
|
export const CATEGORY_SELECT = 'newznabCategorySelect';
|
||||||
export const NUMBER = 'number';
|
export const NUMBER = 'number';
|
||||||
export const OAUTH = 'oauth';
|
export const OAUTH = 'oauth';
|
||||||
export const PASSWORD = 'password';
|
export const PASSWORD = 'password';
|
||||||
@@ -32,6 +33,7 @@ export const all = [
|
|||||||
KEY_VALUE_LIST,
|
KEY_VALUE_LIST,
|
||||||
INFO,
|
INFO,
|
||||||
MOVIE_MONITORED_SELECT,
|
MOVIE_MONITORED_SELECT,
|
||||||
|
CATEGORY_SELECT,
|
||||||
NUMBER,
|
NUMBER,
|
||||||
OAUTH,
|
OAUTH,
|
||||||
PASSWORD,
|
PASSWORD,
|
||||||
|
|||||||
@@ -226,6 +226,42 @@ class HistoryRow extends Component {
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
data.label ?
|
||||||
|
<HistoryRowParameter
|
||||||
|
title='Label'
|
||||||
|
value={data.label}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
data.track ?
|
||||||
|
<HistoryRowParameter
|
||||||
|
title='Track'
|
||||||
|
value={data.track}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
data.year ?
|
||||||
|
<HistoryRowParameter
|
||||||
|
title='Year'
|
||||||
|
value={data.year}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
data.genre ?
|
||||||
|
<HistoryRowParameter
|
||||||
|
title='Genre'
|
||||||
|
value={data.genre}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
data.author ?
|
data.author ?
|
||||||
<HistoryRowParameter
|
<HistoryRowParameter
|
||||||
@@ -243,6 +279,15 @@ class HistoryRow extends Component {
|
|||||||
/> :
|
/> :
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
data.publisher ?
|
||||||
|
<HistoryRowParameter
|
||||||
|
title='Publisher'
|
||||||
|
value={data.publisher}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ class AddIndexerModalContent extends Component {
|
|||||||
const filteredIndexers = indexers.filter((indexer) => {
|
const filteredIndexers = indexers.filter((indexer) => {
|
||||||
const { filter, filterProtocols, filterLanguages, filterPrivacyLevels } = this.state;
|
const { filter, filterProtocols, filterLanguages, filterPrivacyLevels } = this.state;
|
||||||
|
|
||||||
if (!indexer.name.toLowerCase().includes(filter.toLocaleLowerCase())) {
|
if (!indexer.name.toLowerCase().includes(filter.toLocaleLowerCase()) && !indexer.description.toLowerCase().includes(filter.toLocaleLowerCase())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -61,15 +61,15 @@ class TagsModalContent extends Component {
|
|||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const applyTagsOptions = [
|
const applyTagsOptions = [
|
||||||
{ key: 'add', value: 'Add' },
|
{ key: 'add', value: translate('Add') },
|
||||||
{ key: 'remove', value: 'Remove' },
|
{ key: 'remove', value: translate('Remove') },
|
||||||
{ key: 'replace', value: 'Replace' }
|
{ key: 'replace', value: translate('Replace') }
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
Tags
|
{translate('Tags')}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ class IndexerIndex extends Component {
|
|||||||
|
|
||||||
onKeyUp = (event) => {
|
onKeyUp = (event) => {
|
||||||
const jumpBarItems = this.state.jumpBarItems.order;
|
const jumpBarItems = this.state.jumpBarItems.order;
|
||||||
if (event.path.length === 4) {
|
if (event.composedPath && event.composedPath().length === 4) {
|
||||||
if (event.keyCode === keyCodes.HOME && event.ctrlKey) {
|
if (event.keyCode === keyCodes.HOME && event.ctrlKey) {
|
||||||
this.setState({ jumpToCharacter: jumpBarItems[0] });
|
this.setState({ jumpToCharacter: jumpBarItems[0] });
|
||||||
}
|
}
|
||||||
@@ -272,6 +272,7 @@ class IndexerIndex extends Component {
|
|||||||
saveError,
|
saveError,
|
||||||
isDeleting,
|
isDeleting,
|
||||||
isTestingAll,
|
isTestingAll,
|
||||||
|
isSyncingIndexers,
|
||||||
deleteError,
|
deleteError,
|
||||||
onScroll,
|
onScroll,
|
||||||
onSortSelect,
|
onSortSelect,
|
||||||
@@ -309,6 +310,15 @@ class IndexerIndex extends Component {
|
|||||||
onPress={this.onAddIndexerPress}
|
onPress={this.onAddIndexerPress}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<PageToolbarSeparator />
|
||||||
|
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('SyncAppIndexers')}
|
||||||
|
iconName={icons.REFRESH}
|
||||||
|
isSpinning={isSyncingIndexers}
|
||||||
|
onPress={this.props.onAppIndexerSyncPress}
|
||||||
|
/>
|
||||||
|
|
||||||
<PageToolbarButton
|
<PageToolbarButton
|
||||||
label={translate('TestAllIndexers')}
|
label={translate('TestAllIndexers')}
|
||||||
iconName={icons.TEST}
|
iconName={icons.TEST}
|
||||||
@@ -493,10 +503,12 @@ IndexerIndex.propTypes = {
|
|||||||
saveError: PropTypes.object,
|
saveError: PropTypes.object,
|
||||||
isDeleting: PropTypes.bool.isRequired,
|
isDeleting: PropTypes.bool.isRequired,
|
||||||
isTestingAll: PropTypes.bool.isRequired,
|
isTestingAll: PropTypes.bool.isRequired,
|
||||||
|
isSyncingIndexers: PropTypes.bool.isRequired,
|
||||||
deleteError: PropTypes.object,
|
deleteError: PropTypes.object,
|
||||||
onSortSelect: PropTypes.func.isRequired,
|
onSortSelect: PropTypes.func.isRequired,
|
||||||
onFilterSelect: PropTypes.func.isRequired,
|
onFilterSelect: PropTypes.func.isRequired,
|
||||||
onTestAllPress: PropTypes.func.isRequired,
|
onTestAllPress: PropTypes.func.isRequired,
|
||||||
|
onAppIndexerSyncPress: PropTypes.func.isRequired,
|
||||||
onScroll: PropTypes.func.isRequired,
|
onScroll: PropTypes.func.isRequired,
|
||||||
onSaveSelected: PropTypes.func.isRequired
|
onSaveSelected: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,10 +2,13 @@ import PropTypes from 'prop-types';
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
import * as commandNames from 'Commands/commandNames';
|
||||||
import withScrollPosition from 'Components/withScrollPosition';
|
import withScrollPosition from 'Components/withScrollPosition';
|
||||||
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
import { testAllIndexers } from 'Store/Actions/indexerActions';
|
import { testAllIndexers } from 'Store/Actions/indexerActions';
|
||||||
import { saveIndexerEditor, setMovieFilter, setMovieSort, setMovieTableOption } from 'Store/Actions/indexerIndexActions';
|
import { saveIndexerEditor, setMovieFilter, setMovieSort, setMovieTableOption } from 'Store/Actions/indexerIndexActions';
|
||||||
import scrollPositions from 'Store/scrollPositions';
|
import scrollPositions from 'Store/scrollPositions';
|
||||||
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
import createIndexerClientSideCollectionItemsSelector from 'Store/Selectors/createIndexerClientSideCollectionItemsSelector';
|
import createIndexerClientSideCollectionItemsSelector from 'Store/Selectors/createIndexerClientSideCollectionItemsSelector';
|
||||||
import IndexerIndex from './IndexerIndex';
|
import IndexerIndex from './IndexerIndex';
|
||||||
@@ -13,13 +16,16 @@ import IndexerIndex from './IndexerIndex';
|
|||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
createIndexerClientSideCollectionItemsSelector('indexerIndex'),
|
createIndexerClientSideCollectionItemsSelector('indexerIndex'),
|
||||||
|
createCommandExecutingSelector(commandNames.APP_INDEXER_SYNC),
|
||||||
createDimensionsSelector(),
|
createDimensionsSelector(),
|
||||||
(
|
(
|
||||||
indexers,
|
indexers,
|
||||||
|
isSyncingIndexers,
|
||||||
dimensionsState
|
dimensionsState
|
||||||
) => {
|
) => {
|
||||||
return {
|
return {
|
||||||
...indexers,
|
...indexers,
|
||||||
|
isSyncingIndexers,
|
||||||
isSmallScreen: dimensionsState.isSmallScreen
|
isSmallScreen: dimensionsState.isSmallScreen
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -46,6 +52,12 @@ function createMapDispatchToProps(dispatch, props) {
|
|||||||
|
|
||||||
onTestAllPress() {
|
onTestAllPress() {
|
||||||
dispatch(testAllIndexers());
|
dispatch(testAllIndexers());
|
||||||
|
},
|
||||||
|
|
||||||
|
onAppIndexerSyncPress() {
|
||||||
|
dispatch(executeCommand({
|
||||||
|
name: commandNames.APP_INDEXER_SYNC
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ function IndexerIndexSortMenu(props) {
|
|||||||
sortDirection={sortDirection}
|
sortDirection={sortDirection}
|
||||||
onPress={onSortSelect}
|
onPress={onSortSelect}
|
||||||
>
|
>
|
||||||
Status
|
{translate('Status')}
|
||||||
</SortMenuItem>
|
</SortMenuItem>
|
||||||
|
|
||||||
<SortMenuItem
|
<SortMenuItem
|
||||||
@@ -62,7 +62,7 @@ function IndexerIndexSortMenu(props) {
|
|||||||
sortDirection={sortDirection}
|
sortDirection={sortDirection}
|
||||||
onPress={onSortSelect}
|
onPress={onSortSelect}
|
||||||
>
|
>
|
||||||
{'Priority'}
|
{translate('Priority')}
|
||||||
</SortMenuItem>
|
</SortMenuItem>
|
||||||
|
|
||||||
<SortMenuItem
|
<SortMenuItem
|
||||||
@@ -71,7 +71,7 @@ function IndexerIndexSortMenu(props) {
|
|||||||
sortDirection={sortDirection}
|
sortDirection={sortDirection}
|
||||||
onPress={onSortSelect}
|
onPress={onSortSelect}
|
||||||
>
|
>
|
||||||
{'Protocol'}
|
{translate('Protocol')}
|
||||||
</SortMenuItem>
|
</SortMenuItem>
|
||||||
|
|
||||||
<SortMenuItem
|
<SortMenuItem
|
||||||
@@ -80,7 +80,7 @@ function IndexerIndexSortMenu(props) {
|
|||||||
sortDirection={sortDirection}
|
sortDirection={sortDirection}
|
||||||
onPress={onSortSelect}
|
onPress={onSortSelect}
|
||||||
>
|
>
|
||||||
{'Privacy'}
|
{translate('Privacy')}
|
||||||
</SortMenuItem>
|
</SortMenuItem>
|
||||||
</MenuContent>
|
</MenuContent>
|
||||||
</SortMenu>
|
</SortMenu>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ function CapabilitiesLabel(props) {
|
|||||||
let filteredList = categories.filter((item) => item.id < 100000);
|
let filteredList = categories.filter((item) => item.id < 100000);
|
||||||
|
|
||||||
if (categoryFilter.length > 0) {
|
if (categoryFilter.length > 0) {
|
||||||
filteredList = filteredList.filter((item) => categoryFilter.includes(item.id));
|
filteredList = filteredList.filter((item) => categoryFilter.includes(item.id) || (item.subCategories && item.subCategories.some((r) => categoryFilter.includes(r.id))));
|
||||||
}
|
}
|
||||||
|
|
||||||
const nameList = filteredList.map((item) => item.name).sort();
|
const nameList = filteredList.map((item) => item.name).sort();
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ class IndexerIndexRow extends Component {
|
|||||||
privacy,
|
privacy,
|
||||||
priority,
|
priority,
|
||||||
status,
|
status,
|
||||||
|
fields,
|
||||||
appProfile,
|
appProfile,
|
||||||
added,
|
added,
|
||||||
capabilities,
|
capabilities,
|
||||||
@@ -96,6 +97,8 @@ class IndexerIndexRow extends Component {
|
|||||||
isIndexerInfoModalOpen
|
isIndexerInfoModalOpen
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
|
const baseUrl = fields.find((field) => field.name === 'baseUrl')?.value ?? (Array.isArray(indexerUrls) ? indexerUrls[0] : undefined);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{
|
{
|
||||||
@@ -245,12 +248,12 @@ class IndexerIndexRow extends Component {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{
|
{
|
||||||
indexerUrls ?
|
baseUrl ?
|
||||||
<IconButton
|
<IconButton
|
||||||
className={styles.externalLink}
|
className={styles.externalLink}
|
||||||
name={icons.EXTERNAL_LINK}
|
name={icons.EXTERNAL_LINK}
|
||||||
title={translate('Website')}
|
title={translate('Website')}
|
||||||
to={indexerUrls[0].replace('api.', '')}
|
to={baseUrl.replace('api.', '')}
|
||||||
/> : null
|
/> : null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,6 +302,7 @@ IndexerIndexRow.propTypes = {
|
|||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
enable: PropTypes.bool.isRequired,
|
enable: PropTypes.bool.isRequired,
|
||||||
redirect: PropTypes.bool.isRequired,
|
redirect: PropTypes.bool.isRequired,
|
||||||
|
fields: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
appProfile: PropTypes.object.isRequired,
|
appProfile: PropTypes.object.isRequired,
|
||||||
status: PropTypes.object,
|
status: PropTypes.object,
|
||||||
capabilities: PropTypes.object,
|
capabilities: PropTypes.object,
|
||||||
|
|||||||
@@ -20,11 +20,14 @@ function IndexerInfoModalContent(props) {
|
|||||||
encoding,
|
encoding,
|
||||||
language,
|
language,
|
||||||
indexerUrls,
|
indexerUrls,
|
||||||
|
fields,
|
||||||
protocol,
|
protocol,
|
||||||
capabilities,
|
capabilities,
|
||||||
onModalClose
|
onModalClose
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
const baseUrl = fields.find((field) => field.name === 'baseUrl')?.value ?? indexerUrls[0];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
@@ -57,7 +60,7 @@ function IndexerInfoModalContent(props) {
|
|||||||
/>
|
/>
|
||||||
<DescriptionListItemTitle>{translate('IndexerSite')}</DescriptionListItemTitle>
|
<DescriptionListItemTitle>{translate('IndexerSite')}</DescriptionListItemTitle>
|
||||||
<DescriptionListItemDescription>
|
<DescriptionListItemDescription>
|
||||||
<Link to={indexerUrls[0]}>{indexerUrls[0]}</Link>
|
<Link to={baseUrl}>{baseUrl}</Link>
|
||||||
</DescriptionListItemDescription>
|
</DescriptionListItemDescription>
|
||||||
<DescriptionListItemTitle>{`${protocol === 'usenet' ? 'Newznab' : 'Torznab'} Url`}</DescriptionListItemTitle>
|
<DescriptionListItemTitle>{`${protocol === 'usenet' ? 'Newznab' : 'Torznab'} Url`}</DescriptionListItemTitle>
|
||||||
<DescriptionListItemDescription>
|
<DescriptionListItemDescription>
|
||||||
@@ -114,6 +117,7 @@ IndexerInfoModalContent.propTypes = {
|
|||||||
encoding: PropTypes.string.isRequired,
|
encoding: PropTypes.string.isRequired,
|
||||||
language: PropTypes.string.isRequired,
|
language: PropTypes.string.isRequired,
|
||||||
indexerUrls: PropTypes.arrayOf(PropTypes.string).isRequired,
|
indexerUrls: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||||
|
fields: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
protocol: PropTypes.string.isRequired,
|
protocol: PropTypes.string.isRequired,
|
||||||
capabilities: PropTypes.object.isRequired,
|
capabilities: PropTypes.object.isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
$hoverScale: 1.05;
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 0;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--cardBackgroundColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.titleRow {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
height: 38px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indexerRow {
|
||||||
|
color: var(--disabledColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoRow {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
overflow: hidden;
|
||||||
|
width: 85%;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import TextTruncate from 'react-text-truncate';
|
||||||
|
import Label from 'Components/Label';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||||
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
|
import CategoryLabel from 'Search/Table/CategoryLabel';
|
||||||
|
import Peers from 'Search/Table/Peers';
|
||||||
|
import ProtocolLabel from 'Search/Table/ProtocolLabel';
|
||||||
|
import dimensions from 'Styles/Variables/dimensions';
|
||||||
|
import formatAge from 'Utilities/Number/formatAge';
|
||||||
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './SearchIndexOverview.css';
|
||||||
|
|
||||||
|
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
|
||||||
|
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
|
||||||
|
|
||||||
|
function getContentHeight(rowHeight, isSmallScreen) {
|
||||||
|
const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding;
|
||||||
|
|
||||||
|
return rowHeight - padding;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDownloadIcon(isGrabbing, isGrabbed, grabError) {
|
||||||
|
if (isGrabbing) {
|
||||||
|
return icons.SPINNER;
|
||||||
|
} else if (isGrabbed) {
|
||||||
|
return icons.DOWNLOADING;
|
||||||
|
} else if (grabError) {
|
||||||
|
return icons.DOWNLOADING;
|
||||||
|
}
|
||||||
|
|
||||||
|
return icons.DOWNLOAD;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDownloadTooltip(isGrabbing, isGrabbed, grabError) {
|
||||||
|
if (isGrabbing) {
|
||||||
|
return '';
|
||||||
|
} else if (isGrabbed) {
|
||||||
|
return translate('AddedToDownloadClient');
|
||||||
|
} else if (grabError) {
|
||||||
|
return grabError;
|
||||||
|
}
|
||||||
|
|
||||||
|
return translate('AddToDownloadClient');
|
||||||
|
}
|
||||||
|
|
||||||
|
class SearchIndexOverview extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onGrabPress = () => {
|
||||||
|
const {
|
||||||
|
guid,
|
||||||
|
indexerId,
|
||||||
|
onGrabPress
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
onGrabPress({
|
||||||
|
guid,
|
||||||
|
indexerId
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
infoUrl,
|
||||||
|
protocol,
|
||||||
|
downloadUrl,
|
||||||
|
categories,
|
||||||
|
seeders,
|
||||||
|
leechers,
|
||||||
|
size,
|
||||||
|
age,
|
||||||
|
ageHours,
|
||||||
|
ageMinutes,
|
||||||
|
indexer,
|
||||||
|
rowHeight,
|
||||||
|
isSmallScreen,
|
||||||
|
isGrabbed,
|
||||||
|
isGrabbing,
|
||||||
|
grabError
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const contentHeight = getContentHeight(rowHeight, isSmallScreen);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.content}>
|
||||||
|
<div className={styles.info} style={{ height: contentHeight }}>
|
||||||
|
<div className={styles.titleRow}>
|
||||||
|
<div className={styles.title}>
|
||||||
|
<Link
|
||||||
|
to={infoUrl}
|
||||||
|
title={title}
|
||||||
|
>
|
||||||
|
<TextTruncate
|
||||||
|
line={2}
|
||||||
|
text={title}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.actions}>
|
||||||
|
<SpinnerIconButton
|
||||||
|
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
|
||||||
|
kind={grabError ? kinds.DANGER : kinds.DEFAULT}
|
||||||
|
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
|
||||||
|
isDisabled={isGrabbed}
|
||||||
|
isSpinning={isGrabbing}
|
||||||
|
onPress={this.onGrabPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
className={styles.downloadLink}
|
||||||
|
name={icons.SAVE}
|
||||||
|
title={translate('Save')}
|
||||||
|
to={downloadUrl}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.indexerRow}>
|
||||||
|
{indexer}
|
||||||
|
</div>
|
||||||
|
<div className={styles.infoRow}>
|
||||||
|
<ProtocolLabel
|
||||||
|
protocol={protocol}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{
|
||||||
|
protocol === 'torrent' &&
|
||||||
|
<Peers
|
||||||
|
seeders={seeders}
|
||||||
|
leechers={leechers}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
<Label>
|
||||||
|
{formatBytes(size)}
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<Label>
|
||||||
|
{formatAge(age, ageHours, ageMinutes)}
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<CategoryLabel
|
||||||
|
categories={categories}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchIndexOverview.propTypes = {
|
||||||
|
guid: PropTypes.string.isRequired,
|
||||||
|
categories: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
protocol: PropTypes.string.isRequired,
|
||||||
|
age: PropTypes.number.isRequired,
|
||||||
|
ageHours: PropTypes.number.isRequired,
|
||||||
|
ageMinutes: PropTypes.number.isRequired,
|
||||||
|
publishDate: PropTypes.string.isRequired,
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
infoUrl: PropTypes.string.isRequired,
|
||||||
|
downloadUrl: PropTypes.string.isRequired,
|
||||||
|
indexerId: PropTypes.number.isRequired,
|
||||||
|
indexer: PropTypes.string.isRequired,
|
||||||
|
size: PropTypes.number.isRequired,
|
||||||
|
files: PropTypes.number,
|
||||||
|
grabs: PropTypes.number,
|
||||||
|
seeders: PropTypes.number,
|
||||||
|
leechers: PropTypes.number,
|
||||||
|
indexerFlags: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||||
|
rowHeight: PropTypes.number.isRequired,
|
||||||
|
showRelativeDates: PropTypes.bool.isRequired,
|
||||||
|
shortDateFormat: PropTypes.string.isRequired,
|
||||||
|
longDateFormat: PropTypes.string.isRequired,
|
||||||
|
timeFormat: PropTypes.string.isRequired,
|
||||||
|
isSmallScreen: PropTypes.bool.isRequired,
|
||||||
|
onGrabPress: PropTypes.func.isRequired,
|
||||||
|
isGrabbing: PropTypes.bool.isRequired,
|
||||||
|
isGrabbed: PropTypes.bool.isRequired,
|
||||||
|
grabError: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
SearchIndexOverview.defaultProps = {
|
||||||
|
isGrabbing: false,
|
||||||
|
isGrabbed: false
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SearchIndexOverview;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
.grid {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
&:hover {
|
||||||
|
.content {
|
||||||
|
background-color: var(--tableRowHoverBackgroundColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { Grid, WindowScroller } from 'react-virtualized';
|
||||||
|
import Measure from 'Components/Measure';
|
||||||
|
import SearchIndexItemConnector from 'Search/Table/SearchIndexItemConnector';
|
||||||
|
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
|
||||||
|
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
||||||
|
import SearchIndexOverview from './SearchIndexOverview';
|
||||||
|
import styles from './SearchIndexOverviews.css';
|
||||||
|
|
||||||
|
class SearchIndexOverviews extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
width: 0,
|
||||||
|
columnCount: 1,
|
||||||
|
rowHeight: 100,
|
||||||
|
scrollRestored: false
|
||||||
|
};
|
||||||
|
|
||||||
|
this._grid = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps, prevState) {
|
||||||
|
const {
|
||||||
|
items,
|
||||||
|
sortKey,
|
||||||
|
jumpToCharacter,
|
||||||
|
scrollTop,
|
||||||
|
isSmallScreen
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
width,
|
||||||
|
rowHeight,
|
||||||
|
scrollRestored
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
if (prevProps.sortKey !== sortKey) {
|
||||||
|
this.calculateGrid(this.state.width, isSmallScreen);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this._grid &&
|
||||||
|
(prevState.width !== width ||
|
||||||
|
prevState.rowHeight !== rowHeight ||
|
||||||
|
hasDifferentItemsOrOrder(prevProps.items, items, 'guid')
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// recomputeGridSize also forces Grid to discard its cache of rendered cells
|
||||||
|
this._grid.recomputeGridSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._grid && scrollTop !== 0 && !scrollRestored) {
|
||||||
|
this.setState({ scrollRestored: true });
|
||||||
|
this._grid.scrollToPosition({ scrollTop });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) {
|
||||||
|
const index = getIndexOfFirstCharacter(items, jumpToCharacter);
|
||||||
|
|
||||||
|
if (this._grid && index != null) {
|
||||||
|
|
||||||
|
this._grid.scrollToCell({
|
||||||
|
rowIndex: index,
|
||||||
|
columnIndex: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Control
|
||||||
|
|
||||||
|
setGridRef = (ref) => {
|
||||||
|
this._grid = ref;
|
||||||
|
};
|
||||||
|
|
||||||
|
calculateGrid = (width = this.state.width, isSmallScreen) => {
|
||||||
|
const rowHeight = 100;
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
width,
|
||||||
|
rowHeight
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
cellRenderer = ({ key, rowIndex, style }) => {
|
||||||
|
const {
|
||||||
|
items,
|
||||||
|
showRelativeDates,
|
||||||
|
shortDateFormat,
|
||||||
|
longDateFormat,
|
||||||
|
timeFormat,
|
||||||
|
isSmallScreen,
|
||||||
|
onGrabPress
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
rowHeight
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const release = items[rowIndex];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles.container}
|
||||||
|
key={key}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
<SearchIndexItemConnector
|
||||||
|
key={release.guid}
|
||||||
|
component={SearchIndexOverview}
|
||||||
|
rowHeight={rowHeight}
|
||||||
|
showRelativeDates={showRelativeDates}
|
||||||
|
shortDateFormat={shortDateFormat}
|
||||||
|
longDateFormat={longDateFormat}
|
||||||
|
timeFormat={timeFormat}
|
||||||
|
isSmallScreen={isSmallScreen}
|
||||||
|
style={style}
|
||||||
|
guid={release.guid}
|
||||||
|
onGrabPress={onGrabPress}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onMeasure = ({ width }) => {
|
||||||
|
this.calculateGrid(width, this.props.isSmallScreen);
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
items
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
width,
|
||||||
|
rowHeight
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Measure
|
||||||
|
whitelist={['width']}
|
||||||
|
onMeasure={this.onMeasure}
|
||||||
|
>
|
||||||
|
<WindowScroller
|
||||||
|
scrollElement={undefined}
|
||||||
|
>
|
||||||
|
{({ height, registerChild, onChildScroll, scrollTop }) => {
|
||||||
|
if (!height) {
|
||||||
|
return <div />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={registerChild}>
|
||||||
|
<Grid
|
||||||
|
ref={this.setGridRef}
|
||||||
|
className={styles.grid}
|
||||||
|
autoHeight={true}
|
||||||
|
height={height}
|
||||||
|
columnCount={1}
|
||||||
|
columnWidth={width}
|
||||||
|
rowCount={items.length}
|
||||||
|
rowHeight={rowHeight}
|
||||||
|
width={width}
|
||||||
|
onScroll={onChildScroll}
|
||||||
|
scrollTop={scrollTop}
|
||||||
|
overscanRowCount={2}
|
||||||
|
cellRenderer={this.cellRenderer}
|
||||||
|
scrollToAlignment={'start'}
|
||||||
|
isScrollingOptOut={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</WindowScroller>
|
||||||
|
</Measure>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchIndexOverviews.propTypes = {
|
||||||
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
sortKey: PropTypes.string,
|
||||||
|
scrollTop: PropTypes.number.isRequired,
|
||||||
|
jumpToCharacter: PropTypes.string,
|
||||||
|
scroller: PropTypes.instanceOf(Element).isRequired,
|
||||||
|
showRelativeDates: PropTypes.bool.isRequired,
|
||||||
|
shortDateFormat: PropTypes.string.isRequired,
|
||||||
|
longDateFormat: PropTypes.string.isRequired,
|
||||||
|
isSmallScreen: PropTypes.bool.isRequired,
|
||||||
|
timeFormat: PropTypes.string.isRequired,
|
||||||
|
onGrabPress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SearchIndexOverviews;
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { grabRelease } from 'Store/Actions/releaseActions';
|
||||||
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
|
import SearchIndexOverviews from './SearchIndexOverviews';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
createUISettingsSelector(),
|
||||||
|
createDimensionsSelector(),
|
||||||
|
(uiSettings, dimensions) => {
|
||||||
|
return {
|
||||||
|
showRelativeDates: uiSettings.showRelativeDates,
|
||||||
|
shortDateFormat: uiSettings.shortDateFormat,
|
||||||
|
longDateFormat: uiSettings.longDateFormat,
|
||||||
|
timeFormat: uiSettings.timeFormat,
|
||||||
|
isSmallScreen: dimensions.isSmallScreen
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMapDispatchToProps(dispatch, props) {
|
||||||
|
return {
|
||||||
|
onGrabPress(payload) {
|
||||||
|
dispatch(grabRelease(payload));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, createMapDispatchToProps)(SearchIndexOverviews);
|
||||||
@@ -11,8 +11,6 @@ import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
|||||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||||
import { align, icons, sortDirections } from 'Helpers/Props';
|
import { align, icons, sortDirections } from 'Helpers/Props';
|
||||||
import AddIndexerModal from 'Indexer/Add/AddIndexerModal';
|
|
||||||
import EditIndexerModalConnector from 'Indexer/Edit/EditIndexerModalConnector';
|
|
||||||
import NoIndexer from 'Indexer/NoIndexer';
|
import NoIndexer from 'Indexer/NoIndexer';
|
||||||
import * as keyCodes from 'Utilities/Constants/keyCodes';
|
import * as keyCodes from 'Utilities/Constants/keyCodes';
|
||||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||||
@@ -23,12 +21,17 @@ import selectAll from 'Utilities/Table/selectAll';
|
|||||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||||
import SearchIndexFilterMenu from './Menus/SearchIndexFilterMenu';
|
import SearchIndexFilterMenu from './Menus/SearchIndexFilterMenu';
|
||||||
import SearchIndexSortMenu from './Menus/SearchIndexSortMenu';
|
import SearchIndexSortMenu from './Menus/SearchIndexSortMenu';
|
||||||
|
import SearchIndexOverviewsConnector from './Mobile/SearchIndexOverviewsConnector';
|
||||||
import NoSearchResults from './NoSearchResults';
|
import NoSearchResults from './NoSearchResults';
|
||||||
import SearchFooterConnector from './SearchFooterConnector';
|
import SearchFooterConnector from './SearchFooterConnector';
|
||||||
import SearchIndexTableConnector from './Table/SearchIndexTableConnector';
|
import SearchIndexTableConnector from './Table/SearchIndexTableConnector';
|
||||||
import styles from './SearchIndex.css';
|
import styles from './SearchIndex.css';
|
||||||
|
|
||||||
function getViewComponent() {
|
function getViewComponent(isSmallScreen) {
|
||||||
|
if (isSmallScreen) {
|
||||||
|
return SearchIndexOverviewsConnector;
|
||||||
|
}
|
||||||
|
|
||||||
return SearchIndexTableConnector;
|
return SearchIndexTableConnector;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,8 +47,6 @@ class SearchIndex extends Component {
|
|||||||
scroller: null,
|
scroller: null,
|
||||||
jumpBarItems: { order: [] },
|
jumpBarItems: { order: [] },
|
||||||
jumpToCharacter: null,
|
jumpToCharacter: null,
|
||||||
isAddIndexerModalOpen: false,
|
|
||||||
isEditIndexerModalOpen: false,
|
|
||||||
searchType: null,
|
searchType: null,
|
||||||
lastToggled: null,
|
lastToggled: null,
|
||||||
allSelected: false,
|
allSelected: false,
|
||||||
@@ -177,21 +178,6 @@ class SearchIndex extends Component {
|
|||||||
//
|
//
|
||||||
// Listeners
|
// Listeners
|
||||||
|
|
||||||
onAddIndexerPress = () => {
|
|
||||||
this.setState({ isAddIndexerModalOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onAddIndexerModalClose = ({ indexerSelected = false } = {}) => {
|
|
||||||
this.setState({
|
|
||||||
isAddIndexerModalOpen: false,
|
|
||||||
isEditIndexerModalOpen: indexerSelected
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onEditIndexerModalClose = () => {
|
|
||||||
this.setState({ isEditIndexerModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
onJumpBarItemPress = (jumpToCharacter) => {
|
onJumpBarItemPress = (jumpToCharacter) => {
|
||||||
this.setState({ jumpToCharacter });
|
this.setState({ jumpToCharacter });
|
||||||
};
|
};
|
||||||
@@ -253,6 +239,7 @@ class SearchIndex extends Component {
|
|||||||
onScroll,
|
onScroll,
|
||||||
onSortSelect,
|
onSortSelect,
|
||||||
onFilterSelect,
|
onFilterSelect,
|
||||||
|
isSmallScreen,
|
||||||
hasIndexers,
|
hasIndexers,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
@@ -260,8 +247,6 @@ class SearchIndex extends Component {
|
|||||||
const {
|
const {
|
||||||
scroller,
|
scroller,
|
||||||
jumpBarItems,
|
jumpBarItems,
|
||||||
isAddIndexerModalOpen,
|
|
||||||
isEditIndexerModalOpen,
|
|
||||||
jumpToCharacter,
|
jumpToCharacter,
|
||||||
selectedState,
|
selectedState,
|
||||||
allSelected,
|
allSelected,
|
||||||
@@ -270,7 +255,7 @@ class SearchIndex extends Component {
|
|||||||
|
|
||||||
const selectedIndexerIds = this.getSelectedIds();
|
const selectedIndexerIds = this.getSelectedIds();
|
||||||
|
|
||||||
const ViewComponent = getViewComponent();
|
const ViewComponent = getViewComponent(isSmallScreen);
|
||||||
const isLoaded = !!(!error && isPopulated && items.length && scroller);
|
const isLoaded = !!(!error && isPopulated && items.length && scroller);
|
||||||
const hasNoIndexer = !totalItems;
|
const hasNoIndexer = !totalItems;
|
||||||
|
|
||||||
@@ -384,16 +369,6 @@ class SearchIndex extends Component {
|
|||||||
onSearchPress={this.onSearchPress}
|
onSearchPress={this.onSearchPress}
|
||||||
onBulkGrabPress={this.onBulkGrabPress}
|
onBulkGrabPress={this.onBulkGrabPress}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AddIndexerModal
|
|
||||||
isOpen={isAddIndexerModalOpen}
|
|
||||||
onModalClose={this.onAddIndexerModalClose}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<EditIndexerModalConnector
|
|
||||||
isOpen={isEditIndexerModalOpen}
|
|
||||||
onModalClose={this.onEditIndexerModalClose}
|
|
||||||
/>
|
|
||||||
</PageContent>
|
</PageContent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
.category {
|
.category {
|
||||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||||
|
|
||||||
flex: 0 0 110px;
|
flex: 0 0 130px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.age,
|
.age,
|
||||||
|
|||||||
@@ -18,20 +18,20 @@ function createMapStateToProps() {
|
|||||||
return createSelector(
|
return createSelector(
|
||||||
createReleaseSelector(),
|
createReleaseSelector(),
|
||||||
(
|
(
|
||||||
movie
|
release
|
||||||
) => {
|
) => {
|
||||||
|
|
||||||
// If a movie is deleted this selector may fire before the parent
|
// If a release is deleted this selector may fire before the parent
|
||||||
// selecors, which will result in an undefined movie, if that happens
|
// selecors, which will result in an undefined release, if that happens
|
||||||
// we want to return early here and again in the render function to avoid
|
// we want to return early here and again in the render function to avoid
|
||||||
// trying to show a movie that has no information available.
|
// trying to show a release that has no information available.
|
||||||
|
|
||||||
if (!movie) {
|
if (!release) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...movie
|
...release
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -41,7 +41,7 @@ const mapDispatchToProps = {
|
|||||||
dispatchExecuteCommand: executeCommand
|
dispatchExecuteCommand: executeCommand
|
||||||
};
|
};
|
||||||
|
|
||||||
class MovieIndexItemConnector extends Component {
|
class SearchIndexItemConnector extends Component {
|
||||||
|
|
||||||
//
|
//
|
||||||
// Render
|
// Render
|
||||||
@@ -66,9 +66,9 @@ class MovieIndexItemConnector extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MovieIndexItemConnector.propTypes = {
|
SearchIndexItemConnector.propTypes = {
|
||||||
guid: PropTypes.string,
|
guid: PropTypes.string,
|
||||||
component: PropTypes.elementType.isRequired
|
component: PropTypes.elementType.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(MovieIndexItemConnector);
|
export default connect(createMapStateToProps, mapDispatchToProps)(SearchIndexItemConnector);
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
.category {
|
.category {
|
||||||
composes: cell;
|
composes: cell;
|
||||||
|
|
||||||
flex: 0 0 110px;
|
flex: 0 0 130px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.age,
|
.age,
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class AddApplicationModalContent extends Component {
|
|||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
Add Application
|
{translate('AddApplication')}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
|
|||||||
@@ -71,14 +71,14 @@ class Application extends Component {
|
|||||||
{
|
{
|
||||||
syncLevel === 'addOnly' &&
|
syncLevel === 'addOnly' &&
|
||||||
<Label kind={kinds.WARNING}>
|
<Label kind={kinds.WARNING}>
|
||||||
Add and Remove Only
|
{translate('AddRemoveOnly')}
|
||||||
</Label>
|
</Label>
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
syncLevel === 'fullSync' &&
|
syncLevel === 'fullSync' &&
|
||||||
<Label kind={kinds.SUCCESS}>
|
<Label kind={kinds.SUCCESS}>
|
||||||
Full Sync
|
{translate('FullSync')}
|
||||||
</Label>
|
</Label>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,7 +88,7 @@ class Application extends Component {
|
|||||||
kind={kinds.DISABLED}
|
kind={kinds.DISABLED}
|
||||||
outline={true}
|
outline={true}
|
||||||
>
|
>
|
||||||
Disabled
|
{translate('Disabled')}
|
||||||
</Label>
|
</Label>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 AddCategoryModalContentConnector from './AddCategoryModalContentConnector';
|
||||||
|
|
||||||
|
function AddCategoryModal({ isOpen, onModalClose, ...otherProps }) {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
size={sizes.MEDIUM}
|
||||||
|
isOpen={isOpen}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
>
|
||||||
|
<AddCategoryModalContentConnector
|
||||||
|
{...otherProps}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AddCategoryModal.propTypes = {
|
||||||
|
isOpen: PropTypes.bool.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddCategoryModal;
|
||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||||
|
import AddCategoryModal from './AddCategoryModal';
|
||||||
|
|
||||||
|
function createMapDispatchToProps(dispatch, props) {
|
||||||
|
const section = 'settings.downloadClientCategories';
|
||||||
|
|
||||||
|
return {
|
||||||
|
dispatchClearPendingChanges() {
|
||||||
|
dispatch(clearPendingChanges({ section }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class AddCategoryModalConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onModalClose = () => {
|
||||||
|
this.props.dispatchClearPendingChanges();
|
||||||
|
this.props.onModalClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
dispatchClearPendingChanges,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AddCategoryModal
|
||||||
|
{...otherProps}
|
||||||
|
onModalClose={this.onModalClose}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AddCategoryModalConnector.propTypes = {
|
||||||
|
onModalClose: PropTypes.func.isRequired,
|
||||||
|
dispatchClearPendingChanges: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(null, createMapDispatchToProps)(AddCategoryModalConnector);
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
.deleteButton {
|
||||||
|
composes: button from '~Components/Link/Button.css';
|
||||||
|
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
+111
@@ -0,0 +1,111 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import Form from 'Components/Form/Form';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import { inputTypes, kinds } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './AddCategoryModalContent.css';
|
||||||
|
|
||||||
|
function AddCategoryModalContent(props) {
|
||||||
|
const {
|
||||||
|
advancedSettings,
|
||||||
|
item,
|
||||||
|
onInputChange,
|
||||||
|
onFieldChange,
|
||||||
|
onCancelPress,
|
||||||
|
onSavePress,
|
||||||
|
onDeleteSpecificationPress,
|
||||||
|
...otherProps
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
clientCategory,
|
||||||
|
categories
|
||||||
|
} = item;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onCancelPress}>
|
||||||
|
<ModalHeader>
|
||||||
|
{`${id ? 'Edit' : 'Add'} Category`}
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<Form
|
||||||
|
{...otherProps}
|
||||||
|
>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>
|
||||||
|
{translate('DownloadClientCategory')}
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.TEXT}
|
||||||
|
name="clientCategory"
|
||||||
|
{...clientCategory}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>
|
||||||
|
{translate('MappedCategories')}
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CATEGORY_SELECT}
|
||||||
|
name="categories"
|
||||||
|
{...categories}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Form>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
{
|
||||||
|
id &&
|
||||||
|
<Button
|
||||||
|
className={styles.deleteButton}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
onPress={onDeleteSpecificationPress}
|
||||||
|
>
|
||||||
|
{translate('Delete')}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onPress={onCancelPress}
|
||||||
|
>
|
||||||
|
{translate('Cancel')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<SpinnerErrorButton
|
||||||
|
isSpinning={false}
|
||||||
|
onPress={onSavePress}
|
||||||
|
>
|
||||||
|
{translate('Save')}
|
||||||
|
</SpinnerErrorButton>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AddCategoryModalContent.propTypes = {
|
||||||
|
advancedSettings: PropTypes.bool.isRequired,
|
||||||
|
item: PropTypes.object.isRequired,
|
||||||
|
onInputChange: PropTypes.func.isRequired,
|
||||||
|
onFieldChange: PropTypes.func.isRequired,
|
||||||
|
onCancelPress: PropTypes.func.isRequired,
|
||||||
|
onSavePress: PropTypes.func.isRequired,
|
||||||
|
onDeleteSpecificationPress: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddCategoryModalContent;
|
||||||
+78
@@ -0,0 +1,78 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { clearDownloadClientCategoryPending, saveDownloadClientCategory, setDownloadClientCategoryFieldValue, setDownloadClientCategoryValue } from 'Store/Actions/settingsActions';
|
||||||
|
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
||||||
|
import AddCategoryModalContent from './AddCategoryModalContent';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.settings.advancedSettings,
|
||||||
|
createProviderSettingsSelector('downloadClientCategories'),
|
||||||
|
(advancedSettings, specification) => {
|
||||||
|
return {
|
||||||
|
advancedSettings,
|
||||||
|
...specification
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
setDownloadClientCategoryValue,
|
||||||
|
setDownloadClientCategoryFieldValue,
|
||||||
|
saveDownloadClientCategory,
|
||||||
|
clearDownloadClientCategoryPending
|
||||||
|
};
|
||||||
|
|
||||||
|
class AddCategoryModalContentConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onInputChange = ({ name, value }) => {
|
||||||
|
this.props.setDownloadClientCategoryValue({ name, value });
|
||||||
|
};
|
||||||
|
|
||||||
|
onFieldChange = ({ name, value }) => {
|
||||||
|
this.props.setDownloadClientCategoryFieldValue({ name, value });
|
||||||
|
};
|
||||||
|
|
||||||
|
onCancelPress = () => {
|
||||||
|
this.props.clearDownloadClientCategoryPending();
|
||||||
|
this.props.onModalClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
onSavePress = () => {
|
||||||
|
this.props.saveDownloadClientCategory({ id: this.props.id });
|
||||||
|
this.props.onModalClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<AddCategoryModalContent
|
||||||
|
{...this.props}
|
||||||
|
onCancelPress={this.onCancelPress}
|
||||||
|
onSavePress={this.onSavePress}
|
||||||
|
onInputChange={this.onInputChange}
|
||||||
|
onFieldChange={this.onFieldChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AddCategoryModalContentConnector.propTypes = {
|
||||||
|
id: PropTypes.number,
|
||||||
|
item: PropTypes.object.isRequired,
|
||||||
|
setDownloadClientCategoryValue: PropTypes.func.isRequired,
|
||||||
|
setDownloadClientCategoryFieldValue: PropTypes.func.isRequired,
|
||||||
|
clearDownloadClientCategoryPending: PropTypes.func.isRequired,
|
||||||
|
saveDownloadClientCategory: PropTypes.func.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(AddCategoryModalContentConnector);
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
.customFormat {
|
||||||
|
composes: card from '~Components/Card.css';
|
||||||
|
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nameContainer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
@add-mixin truncate;
|
||||||
|
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.labels {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 5px;
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltipLabel {
|
||||||
|
composes: label from '~Components/Label.css';
|
||||||
|
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import Card from 'Components/Card';
|
||||||
|
import Label from 'Components/Label';
|
||||||
|
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||||
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import AddCategoryModalConnector from './AddCategoryModalConnector';
|
||||||
|
import styles from './Category.css';
|
||||||
|
|
||||||
|
class Category extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isEditSpecificationModalOpen: false,
|
||||||
|
isDeleteSpecificationModalOpen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onEditSpecificationPress = () => {
|
||||||
|
this.setState({ isEditSpecificationModalOpen: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onEditSpecificationModalClose = () => {
|
||||||
|
this.setState({ isEditSpecificationModalOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
onDeleteSpecificationPress = () => {
|
||||||
|
this.setState({
|
||||||
|
isEditSpecificationModalOpen: false,
|
||||||
|
isDeleteSpecificationModalOpen: true
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onDeleteSpecificationModalClose = () => {
|
||||||
|
this.setState({ isDeleteSpecificationModalOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
onConfirmDeleteSpecification = () => {
|
||||||
|
this.props.onConfirmDeleteSpecification(this.props.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
clientCategory,
|
||||||
|
categories
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
className={styles.customFormat}
|
||||||
|
overlayContent={true}
|
||||||
|
onPress={this.onEditSpecificationPress}
|
||||||
|
>
|
||||||
|
<div className={styles.nameContainer}>
|
||||||
|
<div className={styles.name}>
|
||||||
|
{clientCategory}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Label kind={kinds.PRIMARY}>
|
||||||
|
{`${categories.length} ${categories.length > 1 ? translate('Categories') : translate('Category')}`}
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<AddCategoryModalConnector
|
||||||
|
id={id}
|
||||||
|
isOpen={this.state.isEditSpecificationModalOpen}
|
||||||
|
onModalClose={this.onEditSpecificationModalClose}
|
||||||
|
onDeleteSpecificationPress={this.onDeleteSpecificationPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={this.state.isDeleteSpecificationModalOpen}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
title={translate('DeleteClientCategory')}
|
||||||
|
message={
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
{translate('AreYouSureYouWantToDeleteCategory', [name])}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
confirmLabel={translate('Delete')}
|
||||||
|
onConfirm={this.onConfirmDeleteSpecification}
|
||||||
|
onCancel={this.onDeleteSpecificationModalClose}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Category.propTypes = {
|
||||||
|
id: PropTypes.number.isRequired,
|
||||||
|
categories: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||||
|
clientCategory: PropTypes.string.isRequired,
|
||||||
|
onConfirmDeleteSpecification: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Category;
|
||||||
+72
-2
@@ -1,11 +1,14 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import Alert from 'Components/Alert';
|
import Alert from 'Components/Alert';
|
||||||
|
import Card from 'Components/Card';
|
||||||
|
import FieldSet from 'Components/FieldSet';
|
||||||
import Form from 'Components/Form/Form';
|
import Form from 'Components/Form/Form';
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
|
import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
@@ -13,12 +16,33 @@ import ModalBody from 'Components/Modal/ModalBody';
|
|||||||
import ModalContent from 'Components/Modal/ModalContent';
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import { inputTypes, kinds } from 'Helpers/Props';
|
import { icons, inputTypes, kinds } from 'Helpers/Props';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
|
import AddCategoryModalConnector from './Categories/AddCategoryModalConnector';
|
||||||
|
import Category from './Categories/Category';
|
||||||
import styles from './EditDownloadClientModalContent.css';
|
import styles from './EditDownloadClientModalContent.css';
|
||||||
|
|
||||||
class EditDownloadClientModalContent extends Component {
|
class EditDownloadClientModalContent extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isAddCategoryModalOpen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onAddCategoryPress = () => {
|
||||||
|
this.setState({ isAddCategoryModalOpen: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onAddCategoryModalClose = () => {
|
||||||
|
this.setState({ isAddCategoryModalOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
// Render
|
// Render
|
||||||
|
|
||||||
@@ -27,6 +51,7 @@ class EditDownloadClientModalContent extends Component {
|
|||||||
advancedSettings,
|
advancedSettings,
|
||||||
isFetching,
|
isFetching,
|
||||||
error,
|
error,
|
||||||
|
categories,
|
||||||
isSaving,
|
isSaving,
|
||||||
isTesting,
|
isTesting,
|
||||||
saveError,
|
saveError,
|
||||||
@@ -37,15 +62,21 @@ class EditDownloadClientModalContent extends Component {
|
|||||||
onSavePress,
|
onSavePress,
|
||||||
onTestPress,
|
onTestPress,
|
||||||
onDeleteDownloadClientPress,
|
onDeleteDownloadClientPress,
|
||||||
|
onConfirmDeleteCategory,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
isAddCategoryModalOpen
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
implementationName,
|
implementationName,
|
||||||
name,
|
name,
|
||||||
enable,
|
enable,
|
||||||
priority,
|
priority,
|
||||||
|
supportsCategories,
|
||||||
fields,
|
fields,
|
||||||
message
|
message
|
||||||
} = item;
|
} = item;
|
||||||
@@ -136,6 +167,43 @@ class EditDownloadClientModalContent extends Component {
|
|||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
{
|
||||||
|
supportsCategories.value ?
|
||||||
|
<FieldSet legend={translate('MappedCategories')}>
|
||||||
|
<div className={styles.customFormats}>
|
||||||
|
{
|
||||||
|
categories.map((tag) => {
|
||||||
|
return (
|
||||||
|
<Category
|
||||||
|
key={tag.id}
|
||||||
|
{...tag}
|
||||||
|
onConfirmDeleteSpecification={onConfirmDeleteCategory}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
<Card
|
||||||
|
className={styles.addCategory}
|
||||||
|
onPress={this.onAddCategoryPress}
|
||||||
|
>
|
||||||
|
<div className={styles.center}>
|
||||||
|
<Icon
|
||||||
|
name={icons.ADD}
|
||||||
|
size={25}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</FieldSet> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
<AddCategoryModalConnector
|
||||||
|
isOpen={isAddCategoryModalOpen}
|
||||||
|
onModalClose={this.onAddCategoryModalClose}
|
||||||
|
/>
|
||||||
|
|
||||||
</Form>
|
</Form>
|
||||||
}
|
}
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
@@ -185,13 +253,15 @@ EditDownloadClientModalContent.propTypes = {
|
|||||||
isSaving: PropTypes.bool.isRequired,
|
isSaving: PropTypes.bool.isRequired,
|
||||||
saveError: PropTypes.object,
|
saveError: PropTypes.object,
|
||||||
isTesting: PropTypes.bool.isRequired,
|
isTesting: PropTypes.bool.isRequired,
|
||||||
|
categories: PropTypes.arrayOf(PropTypes.object),
|
||||||
item: PropTypes.object.isRequired,
|
item: PropTypes.object.isRequired,
|
||||||
onInputChange: PropTypes.func.isRequired,
|
onInputChange: PropTypes.func.isRequired,
|
||||||
onFieldChange: PropTypes.func.isRequired,
|
onFieldChange: PropTypes.func.isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired,
|
onModalClose: PropTypes.func.isRequired,
|
||||||
onSavePress: PropTypes.func.isRequired,
|
onSavePress: PropTypes.func.isRequired,
|
||||||
onTestPress: PropTypes.func.isRequired,
|
onTestPress: PropTypes.func.isRequired,
|
||||||
onDeleteDownloadClientPress: PropTypes.func
|
onDeleteDownloadClientPress: PropTypes.func,
|
||||||
|
onConfirmDeleteCategory: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default EditDownloadClientModalContent;
|
export default EditDownloadClientModalContent;
|
||||||
|
|||||||
+24
-4
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { saveDownloadClient, setDownloadClientFieldValue, setDownloadClientValue, testDownloadClient } from 'Store/Actions/settingsActions';
|
import { deleteDownloadClientCategory, fetchDownloadClientCategories, saveDownloadClient, setDownloadClientFieldValue, setDownloadClientValue, testDownloadClient } from 'Store/Actions/settingsActions';
|
||||||
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
||||||
import EditDownloadClientModalContent from './EditDownloadClientModalContent';
|
import EditDownloadClientModalContent from './EditDownloadClientModalContent';
|
||||||
|
|
||||||
@@ -10,10 +10,12 @@ function createMapStateToProps() {
|
|||||||
return createSelector(
|
return createSelector(
|
||||||
(state) => state.settings.advancedSettings,
|
(state) => state.settings.advancedSettings,
|
||||||
createProviderSettingsSelector('downloadClients'),
|
createProviderSettingsSelector('downloadClients'),
|
||||||
(advancedSettings, downloadClient) => {
|
(state) => state.settings.downloadClientCategories,
|
||||||
|
(advancedSettings, downloadClient, categories) => {
|
||||||
return {
|
return {
|
||||||
advancedSettings,
|
advancedSettings,
|
||||||
...downloadClient
|
...downloadClient,
|
||||||
|
categories: categories.items
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -23,7 +25,9 @@ const mapDispatchToProps = {
|
|||||||
setDownloadClientValue,
|
setDownloadClientValue,
|
||||||
setDownloadClientFieldValue,
|
setDownloadClientFieldValue,
|
||||||
saveDownloadClient,
|
saveDownloadClient,
|
||||||
testDownloadClient
|
testDownloadClient,
|
||||||
|
fetchDownloadClientCategories,
|
||||||
|
deleteDownloadClientCategory
|
||||||
};
|
};
|
||||||
|
|
||||||
class EditDownloadClientModalContentConnector extends Component {
|
class EditDownloadClientModalContentConnector extends Component {
|
||||||
@@ -31,6 +35,14 @@ class EditDownloadClientModalContentConnector extends Component {
|
|||||||
//
|
//
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
tagsFromId
|
||||||
|
} = this.props;
|
||||||
|
this.props.fetchDownloadClientCategories({ id: tagsFromId || id });
|
||||||
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
componentDidUpdate(prevProps, prevState) {
|
||||||
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
|
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
|
||||||
this.props.onModalClose();
|
this.props.onModalClose();
|
||||||
@@ -56,6 +68,10 @@ class EditDownloadClientModalContentConnector extends Component {
|
|||||||
this.props.testDownloadClient({ id: this.props.id });
|
this.props.testDownloadClient({ id: this.props.id });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onConfirmDeleteCategory = (id) => {
|
||||||
|
this.props.deleteDownloadClientCategory({ id });
|
||||||
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
// Render
|
// Render
|
||||||
|
|
||||||
@@ -67,6 +83,7 @@ class EditDownloadClientModalContentConnector extends Component {
|
|||||||
onTestPress={this.onTestPress}
|
onTestPress={this.onTestPress}
|
||||||
onInputChange={this.onInputChange}
|
onInputChange={this.onInputChange}
|
||||||
onFieldChange={this.onFieldChange}
|
onFieldChange={this.onFieldChange}
|
||||||
|
onConfirmDeleteCategory={this.onConfirmDeleteCategory}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -74,10 +91,13 @@ class EditDownloadClientModalContentConnector extends Component {
|
|||||||
|
|
||||||
EditDownloadClientModalContentConnector.propTypes = {
|
EditDownloadClientModalContentConnector.propTypes = {
|
||||||
id: PropTypes.number,
|
id: PropTypes.number,
|
||||||
|
tagsFromId: PropTypes.number,
|
||||||
isFetching: PropTypes.bool.isRequired,
|
isFetching: PropTypes.bool.isRequired,
|
||||||
isSaving: PropTypes.bool.isRequired,
|
isSaving: PropTypes.bool.isRequired,
|
||||||
saveError: PropTypes.object,
|
saveError: PropTypes.object,
|
||||||
item: PropTypes.object.isRequired,
|
item: PropTypes.object.isRequired,
|
||||||
|
fetchDownloadClientCategories: PropTypes.func.isRequired,
|
||||||
|
deleteDownloadClientCategory: PropTypes.func.isRequired,
|
||||||
setDownloadClientValue: PropTypes.func.isRequired,
|
setDownloadClientValue: PropTypes.func.isRequired,
|
||||||
setDownloadClientFieldValue: PropTypes.func.isRequired,
|
setDownloadClientFieldValue: PropTypes.func.isRequired,
|
||||||
saveDownloadClient: PropTypes.func.isRequired,
|
saveDownloadClient: PropTypes.func.isRequired,
|
||||||
|
|||||||
@@ -11,12 +11,20 @@ import ConfirmModal from 'Components/Modal/ConfirmModal';
|
|||||||
import { icons, inputTypes, kinds } from 'Helpers/Props';
|
import { icons, inputTypes, kinds } from 'Helpers/Props';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
const authenticationMethodOptions = [
|
export const authenticationRequiredWarning = translate('AuthenticationRequiredWarning');
|
||||||
{ key: 'none', value: 'None' },
|
|
||||||
|
export const authenticationMethodOptions = [
|
||||||
|
{ key: 'none', value: 'None', isDisabled: true },
|
||||||
|
{ key: 'external', value: 'External', isHidden: true },
|
||||||
{ key: 'basic', value: 'Basic (Browser Popup)' },
|
{ key: 'basic', value: 'Basic (Browser Popup)' },
|
||||||
{ key: 'forms', value: 'Forms (Login Page)' }
|
{ key: 'forms', value: 'Forms (Login Page)' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const authenticationRequiredOptions = [
|
||||||
|
{ key: 'enabled', value: 'Enabled' },
|
||||||
|
{ key: 'disabledForLocalAddresses', value: 'Disabled for Local Addresses' }
|
||||||
|
];
|
||||||
|
|
||||||
const certificateValidationOptions = [
|
const certificateValidationOptions = [
|
||||||
{ key: 'enabled', value: 'Enabled' },
|
{ key: 'enabled', value: 'Enabled' },
|
||||||
{ key: 'disabledForLocalAddresses', value: 'Disabled for Local Addresses' },
|
{ key: 'disabledForLocalAddresses', value: 'Disabled for Local Addresses' },
|
||||||
@@ -68,6 +76,7 @@ class SecuritySettings extends Component {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
authenticationMethod,
|
authenticationMethod,
|
||||||
|
authenticationRequired,
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
apiKey,
|
apiKey,
|
||||||
@@ -86,13 +95,31 @@ class SecuritySettings extends Component {
|
|||||||
name="authenticationMethod"
|
name="authenticationMethod"
|
||||||
values={authenticationMethodOptions}
|
values={authenticationMethodOptions}
|
||||||
helpText={translate('AuthenticationMethodHelpText')}
|
helpText={translate('AuthenticationMethodHelpText')}
|
||||||
|
helpTextWarning={authenticationRequiredWarning}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...authenticationMethod}
|
{...authenticationMethod}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
{
|
{
|
||||||
authenticationEnabled &&
|
authenticationEnabled ?
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('AuthenticationRequired')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="authenticationRequired"
|
||||||
|
values={authenticationRequiredOptions}
|
||||||
|
helpText={translate('AuthenticationRequiredHelpText')}
|
||||||
|
onChange={onInputChange}
|
||||||
|
{...authenticationRequired}
|
||||||
|
/>
|
||||||
|
</FormGroup> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
authenticationEnabled ?
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>{translate('Username')}</FormLabel>
|
<FormLabel>{translate('Username')}</FormLabel>
|
||||||
|
|
||||||
@@ -102,11 +129,12 @@ class SecuritySettings extends Component {
|
|||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...username}
|
{...username}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
authenticationEnabled &&
|
authenticationEnabled ?
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>{translate('Password')}</FormLabel>
|
<FormLabel>{translate('Password')}</FormLabel>
|
||||||
|
|
||||||
@@ -116,7 +144,8 @@ class SecuritySettings extends Component {
|
|||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...password}
|
{...password}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ class IndexerProxy extends Component {
|
|||||||
kind={kinds.DISABLED}
|
kind={kinds.DISABLED}
|
||||||
outline={true}
|
outline={true}
|
||||||
>
|
>
|
||||||
Disabled
|
{translate('Disabled')}
|
||||||
</Label> :
|
</Label> :
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ function createLanguagesSelector() {
|
|||||||
return createSelector(
|
return createSelector(
|
||||||
(state) => state.localization,
|
(state) => state.localization,
|
||||||
(localization) => {
|
(localization) => {
|
||||||
console.log(localization);
|
|
||||||
|
|
||||||
const items = localization.items;
|
const items = localization.items;
|
||||||
|
|
||||||
if (!items) {
|
if (!items) {
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
import { createAction } from 'redux-actions';
|
||||||
|
import { batchActions } from 'redux-batched-actions';
|
||||||
|
import createClearReducer from 'Store/Actions/Creators/Reducers/createClearReducer';
|
||||||
|
import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
|
||||||
|
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
||||||
|
import { createThunk } from 'Store/thunks';
|
||||||
|
import getNextId from 'Utilities/State/getNextId';
|
||||||
|
import getProviderState from 'Utilities/State/getProviderState';
|
||||||
|
import getSectionState from 'Utilities/State/getSectionState';
|
||||||
|
import selectProviderSchema from 'Utilities/State/selectProviderSchema';
|
||||||
|
import { removeItem, set, update, updateItem } from '../baseActions';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Variables
|
||||||
|
|
||||||
|
const section = 'settings.downloadClientCategories';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Actions Types
|
||||||
|
|
||||||
|
export const FETCH_DOWNLOAD_CLIENT_CATEGORIES = 'settings/downloadClientCategories/fetchDownloadClientCategories';
|
||||||
|
export const FETCH_DOWNLOAD_CLIENT_CATEGORY_SCHEMA = 'settings/downloadClientCategories/fetchDownloadClientCategorySchema';
|
||||||
|
export const SELECT_DOWNLOAD_CLIENT_CATEGORY_SCHEMA = 'settings/downloadClientCategories/selectDownloadClientCategorySchema';
|
||||||
|
export const SET_DOWNLOAD_CLIENT_CATEGORY_VALUE = 'settings/downloadClientCategories/setDownloadClientCategoryValue';
|
||||||
|
export const SET_DOWNLOAD_CLIENT_CATEGORY_FIELD_VALUE = 'settings/downloadClientCategories/setDownloadClientCategoryFieldValue';
|
||||||
|
export const SAVE_DOWNLOAD_CLIENT_CATEGORY = 'settings/downloadClientCategories/saveDownloadClientCategory';
|
||||||
|
export const DELETE_DOWNLOAD_CLIENT_CATEGORY = 'settings/downloadClientCategories/deleteDownloadClientCategory';
|
||||||
|
export const DELETE_ALL_DOWNLOAD_CLIENT_CATEGORY = 'settings/downloadClientCategories/deleteAllDownloadClientCategory';
|
||||||
|
export const CLEAR_DOWNLOAD_CLIENT_CATEGORIES = 'settings/downloadClientCategories/clearDownloadClientCategories';
|
||||||
|
export const CLEAR_DOWNLOAD_CLIENT_CATEGORY_PENDING = 'settings/downloadClientCategories/clearDownloadClientCategoryPending';
|
||||||
|
//
|
||||||
|
// Action Creators
|
||||||
|
|
||||||
|
export const fetchDownloadClientCategories = createThunk(FETCH_DOWNLOAD_CLIENT_CATEGORIES);
|
||||||
|
export const fetchDownloadClientCategorySchema = createThunk(FETCH_DOWNLOAD_CLIENT_CATEGORY_SCHEMA);
|
||||||
|
export const selectDownloadClientCategorySchema = createAction(SELECT_DOWNLOAD_CLIENT_CATEGORY_SCHEMA);
|
||||||
|
|
||||||
|
export const saveDownloadClientCategory = createThunk(SAVE_DOWNLOAD_CLIENT_CATEGORY);
|
||||||
|
export const deleteDownloadClientCategory = createThunk(DELETE_DOWNLOAD_CLIENT_CATEGORY);
|
||||||
|
export const deleteAllDownloadClientCategory = createThunk(DELETE_ALL_DOWNLOAD_CLIENT_CATEGORY);
|
||||||
|
|
||||||
|
export const setDownloadClientCategoryValue = createAction(SET_DOWNLOAD_CLIENT_CATEGORY_VALUE, (payload) => {
|
||||||
|
return {
|
||||||
|
section,
|
||||||
|
...payload
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export const setDownloadClientCategoryFieldValue = createAction(SET_DOWNLOAD_CLIENT_CATEGORY_FIELD_VALUE, (payload) => {
|
||||||
|
return {
|
||||||
|
section,
|
||||||
|
...payload
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export const clearDownloadClientCategory = createAction(CLEAR_DOWNLOAD_CLIENT_CATEGORIES);
|
||||||
|
|
||||||
|
export const clearDownloadClientCategoryPending = createThunk(CLEAR_DOWNLOAD_CLIENT_CATEGORY_PENDING);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Details
|
||||||
|
|
||||||
|
export default {
|
||||||
|
|
||||||
|
//
|
||||||
|
// State
|
||||||
|
|
||||||
|
defaultState: {
|
||||||
|
isPopulated: false,
|
||||||
|
error: null,
|
||||||
|
isSchemaFetching: false,
|
||||||
|
isSchemaPopulated: false,
|
||||||
|
schemaError: null,
|
||||||
|
schema: [],
|
||||||
|
selectedSchema: {},
|
||||||
|
isSaving: false,
|
||||||
|
saveError: null,
|
||||||
|
items: [],
|
||||||
|
pendingChanges: {}
|
||||||
|
},
|
||||||
|
|
||||||
|
//
|
||||||
|
// Action Handlers
|
||||||
|
|
||||||
|
actionHandlers: {
|
||||||
|
[FETCH_DOWNLOAD_CLIENT_CATEGORIES]: (getState, payload, dispatch) => {
|
||||||
|
let tags = [];
|
||||||
|
if (payload.id) {
|
||||||
|
const cfState = getSectionState(getState(), 'settings.downloadClients', true);
|
||||||
|
const cf = cfState.items[cfState.itemMap[payload.id]];
|
||||||
|
tags = cf.categories.map((tag, i) => {
|
||||||
|
return {
|
||||||
|
id: i + 1,
|
||||||
|
...tag
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(batchActions([
|
||||||
|
update({ section, data: tags }),
|
||||||
|
set({
|
||||||
|
section,
|
||||||
|
isPopulated: true
|
||||||
|
})
|
||||||
|
]));
|
||||||
|
},
|
||||||
|
|
||||||
|
[SAVE_DOWNLOAD_CLIENT_CATEGORY]: (getState, payload, dispatch) => {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
...otherPayload
|
||||||
|
} = payload;
|
||||||
|
|
||||||
|
const saveData = getProviderState({ id, ...otherPayload }, getState, section, false);
|
||||||
|
|
||||||
|
// we have to set id since not actually posting to server yet
|
||||||
|
if (!saveData.id) {
|
||||||
|
saveData.id = getNextId(getState().settings.downloadClientCategories.items);
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(batchActions([
|
||||||
|
updateItem({ section, ...saveData }),
|
||||||
|
set({
|
||||||
|
section,
|
||||||
|
pendingChanges: {}
|
||||||
|
})
|
||||||
|
]));
|
||||||
|
},
|
||||||
|
|
||||||
|
[DELETE_DOWNLOAD_CLIENT_CATEGORY]: (getState, payload, dispatch) => {
|
||||||
|
const id = payload.id;
|
||||||
|
return dispatch(removeItem({ section, id }));
|
||||||
|
},
|
||||||
|
|
||||||
|
[DELETE_ALL_DOWNLOAD_CLIENT_CATEGORY]: (getState, payload, dispatch) => {
|
||||||
|
return dispatch(set({
|
||||||
|
section,
|
||||||
|
items: []
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
[CLEAR_DOWNLOAD_CLIENT_CATEGORY_PENDING]: (getState, payload, dispatch) => {
|
||||||
|
return dispatch(set({
|
||||||
|
section,
|
||||||
|
pendingChanges: {}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
//
|
||||||
|
// Reducers
|
||||||
|
|
||||||
|
reducers: {
|
||||||
|
[SET_DOWNLOAD_CLIENT_CATEGORY_VALUE]: createSetSettingValueReducer(section),
|
||||||
|
[SET_DOWNLOAD_CLIENT_CATEGORY_FIELD_VALUE]: createSetProviderFieldValueReducer(section),
|
||||||
|
|
||||||
|
[SELECT_DOWNLOAD_CLIENT_CATEGORY_SCHEMA]: (state, { payload }) => {
|
||||||
|
return selectProviderSchema(state, section, payload, (selectedSchema) => {
|
||||||
|
return selectedSchema;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
[CLEAR_DOWNLOAD_CLIENT_CATEGORIES]: createClearReducer(section, {
|
||||||
|
isPopulated: false,
|
||||||
|
error: null,
|
||||||
|
items: []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -9,6 +9,7 @@ import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/
|
|||||||
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
||||||
import { createThunk } from 'Store/thunks';
|
import { createThunk } from 'Store/thunks';
|
||||||
import selectProviderSchema from 'Utilities/State/selectProviderSchema';
|
import selectProviderSchema from 'Utilities/State/selectProviderSchema';
|
||||||
|
import { set } from '../baseActions';
|
||||||
|
|
||||||
//
|
//
|
||||||
// Variables
|
// Variables
|
||||||
@@ -90,10 +91,34 @@ export default {
|
|||||||
[FETCH_DOWNLOAD_CLIENTS]: createFetchHandler(section, '/downloadclient'),
|
[FETCH_DOWNLOAD_CLIENTS]: createFetchHandler(section, '/downloadclient'),
|
||||||
[FETCH_DOWNLOAD_CLIENT_SCHEMA]: createFetchSchemaHandler(section, '/downloadclient/schema'),
|
[FETCH_DOWNLOAD_CLIENT_SCHEMA]: createFetchSchemaHandler(section, '/downloadclient/schema'),
|
||||||
|
|
||||||
[SAVE_DOWNLOAD_CLIENT]: createSaveProviderHandler(section, '/downloadclient'),
|
[SAVE_DOWNLOAD_CLIENT]: (getState, payload, dispatch) => {
|
||||||
|
// move the format tags in as a pending change
|
||||||
|
const state = getState();
|
||||||
|
const pendingChanges = state.settings.downloadClients.pendingChanges;
|
||||||
|
pendingChanges.categories = state.settings.downloadClientCategories.items;
|
||||||
|
dispatch(set({
|
||||||
|
section,
|
||||||
|
pendingChanges
|
||||||
|
}));
|
||||||
|
|
||||||
|
createSaveProviderHandler(section, '/downloadclient')(getState, payload, dispatch);
|
||||||
|
},
|
||||||
|
|
||||||
[CANCEL_SAVE_DOWNLOAD_CLIENT]: createCancelSaveProviderHandler(section),
|
[CANCEL_SAVE_DOWNLOAD_CLIENT]: createCancelSaveProviderHandler(section),
|
||||||
[DELETE_DOWNLOAD_CLIENT]: createRemoveItemHandler(section, '/downloadclient'),
|
[DELETE_DOWNLOAD_CLIENT]: createRemoveItemHandler(section, '/downloadclient'),
|
||||||
[TEST_DOWNLOAD_CLIENT]: createTestProviderHandler(section, '/downloadclient'),
|
|
||||||
|
[TEST_DOWNLOAD_CLIENT]: (getState, payload, dispatch) => {
|
||||||
|
const state = getState();
|
||||||
|
const pendingChanges = state.settings.downloadClients.pendingChanges;
|
||||||
|
pendingChanges.categories = state.settings.downloadClientCategories.items;
|
||||||
|
dispatch(set({
|
||||||
|
section,
|
||||||
|
pendingChanges
|
||||||
|
}));
|
||||||
|
|
||||||
|
createTestProviderHandler(section, '/downloadclient')(getState, payload, dispatch);
|
||||||
|
},
|
||||||
|
|
||||||
[CANCEL_TEST_DOWNLOAD_CLIENT]: createCancelTestProviderHandler(section),
|
[CANCEL_TEST_DOWNLOAD_CLIENT]: createCancelTestProviderHandler(section),
|
||||||
[TEST_ALL_DOWNLOAD_CLIENTS]: createTestAllProvidersHandler(section, '/downloadclient')
|
[TEST_ALL_DOWNLOAD_CLIENTS]: createTestAllProvidersHandler(section, '/downloadclient')
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export const defaultState = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'grabTitle',
|
name: 'grabTitle',
|
||||||
label: translate('Grab Title'),
|
label: translate('GrabTitle'),
|
||||||
isSortable: false,
|
isSortable: false,
|
||||||
isVisible: false
|
isVisible: false
|
||||||
},
|
},
|
||||||
@@ -78,7 +78,7 @@ export const defaultState = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'elapsedTime',
|
name: 'elapsedTime',
|
||||||
label: translate('Elapsed Time'),
|
label: translate('ElapsedTime'),
|
||||||
isSortable: false,
|
isSortable: false,
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -310,8 +310,6 @@ export const actionHandlers = handleThunks({
|
|||||||
isGrabbing: true
|
isGrabbing: true
|
||||||
}));
|
}));
|
||||||
|
|
||||||
console.log(payload);
|
|
||||||
|
|
||||||
const promise = createAjaxRequest({
|
const promise = createAjaxRequest({
|
||||||
url: '/search/bulk',
|
url: '/search/bulk',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import createHandleActions from './Creators/createHandleActions';
|
|||||||
import applications from './Settings/applications';
|
import applications from './Settings/applications';
|
||||||
import appProfiles from './Settings/appProfiles';
|
import appProfiles from './Settings/appProfiles';
|
||||||
import development from './Settings/development';
|
import development from './Settings/development';
|
||||||
|
import downloadClientCategories from './Settings/downloadClientCategories';
|
||||||
import downloadClients from './Settings/downloadClients';
|
import downloadClients from './Settings/downloadClients';
|
||||||
import general from './Settings/general';
|
import general from './Settings/general';
|
||||||
import indexerCategories from './Settings/indexerCategories';
|
import indexerCategories from './Settings/indexerCategories';
|
||||||
@@ -11,6 +12,7 @@ import indexerProxies from './Settings/indexerProxies';
|
|||||||
import notifications from './Settings/notifications';
|
import notifications from './Settings/notifications';
|
||||||
import ui from './Settings/ui';
|
import ui from './Settings/ui';
|
||||||
|
|
||||||
|
export * from './Settings/downloadClientCategories';
|
||||||
export * from './Settings/downloadClients';
|
export * from './Settings/downloadClients';
|
||||||
export * from './Settings/general';
|
export * from './Settings/general';
|
||||||
export * from './Settings/indexerCategories';
|
export * from './Settings/indexerCategories';
|
||||||
@@ -32,6 +34,7 @@ export const section = 'settings';
|
|||||||
export const defaultState = {
|
export const defaultState = {
|
||||||
advancedSettings: false,
|
advancedSettings: false,
|
||||||
|
|
||||||
|
downloadClientCategories: downloadClientCategories.defaultState,
|
||||||
downloadClients: downloadClients.defaultState,
|
downloadClients: downloadClients.defaultState,
|
||||||
general: general.defaultState,
|
general: general.defaultState,
|
||||||
indexerCategories: indexerCategories.defaultState,
|
indexerCategories: indexerCategories.defaultState,
|
||||||
@@ -61,6 +64,7 @@ export const toggleAdvancedSettings = createAction(TOGGLE_ADVANCED_SETTINGS);
|
|||||||
// Action Handlers
|
// Action Handlers
|
||||||
|
|
||||||
export const actionHandlers = handleThunks({
|
export const actionHandlers = handleThunks({
|
||||||
|
...downloadClientCategories.actionHandlers,
|
||||||
...downloadClients.actionHandlers,
|
...downloadClients.actionHandlers,
|
||||||
...general.actionHandlers,
|
...general.actionHandlers,
|
||||||
...indexerCategories.actionHandlers,
|
...indexerCategories.actionHandlers,
|
||||||
@@ -81,6 +85,7 @@ export const reducers = createHandleActions({
|
|||||||
return Object.assign({}, state, { advancedSettings: !state.advancedSettings });
|
return Object.assign({}, state, { advancedSettings: !state.advancedSettings });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
...downloadClientCategories.reducers,
|
||||||
...downloadClients.reducers,
|
...downloadClients.reducers,
|
||||||
...general.reducers,
|
...general.reducers,
|
||||||
...indexerCategories.reducers,
|
...indexerCategories.reducers,
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import * as dark from './dark';
|
import * as dark from './dark';
|
||||||
import * as light from './light';
|
import * as light from './light';
|
||||||
|
|
||||||
|
const defaultDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
const auto = defaultDark ? { ...dark } : { ...light };
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
auto,
|
||||||
light,
|
light,
|
||||||
dark
|
dark
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const columns = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'size',
|
name: 'size',
|
||||||
label: 'Size',
|
label: translate('Size'),
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ class Updates extends Component {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={styles.message}>
|
<div className={styles.message}>
|
||||||
The latest version of Prowlarr is already installed
|
{translate('TheLatestVersionIsAlreadyInstalled', ['Prowlarr'])}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import filesize from 'filesize';
|
import { filesize } from 'filesize';
|
||||||
|
|
||||||
function formatBytes(input) {
|
function formatBytes(input) {
|
||||||
const size = Number(input);
|
const size = Number(input);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<!-- Windows Phone -->
|
<!-- Windows Phone -->
|
||||||
<meta name="msapplication-navbutton-color" content="#3a3f51" />
|
<meta name="msapplication-navbutton-color" content="#3a3f51" />
|
||||||
|
|
||||||
<meta name="description" content="Prowlarr (Preview)" />
|
<meta name="description" content="Prowlarr" />
|
||||||
|
|
||||||
<link
|
<link
|
||||||
rel="apple-touch-icon"
|
rel="apple-touch-icon"
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
<link rel="stylesheet" type="text/css" href="/Content/Fonts/fonts.css">
|
<link rel="stylesheet" type="text/css" href="/Content/Fonts/fonts.css">
|
||||||
<!-- webpack bundles head -->
|
<!-- webpack bundles head -->
|
||||||
|
|
||||||
<title>Prowlarr (Preview)</title>
|
<title>Prowlarr</title>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
The super basic styling for .root will live here,
|
The super basic styling for .root will live here,
|
||||||
|
|||||||
+57
-54
@@ -11,7 +11,8 @@
|
|||||||
"lint": "esprint check",
|
"lint": "esprint check",
|
||||||
"lint-fix": "esprint check --fix",
|
"lint-fix": "esprint check --fix",
|
||||||
"stylelint-linux": "stylelint $(find frontend -name '*.css') --config frontend/.stylelintrc",
|
"stylelint-linux": "stylelint $(find frontend -name '*.css') --config frontend/.stylelintrc",
|
||||||
"stylelint-windows": "stylelint frontend/**/*.css --config frontend/.stylelintrc"
|
"stylelint-windows": "stylelint frontend/**/*.css --config frontend/.stylelintrc",
|
||||||
|
"check-modules": "are-you-es5 check . -r"
|
||||||
},
|
},
|
||||||
"repository": "https://github.com/Prowlarr/Prowlarr",
|
"repository": "https://github.com/Prowlarr/Prowlarr",
|
||||||
"author": "Team Prowlarr",
|
"author": "Team Prowlarr",
|
||||||
@@ -25,107 +26,109 @@
|
|||||||
"not chrome < 60"
|
"not chrome < 60"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "6.1.1",
|
"@fortawesome/fontawesome-free": "6.2.1",
|
||||||
"@fortawesome/fontawesome-svg-core": "6.1.1",
|
"@fortawesome/fontawesome-svg-core": "6.2.1",
|
||||||
"@fortawesome/free-regular-svg-icons": "6.1.1",
|
"@fortawesome/free-regular-svg-icons": "6.2.1",
|
||||||
"@fortawesome/free-solid-svg-icons": "6.1.1",
|
"@fortawesome/free-solid-svg-icons": "6.2.1",
|
||||||
"@fortawesome/react-fontawesome": "0.1.18",
|
"@fortawesome/react-fontawesome": "0.2.0",
|
||||||
"@microsoft/signalr": "6.0.6",
|
"@microsoft/signalr": "6.0.13",
|
||||||
"@sentry/browser": "6.19.2",
|
"@sentry/browser": "7.28.0",
|
||||||
"@sentry/integrations": "6.19.2",
|
"@sentry/integrations": "7.28.0",
|
||||||
"chart.js": "3.7.1",
|
"chart.js": "4.1.1",
|
||||||
"classnames": "2.3.1",
|
"classnames": "2.3.2",
|
||||||
"clipboard": "2.0.10",
|
"clipboard": "2.0.11",
|
||||||
"connected-react-router": "6.9.1",
|
"connected-react-router": "6.9.3",
|
||||||
"element-class": "0.2.2",
|
"element-class": "0.2.2",
|
||||||
"filesize": "6.3.0",
|
"filesize": "10.0.6",
|
||||||
"history": "4.10.1",
|
"history": "4.10.1",
|
||||||
"https-browserify": "1.0.0",
|
"https-browserify": "1.0.0",
|
||||||
"jdu": "1.0.0",
|
"jdu": "1.0.0",
|
||||||
"jquery": "3.6.0",
|
"jquery": "3.6.2",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"mobile-detect": "1.4.5",
|
"mobile-detect": "1.4.5",
|
||||||
"moment": "2.29.2",
|
"moment": "2.29.4",
|
||||||
"mousetrap": "1.6.5",
|
"mousetrap": "1.6.5",
|
||||||
"normalize.css": "8.0.1",
|
"normalize.css": "8.0.1",
|
||||||
"prop-types": "15.8.1",
|
"prop-types": "15.8.1",
|
||||||
"qs": "6.10.3",
|
"qs": "6.11.0",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-addons-shallow-compare": "15.6.3",
|
"react-addons-shallow-compare": "15.6.3",
|
||||||
"react-async-script": "1.2.0",
|
"react-async-script": "1.2.0",
|
||||||
"react-autosuggest": "10.1.0",
|
"react-autosuggest": "10.1.0",
|
||||||
"react-custom-scrollbars-2": "4.4.0",
|
"react-custom-scrollbars-2": "4.5.0",
|
||||||
"react-dnd": "14.0.4",
|
"react-dnd": "14.0.4",
|
||||||
"react-dnd-html5-backend": "14.0.2",
|
"react-dnd-html5-backend": "14.0.2",
|
||||||
"react-dnd-multi-backend": "6.0.2",
|
"react-dnd-multi-backend": "6.0.2",
|
||||||
"react-dnd-touch-backend": "14.1.1",
|
"react-dnd-touch-backend": "14.1.1",
|
||||||
"react-document-title": "2.0.3",
|
"react-document-title": "2.0.3",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
"react-focus-lock": "2.5.0",
|
"react-focus-lock": "2.9.2",
|
||||||
"react-google-recaptcha": "2.1.0",
|
"react-google-recaptcha": "2.1.0",
|
||||||
"react-lazyload": "3.2.0",
|
"react-lazyload": "3.2.0",
|
||||||
"react-measure": "1.4.7",
|
"react-measure": "1.4.7",
|
||||||
"react-popper": "1.3.7",
|
"react-popper": "1.3.7",
|
||||||
"react-redux": "7.2.4",
|
"react-redux": "8.0.5",
|
||||||
"react-router": "5.2.0",
|
"react-router": "5.2.0",
|
||||||
"react-router-dom": "5.2.0",
|
"react-router-dom": "5.2.0",
|
||||||
|
"react-text-truncate": "0.19.0",
|
||||||
"react-virtualized": "9.21.1",
|
"react-virtualized": "9.21.1",
|
||||||
"redux": "4.1.0",
|
"redux": "4.2.0",
|
||||||
"redux-actions": "2.6.5",
|
"redux-actions": "2.6.5",
|
||||||
"redux-batched-actions": "0.5.0",
|
"redux-batched-actions": "0.5.0",
|
||||||
"redux-localstorage": "0.4.1",
|
"redux-localstorage": "0.4.1",
|
||||||
"redux-thunk": "2.3.0",
|
"redux-thunk": "2.4.2",
|
||||||
"reselect": "4.0.0"
|
"reselect": "4.1.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.18.2",
|
"@babel/core": "7.20.5",
|
||||||
"@babel/eslint-parser": "7.18.2",
|
"@babel/eslint-parser": "7.19.1",
|
||||||
"@babel/plugin-proposal-class-properties": "7.17.12",
|
"@babel/plugin-proposal-class-properties": "7.18.6",
|
||||||
"@babel/plugin-proposal-decorators": "7.18.2",
|
"@babel/plugin-proposal-decorators": "7.20.5",
|
||||||
"@babel/plugin-proposal-export-default-from": "7.17.12",
|
"@babel/plugin-proposal-export-default-from": "7.18.10",
|
||||||
"@babel/plugin-proposal-export-namespace-from": "7.17.12",
|
"@babel/plugin-proposal-export-namespace-from": "7.18.9",
|
||||||
"@babel/plugin-proposal-function-sent": "7.18.2",
|
"@babel/plugin-proposal-function-sent": "7.18.6",
|
||||||
"@babel/plugin-proposal-nullish-coalescing-operator": "7.17.12",
|
"@babel/plugin-proposal-nullish-coalescing-operator": "7.18.6",
|
||||||
"@babel/plugin-proposal-numeric-separator": "7.16.7",
|
"@babel/plugin-proposal-numeric-separator": "7.18.6",
|
||||||
"@babel/plugin-proposal-optional-chaining": "7.17.12",
|
"@babel/plugin-proposal-optional-chaining": "7.18.9",
|
||||||
"@babel/plugin-proposal-throw-expressions": "7.16.7",
|
"@babel/plugin-proposal-throw-expressions": "7.18.6",
|
||||||
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
||||||
"@babel/preset-env": "7.18.2",
|
"@babel/preset-env": "7.20.2",
|
||||||
"@babel/preset-react": "7.17.12",
|
"@babel/preset-react": "7.18.6",
|
||||||
"autoprefixer": "10.4.7",
|
"are-you-es5": "2.1.2",
|
||||||
"babel-loader": "8.2.5",
|
"autoprefixer": "10.4.13",
|
||||||
|
"babel-loader": "9.1.0",
|
||||||
"babel-plugin-inline-classnames": "2.0.1",
|
"babel-plugin-inline-classnames": "2.0.1",
|
||||||
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
|
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
|
||||||
"core-js": "3.22.8",
|
"core-js": "3.26.1",
|
||||||
"css-loader": "6.7.1",
|
"css-loader": "6.7.3",
|
||||||
"eslint": "8.17.0",
|
"eslint": "8.30.0",
|
||||||
"eslint-plugin-filenames": "1.3.2",
|
"eslint-plugin-filenames": "1.3.2",
|
||||||
"eslint-plugin-import": "2.26.0",
|
"eslint-plugin-import": "2.26.0",
|
||||||
"eslint-plugin-react": "7.30.0",
|
"eslint-plugin-react": "7.31.11",
|
||||||
"eslint-plugin-simple-import-sort": "7.0.0",
|
"eslint-plugin-simple-import-sort": "8.0.0",
|
||||||
"esprint": "3.6.0",
|
"esprint": "3.6.0",
|
||||||
"file-loader": "6.2.0",
|
"file-loader": "6.2.0",
|
||||||
"filemanager-webpack-plugin": "6.1.7",
|
"filemanager-webpack-plugin": "8.0.0",
|
||||||
"html-webpack-plugin": "5.5.0",
|
"html-webpack-plugin": "5.5.0",
|
||||||
"loader-utils": "^3.0.0",
|
"loader-utils": "^3.2.1",
|
||||||
"mini-css-extract-plugin": "2.6.0",
|
"mini-css-extract-plugin": "2.7.2",
|
||||||
"postcss": "8.4.14",
|
"postcss": "8.4.20",
|
||||||
"postcss-color-function": "4.1.0",
|
"postcss-color-function": "4.1.0",
|
||||||
"postcss-loader": "6.2.1",
|
"postcss-loader": "7.0.2",
|
||||||
"postcss-mixins": "9.0.2",
|
"postcss-mixins": "9.0.4",
|
||||||
"postcss-nested": "5.0.6",
|
"postcss-nested": "6.0.0",
|
||||||
"postcss-simple-vars": "6.0.3",
|
"postcss-simple-vars": "7.0.1",
|
||||||
"postcss-url": "10.1.3",
|
"postcss-url": "10.1.3",
|
||||||
"require-nocache": "1.0.0",
|
"require-nocache": "1.0.0",
|
||||||
"rimraf": "3.0.2",
|
"rimraf": "3.0.2",
|
||||||
"run-sequence": "2.2.1",
|
"run-sequence": "2.2.1",
|
||||||
"streamqueue": "1.1.2",
|
"streamqueue": "1.1.2",
|
||||||
"style-loader": "3.3.1",
|
"style-loader": "3.3.1",
|
||||||
"stylelint": "14.8.5",
|
"stylelint": "14.16.0",
|
||||||
"stylelint-order": "5.0.0",
|
"stylelint-order": "5.0.0",
|
||||||
"url-loader": "4.1.1",
|
"url-loader": "4.1.1",
|
||||||
"webpack": "5.73.0",
|
"webpack": "5.75.0",
|
||||||
"webpack-cli": "4.9.2",
|
"webpack-cli": "5.0.1",
|
||||||
"webpack-livereload-plugin": "3.0.2"
|
"webpack-livereload-plugin": "3.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
is_global = true
|
||||||
|
|
||||||
|
dotnet_diagnostic.CA1014.severity = none
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<Project>
|
<Project>
|
||||||
<!-- Common to all Prowlarr Projects -->
|
<!-- Common to all Prowlarr Projects -->
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
<AnalysisLevel>6.0-all</AnalysisLevel>
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
<ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles>
|
<ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles>
|
||||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||||
@@ -94,7 +95,7 @@
|
|||||||
|
|
||||||
<!-- Standard testing packages -->
|
<!-- Standard testing packages -->
|
||||||
<ItemGroup Condition="'$(TestProject)'=='true'">
|
<ItemGroup Condition="'$(TestProject)'=='true'">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
|
||||||
<PackageReference Include="NunitXml.TestLogger" Version="3.0.117" />
|
<PackageReference Include="NunitXml.TestLogger" Version="3.0.117" />
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ namespace NzbDrone.Automation.Test
|
|||||||
|
|
||||||
_runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger(), null);
|
_runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger(), null);
|
||||||
_runner.KillAll();
|
_runner.KillAll();
|
||||||
_runner.Start();
|
_runner.Start(true);
|
||||||
|
|
||||||
driver.Url = "http://localhost:9696";
|
driver.Url = "http://localhost:9696";
|
||||||
|
|
||||||
|
|||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using FluentAssertions;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
|
|
||||||
|
namespace NzbDrone.Common.Test.ExtensionTests.StringExtensionTests
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class IsValidIPAddressFixture
|
||||||
|
{
|
||||||
|
[TestCase("192.168.0.1")]
|
||||||
|
[TestCase("::1")]
|
||||||
|
[TestCase("2001:db8:4006:812::200e")]
|
||||||
|
public void should_validate_ip_address(string input)
|
||||||
|
{
|
||||||
|
input.IsValidIpAddress().Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase("sonarr.tv")]
|
||||||
|
public void should_not_parse_non_ip_address(string input)
|
||||||
|
{
|
||||||
|
input.IsValidIpAddress().Should().BeFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -212,6 +212,7 @@ namespace NzbDrone.Common.Test.Http
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
[Platform(Exclude = "MacOsX", Reason = "Azure agent update prevents brotli on OSX")]
|
||||||
public void should_execute_get_using_brotli()
|
public void should_execute_get_using_brotli()
|
||||||
{
|
{
|
||||||
var request = new HttpRequest($"https://{_httpBinHost}/brotli");
|
var request = new HttpRequest($"https://{_httpBinHost}/brotli");
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NzbDrone.Common.Http;
|
using NzbDrone.Common.Http;
|
||||||
using NzbDrone.Test.Common;
|
using NzbDrone.Test.Common;
|
||||||
@@ -10,6 +10,7 @@ namespace NzbDrone.Common.Test.Http
|
|||||||
[TestCase("abc://my_host.com:8080/root/api/")]
|
[TestCase("abc://my_host.com:8080/root/api/")]
|
||||||
[TestCase("abc://my_host.com:8080//root/api/")]
|
[TestCase("abc://my_host.com:8080//root/api/")]
|
||||||
[TestCase("abc://my_host.com:8080/root//api/")]
|
[TestCase("abc://my_host.com:8080/root//api/")]
|
||||||
|
[TestCase("abc://[::1]:8080/root//api/")]
|
||||||
public void should_parse(string uri)
|
public void should_parse(string uri)
|
||||||
{
|
{
|
||||||
var newUri = new HttpUri(uri);
|
var newUri = new HttpUri(uri);
|
||||||
@@ -52,6 +53,26 @@ namespace NzbDrone.Common.Test.Http
|
|||||||
newUri.FullUri.Should().Be(expected);
|
newUri.FullUri.Should().Be(expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestCase("", "./relative", "relative")]
|
||||||
|
[TestCase("/", "./relative", "/relative")]
|
||||||
|
[TestCase("/base", "./relative", "/relative")]
|
||||||
|
[TestCase("/base/sub", "./relative", "/base/relative")]
|
||||||
|
[TestCase("/base/sub/", "./relative", "/base/sub/relative")]
|
||||||
|
[TestCase("base/sub", "./relative", "base/relative")]
|
||||||
|
[TestCase("base/sub/", "./relative", "base/sub/relative")]
|
||||||
|
[TestCase("", "../relative", "relative")]
|
||||||
|
[TestCase("/", "../relative", "/relative")]
|
||||||
|
[TestCase("/base", "../relative", "/relative")]
|
||||||
|
[TestCase("/base/sub", "../relative", "/base/relative")]
|
||||||
|
[TestCase("/base/sub/", "../relative", "/base/sub/relative")]
|
||||||
|
[TestCase("base/sub", "../relative", "base/relative")]
|
||||||
|
[TestCase("base/sub/", "../relative", "base/sub/relative")]
|
||||||
|
public void should_combine_uri_with_dot_segment(string basePath, string relativePath, string expected)
|
||||||
|
{
|
||||||
|
var newUri = new HttpUri(basePath) + new HttpUri(relativePath);
|
||||||
|
newUri.FullUri.Should().Be(expected);
|
||||||
|
}
|
||||||
|
|
||||||
[TestCase("", "", "")]
|
[TestCase("", "", "")]
|
||||||
[TestCase("/", "", "/")]
|
[TestCase("/", "", "/")]
|
||||||
[TestCase("base", "", "base")]
|
[TestCase("base", "", "base")]
|
||||||
|
|||||||
@@ -24,12 +24,16 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
|||||||
[TestCase(@"https://beyond-hd.me/api/torrents/2b51db35e1912ffc138825a12b9933d2")]
|
[TestCase(@"https://beyond-hd.me/api/torrents/2b51db35e1912ffc138825a12b9933d2")]
|
||||||
[TestCase(@"Req: [POST] https://www3.yggtorrent.nz/user/login: id=mySecret&pass=mySecret&ci_csrf_token=2b51db35e1912ffc138825a12b9933d2")]
|
[TestCase(@"Req: [POST] https://www3.yggtorrent.nz/user/login: id=mySecret&pass=mySecret&ci_csrf_token=2b51db35e1912ffc138825a12b9933d2")]
|
||||||
[TestCase(@"https://torrentseeds.org/api/torrents/filter?api_token=2b51db35e1912ffc138825a12b9933d2&name=&sortField=created_at&sortDirection=desc&perPage=100&page=1")]
|
[TestCase(@"https://torrentseeds.org/api/torrents/filter?api_token=2b51db35e1912ffc138825a12b9933d2&name=&sortField=created_at&sortDirection=desc&perPage=100&page=1")]
|
||||||
|
[TestCase(@"https://beyond-hd.me/torrent/download/the-next-365-days-2022-2160p-nf-web-dl-dual-ddp-51-dovi-hdr-hevc-apex.225146.2b51db35e1912ffc138825a12b9933d2")]
|
||||||
|
[TestCase(@"https://anthelion.me/api.php?api_key=2b51db35e1910123321025a12b9933d2&o=json&t=movie&q=&tmdb=&imdb=&cat=&limit=100&offset=0")]
|
||||||
|
[TestCase(@"https://avistaz.to/api/v1/jackett/auth: username=mySecret&password=mySecret&pid=mySecret")]
|
||||||
|
|
||||||
// Indexer and Download Client Responses
|
// Indexer and Download Client Responses
|
||||||
|
|
||||||
// avistaz response
|
// avistaz response
|
||||||
[TestCase(@"""download"":""https://avistaz.to/rss/download/2b51db35e1910123321025a12b9933d2/tb51db35e1910123321025a12b9933d2.torrent"",")]
|
[TestCase(@"""download"":""https://avistaz.to/rss/download/2b51db35e1910123321025a12b9933d2/tb51db35e1910123321025a12b9933d2.torrent"",")]
|
||||||
[TestCase(@",""info_hash"":""2b51db35e1910123321025a12b9933d2"",")]
|
[TestCase(@",""info_hash"":""2b51db35e1910123321025a12b9933d2"",")]
|
||||||
|
[TestCase(@"""token"":""2b51db35e1910123321025a12b9933d2""")]
|
||||||
|
|
||||||
// animebytes response
|
// animebytes response
|
||||||
[TestCase(@"""Link"":""https://animebytes.tv/torrent/994064/download/tb51db35e1910123321025a12b9933d2"",")]
|
[TestCase(@"""Link"":""https://animebytes.tv/torrent/994064/download/tb51db35e1910123321025a12b9933d2"",")]
|
||||||
@@ -98,15 +102,30 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
|||||||
// Internal
|
// Internal
|
||||||
[TestCase(@"[Info] MigrationController: *** Migrating Database=prowlarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;Enlist=False ***")]
|
[TestCase(@"[Info] MigrationController: *** Migrating Database=prowlarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;Enlist=False ***")]
|
||||||
[TestCase("/readarr/signalr/messages/negotiate?access_token=1234530f422f4aacb6b301233210aaaa&negotiateVersion=1")]
|
[TestCase("/readarr/signalr/messages/negotiate?access_token=1234530f422f4aacb6b301233210aaaa&negotiateVersion=1")]
|
||||||
|
[TestCase(@"[Info] MigrationController: *** Migrating Database=prowlarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;token=mySecret;Enlist=False&username=mySecret;mypassword=mySecret;mypass=shouldkeep1;test_token=mySecret;password=123%@%_@!#^#@;use_password=mySecret;get_token=shouldkeep2;usetoken=shouldkeep3;passwrd=mySecret;")]
|
||||||
public void should_clean_message(string message)
|
public void should_clean_message(string message)
|
||||||
{
|
{
|
||||||
var cleansedMessage = CleanseLogMessage.Cleanse(message);
|
var cleansedMessage = CleanseLogMessage.Cleanse(message);
|
||||||
|
|
||||||
cleansedMessage.Should().NotContain("mySecret");
|
cleansedMessage.Should().NotContain("mySecret");
|
||||||
|
cleansedMessage.Should().NotContain("123%@%_@!#^#@");
|
||||||
cleansedMessage.Should().NotContain("01233210");
|
cleansedMessage.Should().NotContain("01233210");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestCase(@"[Info] MigrationController: *** Migrating Database=radarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;token=mySecret;Enlist=False&username=mySecret;mypassword=mySecret;mypass=shouldkeep1;test_token=mySecret;password=123%@%_@!#^#@;use_password=mySecret;get_token=shouldkeep2;usetoken=shouldkeep3;passwrd=mySecret;")]
|
||||||
|
public void should_keep_message(string message)
|
||||||
|
{
|
||||||
|
var cleansedMessage = CleanseLogMessage.Cleanse(message);
|
||||||
|
|
||||||
|
cleansedMessage.Should().NotContain("mySecret");
|
||||||
|
cleansedMessage.Should().NotContain("123%@%_@!#^#@");
|
||||||
|
cleansedMessage.Should().NotContain("01233210");
|
||||||
|
|
||||||
|
cleansedMessage.Should().Contain("shouldkeep1");
|
||||||
|
cleansedMessage.Should().Contain("shouldkeep2");
|
||||||
|
cleansedMessage.Should().Contain("shouldkeep3");
|
||||||
|
}
|
||||||
|
|
||||||
[TestCase(@"Some message (from 32.2.3.5 user agent)")]
|
[TestCase(@"Some message (from 32.2.3.5 user agent)")]
|
||||||
[TestCase(@"Auth-Invalidated ip 32.2.3.5")]
|
[TestCase(@"Auth-Invalidated ip 32.2.3.5")]
|
||||||
[TestCase(@"Auth-Success ip 32.2.3.5")]
|
[TestCase(@"Auth-Success ip 32.2.3.5")]
|
||||||
|
|||||||
@@ -7,34 +7,50 @@ namespace NzbDrone.Common.Extensions
|
|||||||
{
|
{
|
||||||
public static bool IsLocalAddress(this IPAddress ipAddress)
|
public static bool IsLocalAddress(this IPAddress ipAddress)
|
||||||
{
|
{
|
||||||
if (ipAddress.IsIPv6LinkLocal)
|
// Map back to IPv4 if mapped to IPv6, for example "::ffff:1.2.3.4" to "1.2.3.4".
|
||||||
|
if (ipAddress.IsIPv4MappedToIPv6)
|
||||||
{
|
{
|
||||||
return true;
|
ipAddress = ipAddress.MapToIPv4();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Checks loopback ranges for both IPv4 and IPv6.
|
||||||
if (IPAddress.IsLoopback(ipAddress))
|
if (IPAddress.IsLoopback(ipAddress))
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IPv4
|
||||||
if (ipAddress.AddressFamily == AddressFamily.InterNetwork)
|
if (ipAddress.AddressFamily == AddressFamily.InterNetwork)
|
||||||
{
|
{
|
||||||
byte[] bytes = ipAddress.GetAddressBytes();
|
return IsLocalIPv4(ipAddress.GetAddressBytes());
|
||||||
switch (bytes[0])
|
}
|
||||||
{
|
|
||||||
case 10:
|
// IPv6
|
||||||
case 127:
|
if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6)
|
||||||
return true;
|
{
|
||||||
case 172:
|
return ipAddress.IsIPv6LinkLocal ||
|
||||||
return bytes[1] < 32 && bytes[1] >= 16;
|
ipAddress.IsIPv6UniqueLocal ||
|
||||||
case 192:
|
ipAddress.IsIPv6SiteLocal;
|
||||||
return bytes[1] == 168;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IsLocalIPv4(byte[] ipv4Bytes)
|
||||||
|
{
|
||||||
|
// Link local (no IP assigned by DHCP): 169.254.0.0 to 169.254.255.255 (169.254.0.0/16)
|
||||||
|
bool IsLinkLocal() => ipv4Bytes[0] == 169 && ipv4Bytes[1] == 254;
|
||||||
|
|
||||||
|
// Class A private range: 10.0.0.0 – 10.255.255.255 (10.0.0.0/8)
|
||||||
|
bool IsClassA() => ipv4Bytes[0] == 10;
|
||||||
|
|
||||||
|
// Class B private range: 172.16.0.0 – 172.31.255.255 (172.16.0.0/12)
|
||||||
|
bool IsClassB() => ipv4Bytes[0] == 172 && ipv4Bytes[1] >= 16 && ipv4Bytes[1] <= 31;
|
||||||
|
|
||||||
|
// Class C private range: 192.168.0.0 – 192.168.255.255 (192.168.0.0/16)
|
||||||
|
bool IsClassC() => ipv4Bytes[0] == 192 && ipv4Bytes[1] == 168;
|
||||||
|
|
||||||
|
return IsLinkLocal() || IsClassA() || IsClassC() || IsClassB();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
@@ -130,7 +131,7 @@ namespace NzbDrone.Common.Extensions
|
|||||||
|
|
||||||
public static string WrapInQuotes(this string text)
|
public static string WrapInQuotes(this string text)
|
||||||
{
|
{
|
||||||
if (!text.Contains(" "))
|
if (!text.Contains(' '))
|
||||||
{
|
{
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
@@ -231,5 +232,30 @@ namespace NzbDrone.Common.Extensions
|
|||||||
.Replace("'", "%27")
|
.Replace("'", "%27")
|
||||||
.Replace("%7E", "~");
|
.Replace("%7E", "~");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool IsValidIpAddress(this string value)
|
||||||
|
{
|
||||||
|
if (!IPAddress.TryParse(value, out var parsedAddress))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedAddress.Equals(IPAddress.Parse("255.255.255.255")))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedAddress.IsIPv6Multicast)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedAddress.AddressFamily == AddressFamily.InterNetwork || parsedAddress.AddressFamily == AddressFamily.InterNetworkV6;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ToUrlHost(this string input)
|
||||||
|
{
|
||||||
|
return input.Contains(':') ? $"[{input}]" : input;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ namespace NzbDrone.Common.Http.Dispatchers
|
|||||||
AddRequestHeaders(requestMessage, request.Headers);
|
AddRequestHeaders(requestMessage, request.Headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
var httpClient = GetClient(request.Url);
|
var httpClient = GetClient(request.Url, request.ProxySettings);
|
||||||
|
|
||||||
var sw = new Stopwatch();
|
var sw = new Stopwatch();
|
||||||
|
|
||||||
@@ -113,7 +113,7 @@ namespace NzbDrone.Common.Http.Dispatchers
|
|||||||
{
|
{
|
||||||
if (request.ResponseStream != null && responseMessage.StatusCode == HttpStatusCode.OK)
|
if (request.ResponseStream != null && responseMessage.StatusCode == HttpStatusCode.OK)
|
||||||
{
|
{
|
||||||
responseMessage.Content.CopyTo(request.ResponseStream, null, cts.Token);
|
await responseMessage.Content.CopyToAsync(request.ResponseStream, null, cts.Token);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -154,9 +154,9 @@ namespace NzbDrone.Common.Http.Dispatchers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual System.Net.Http.HttpClient GetClient(HttpUri uri)
|
protected virtual System.Net.Http.HttpClient GetClient(HttpUri uri, HttpProxySettings requestProxy)
|
||||||
{
|
{
|
||||||
var proxySettings = _proxySettingsProvider.GetProxySettings(uri);
|
var proxySettings = requestProxy ?? _proxySettingsProvider.GetProxySettings(uri);
|
||||||
|
|
||||||
var key = proxySettings?.Key ?? NO_PROXY_KEY;
|
var key = proxySettings?.Key ?? NO_PROXY_KEY;
|
||||||
|
|
||||||
@@ -174,6 +174,7 @@ namespace NzbDrone.Common.Http.Dispatchers
|
|||||||
PreAuthenticate = true,
|
PreAuthenticate = true,
|
||||||
MaxConnectionsPerServer = 12,
|
MaxConnectionsPerServer = 12,
|
||||||
ConnectCallback = onConnect,
|
ConnectCallback = onConnect,
|
||||||
|
PooledConnectionLifetime = TimeSpan.FromMinutes(10),
|
||||||
SslOptions = new SslClientAuthenticationOptions
|
SslOptions = new SslClientAuthenticationOptions
|
||||||
{
|
{
|
||||||
RemoteCertificateValidationCallback = _certificateValidationService.ShouldByPassValidationError
|
RemoteCertificateValidationCallback = _certificateValidationService.ShouldByPassValidationError
|
||||||
@@ -234,6 +235,7 @@ namespace NzbDrone.Common.Http.Dispatchers
|
|||||||
webRequest.Headers.TransferEncoding.ParseAdd(header.Value);
|
webRequest.Headers.TransferEncoding.ParseAdd(header.Value);
|
||||||
break;
|
break;
|
||||||
case "User-Agent":
|
case "User-Agent":
|
||||||
|
webRequest.Headers.UserAgent.Clear();
|
||||||
webRequest.Headers.UserAgent.ParseAdd(header.Value);
|
webRequest.Headers.UserAgent.ParseAdd(header.Value);
|
||||||
break;
|
break;
|
||||||
case "Proxy-Connection":
|
case "Proxy-Connection":
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using System.Net.Http;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using NzbDrone.Common.EnvironmentInfo;
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
|
using NzbDrone.Common.Http.Proxy;
|
||||||
|
|
||||||
namespace NzbDrone.Common.Http
|
namespace NzbDrone.Common.Http
|
||||||
{
|
{
|
||||||
@@ -37,7 +38,7 @@ namespace NzbDrone.Common.Http
|
|||||||
public HttpMethod Method { get; set; }
|
public HttpMethod Method { get; set; }
|
||||||
public HttpHeader Headers { get; set; }
|
public HttpHeader Headers { get; set; }
|
||||||
public Encoding Encoding { get; set; }
|
public Encoding Encoding { get; set; }
|
||||||
public IWebProxy Proxy { get; set; }
|
public HttpProxySettings ProxySettings { get; set; }
|
||||||
public byte[] ContentData { get; set; }
|
public byte[] ContentData { get; set; }
|
||||||
public string ContentSummary { get; set; }
|
public string ContentSummary { get; set; }
|
||||||
public ICredentials Credentials { get; set; }
|
public ICredentials Credentials { get; set; }
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ namespace NzbDrone.Common.Http
|
|||||||
get
|
get
|
||||||
{
|
{
|
||||||
var newUrl = Headers["Location"];
|
var newUrl = Headers["Location"];
|
||||||
|
|
||||||
if (newUrl == null)
|
if (newUrl == null)
|
||||||
{
|
{
|
||||||
newUrl = Headers["Refresh"];
|
newUrl = Headers["Refresh"];
|
||||||
@@ -89,13 +90,13 @@ namespace NzbDrone.Common.Http
|
|||||||
|
|
||||||
if (match.Success)
|
if (match.Success)
|
||||||
{
|
{
|
||||||
return (Request.Url += new HttpUri(match.Groups[2].Value)).FullUri;
|
return (Request.Url + new HttpUri(match.Groups[2].Value)).FullUri;
|
||||||
}
|
}
|
||||||
|
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (Request.Url += new HttpUri(newUrl)).FullUri;
|
return (Request.Url + new HttpUri(newUrl)).FullUri;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ namespace NzbDrone.Common.Http
|
|||||||
{
|
{
|
||||||
public class HttpUri : IEquatable<HttpUri>
|
public class HttpUri : IEquatable<HttpUri>
|
||||||
{
|
{
|
||||||
private static readonly Regex RegexUri = new Regex(@"^(?:(?<scheme>[a-z]+):)?(?://(?<host>[-_A-Z0-9.]+)(?::(?<port>[0-9]{1,5}))?)?(?<path>(?:(?:(?<=^)|/+)[^/?#\r\n]+)+/*|/+)?(?:\?(?<query>[^#\r\n]*))?(?:\#(?<fragment>.*))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
private static readonly Regex RegexUri = new Regex(@"^(?:(?<scheme>[a-z]+):)?(?://(?<host>[-_A-Z0-9.]+|\[[[A-F0-9:]+\])(?::(?<port>[0-9]{1,5}))?)?(?<path>(?:(?:(?<=^)|/+)[^/?#\r\n]+)+/*|/+)?(?:\?(?<query>[^#\r\n]*))?(?:\#(?<fragment>.*))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
private readonly string _uri;
|
private readonly string _uri;
|
||||||
public string FullUri => _uri;
|
public string FullUri => _uri;
|
||||||
@@ -70,6 +70,8 @@ namespace NzbDrone.Common.Http
|
|||||||
|
|
||||||
private void Parse()
|
private void Parse()
|
||||||
{
|
{
|
||||||
|
var parseSuccess = Uri.TryCreate(_uri, UriKind.RelativeOrAbsolute, out var uri);
|
||||||
|
|
||||||
var match = RegexUri.Match(_uri);
|
var match = RegexUri.Match(_uri);
|
||||||
|
|
||||||
var scheme = match.Groups["scheme"];
|
var scheme = match.Groups["scheme"];
|
||||||
@@ -79,7 +81,7 @@ namespace NzbDrone.Common.Http
|
|||||||
var query = match.Groups["query"];
|
var query = match.Groups["query"];
|
||||||
var fragment = match.Groups["fragment"];
|
var fragment = match.Groups["fragment"];
|
||||||
|
|
||||||
if (!match.Success || (scheme.Success && !host.Success && path.Success))
|
if (!parseSuccess || (scheme.Success && !host.Success && path.Success))
|
||||||
{
|
{
|
||||||
throw new ArgumentException("Uri didn't match expected pattern: " + _uri);
|
throw new ArgumentException("Uri didn't match expected pattern: " + _uri);
|
||||||
}
|
}
|
||||||
@@ -164,6 +166,37 @@ namespace NzbDrone.Common.Http
|
|||||||
return relativePath;
|
return relativePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (relativePath.StartsWith("./"))
|
||||||
|
{
|
||||||
|
relativePath = relativePath.TrimStart('.').TrimStart('/');
|
||||||
|
|
||||||
|
var lastIndex = basePath.LastIndexOf("/");
|
||||||
|
|
||||||
|
if (lastIndex > 0)
|
||||||
|
{
|
||||||
|
basePath = basePath.Substring(0, lastIndex) + "/";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (relativePath.StartsWith("../"))
|
||||||
|
{
|
||||||
|
relativePath = relativePath.TrimStart('.').TrimStart('/');
|
||||||
|
|
||||||
|
var lastIndex = basePath.LastIndexOf("/");
|
||||||
|
|
||||||
|
if (lastIndex > 0)
|
||||||
|
{
|
||||||
|
basePath = basePath.Substring(0, lastIndex) + "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
var secondLastIndex = basePath.LastIndexOf("/");
|
||||||
|
|
||||||
|
if (lastIndex > 0)
|
||||||
|
{
|
||||||
|
basePath = basePath.Substring(0, secondLastIndex) + "/";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var baseSlashIndex = basePath.LastIndexOf('/');
|
var baseSlashIndex = basePath.LastIndexOf('/');
|
||||||
|
|
||||||
if (baseSlashIndex >= 0)
|
if (baseSlashIndex >= 0)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace NzbDrone.Common.Http
|
namespace NzbDrone.Common.Http
|
||||||
{
|
{
|
||||||
@@ -11,19 +11,23 @@ namespace NzbDrone.Common.Http
|
|||||||
{
|
{
|
||||||
if (response.Headers.ContainsKey("Retry-After"))
|
if (response.Headers.ContainsKey("Retry-After"))
|
||||||
{
|
{
|
||||||
var retryAfter = response.Headers["Retry-After"].ToString();
|
var retryAfter = response.Headers["Retry-After"];
|
||||||
int seconds;
|
|
||||||
DateTime date;
|
|
||||||
|
|
||||||
if (int.TryParse(retryAfter, out seconds))
|
if (int.TryParse(retryAfter, out var seconds))
|
||||||
{
|
{
|
||||||
RetryAfter = TimeSpan.FromSeconds(seconds);
|
RetryAfter = TimeSpan.FromSeconds(seconds);
|
||||||
}
|
}
|
||||||
else if (DateTime.TryParse(retryAfter, out date))
|
else if (DateTime.TryParse(retryAfter, out var date))
|
||||||
{
|
{
|
||||||
RetryAfter = date.ToUniversalTime() - DateTime.UtcNow;
|
RetryAfter = date.ToUniversalTime() - DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public TooManyRequestsException(HttpRequest request, HttpResponse response, TimeSpan retryWait)
|
||||||
|
: base(request, response)
|
||||||
|
{
|
||||||
|
RetryAfter = retryWait;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
using System;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
@@ -8,18 +7,20 @@ namespace NzbDrone.Common.Instrumentation
|
|||||||
{
|
{
|
||||||
public class CleanseLogMessage
|
public class CleanseLogMessage
|
||||||
{
|
{
|
||||||
private static readonly Regex[] CleansingRules = new[]
|
private static readonly Regex[] CleansingRules =
|
||||||
{
|
{
|
||||||
// Url
|
// Url
|
||||||
new Regex(@"(?<=[?&: ;])(apikey|(?:(?:access|api)[-_]?)?token|pass(?:key|wd)?|auth|authkey|user|u?id|api|[a-z_]*apikey|account|pwd)=(?<secret>[^&=""]+?)(?=[ ""&=]|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
new Regex(@"(?<=[?&: ;])(apikey|api_key|(?:(?:access|api)[-_]?)?token|pass(?:key|wd)?|auth|authkey|user|u?id|api|[a-z_]*apikey|account|pid|pwd)=(?<secret>[^&=""]+?)(?=[ ""&=]|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||||
new Regex(@"(?<=[?& ;])[^=]*?(_?(?<!use|get_)token|username|passwo?rd)=(?<secret>[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
new Regex(@"(?<=[?& ;])[^=]*?(_?(?<!use|get_)token|username|passwo?rd)=(?<secret>[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||||
new Regex(@"rss\.torrentleech\.org/(?!rss)(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
new Regex(@"rss\.torrentleech\.org/(?!rss)(?<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(@"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(@"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(@"/fetch/[a-z0-9]{32}/(?<secret>[a-z0-9]{32})", RegexOptions.Compiled),
|
||||||
new Regex(@"getnzb.*?(?<=\?|&)(r)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
new Regex(@"getnzb.*?(?<=\?|&)(r)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||||
|
new Regex(@"\b(\w*)?(_?(?<!use|get_)token|username|passwo?rd)=(?<secret>[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||||
new Regex(@"(?<=authkey = "")(?<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),
|
new Regex(@"(?<=beyond-hd\.[a-z]+/api/torrents/)(?<secret>[^&=][a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||||
|
new Regex(@"(?<=beyond-hd\.[a-z]+/torrent/download/[\w\d-]+[.]\d+[.])(?<secret>[a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||||
|
|
||||||
// UNIT3D
|
// UNIT3D
|
||||||
new Regex(@"(?<=[a-z0-9-]+\.[a-z]+/torrent/download/\d+\.)(?<secret>[^&=][a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
new Regex(@"(?<=[a-z0-9-]+\.[a-z]+/torrent/download/\d+\.)(?<secret>[^&=][a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||||
@@ -57,9 +58,10 @@ namespace NzbDrone.Common.Instrumentation
|
|||||||
new Regex(@"(?:avistaz|exoticaz|cinemaz|privatehd)\.[a-z]{2,3}/rss/download/(?<secret>[^&=]+?)/(?<secret>[^&=]+?)\.torrent", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
new Regex(@"(?:avistaz|exoticaz|cinemaz|privatehd)\.[a-z]{2,3}/rss/download/(?<secret>[^&=]+?)/(?<secret>[^&=]+?)\.torrent", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||||
new Regex(@"(?:animebytes)\.[a-z]{2,3}/torrent/[0-9]+/download/(?<secret>[^&=]+?)[""]", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
new Regex(@"(?:animebytes)\.[a-z]{2,3}/torrent/[0-9]+/download/(?<secret>[^&=]+?)[""]", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||||
new Regex(@",""info_hash"":""(?<secret>[^&=]+?)"",", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
new Regex(@",""info_hash"":""(?<secret>[^&=]+?)"",", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||||
|
new Regex(@"""token"":""(?<secret>[^&=]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||||
new Regex(@",""pass[- _]?key"":""(?<secret>[^&=]+?)"",", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
new Regex(@",""pass[- _]?key"":""(?<secret>[^&=]+?)"",", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||||
new Regex(@",""rss[- _]?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);
|
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);
|
||||||
|
|
||||||
|
|||||||
@@ -11,26 +11,41 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
sentryEvent.Message = CleanseLogMessage.Cleanse(sentryEvent.Message.Message);
|
if (sentryEvent.Message is not null)
|
||||||
|
{
|
||||||
|
sentryEvent.Message.Formatted = CleanseLogMessage.Cleanse(sentryEvent.Message.Formatted);
|
||||||
|
sentryEvent.Message.Message = CleanseLogMessage.Cleanse(sentryEvent.Message.Message);
|
||||||
|
sentryEvent.Message.Params = sentryEvent.Message.Params?.Select(x => CleanseLogMessage.Cleanse(x switch
|
||||||
|
{
|
||||||
|
string str => str,
|
||||||
|
_ => x.ToString()
|
||||||
|
})).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
if (sentryEvent.Fingerprint != null)
|
if (sentryEvent.Fingerprint.Any())
|
||||||
{
|
{
|
||||||
var fingerprint = sentryEvent.Fingerprint.Select(x => CleanseLogMessage.Cleanse(x)).ToList();
|
var fingerprint = sentryEvent.Fingerprint.Select(x => CleanseLogMessage.Cleanse(x)).ToList();
|
||||||
sentryEvent.SetFingerprint(fingerprint);
|
sentryEvent.SetFingerprint(fingerprint);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sentryEvent.Extra != null)
|
if (sentryEvent.Extra.Any())
|
||||||
{
|
{
|
||||||
var extras = sentryEvent.Extra.ToDictionary(x => x.Key, y => (object)CleanseLogMessage.Cleanse((string)y.Value));
|
var extras = sentryEvent.Extra.ToDictionary(x => x.Key, y => (object)CleanseLogMessage.Cleanse(y.Value as string));
|
||||||
sentryEvent.SetExtras(extras);
|
sentryEvent.SetExtras(extras);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var exception in sentryEvent.SentryExceptions)
|
if (sentryEvent.SentryExceptions is not null)
|
||||||
{
|
{
|
||||||
exception.Value = CleanseLogMessage.Cleanse(exception.Value);
|
foreach (var exception in sentryEvent.SentryExceptions)
|
||||||
foreach (var frame in exception.Stacktrace.Frames)
|
|
||||||
{
|
{
|
||||||
frame.FileName = ShortenPath(frame.FileName);
|
exception.Value = CleanseLogMessage.Cleanse(exception.Value);
|
||||||
|
if (exception.Stacktrace is not null)
|
||||||
|
{
|
||||||
|
foreach (var frame in exception.Stacktrace.Frames)
|
||||||
|
{
|
||||||
|
frame.FileName = ShortenPath(frame.FileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using System.Threading;
|
|||||||
using NLog;
|
using NLog;
|
||||||
using NLog.Common;
|
using NLog.Common;
|
||||||
using NLog.Targets;
|
using NLog.Targets;
|
||||||
|
using Npgsql;
|
||||||
using NzbDrone.Common.EnvironmentInfo;
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using Sentry;
|
using Sentry;
|
||||||
@@ -34,6 +35,14 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
|||||||
SQLiteErrorCode.Auth
|
SQLiteErrorCode.Auth
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static readonly HashSet<string> FilteredPostgresErrorCodes = new HashSet<string>
|
||||||
|
{
|
||||||
|
PostgresErrorCodes.OutOfMemory,
|
||||||
|
PostgresErrorCodes.TooManyConnections,
|
||||||
|
PostgresErrorCodes.DiskFull,
|
||||||
|
PostgresErrorCodes.ProgramLimitExceeded
|
||||||
|
};
|
||||||
|
|
||||||
// use string and not Type so we don't need a reference to the project
|
// use string and not Type so we don't need a reference to the project
|
||||||
// where these are defined
|
// where these are defined
|
||||||
private static readonly HashSet<string> FilteredExceptionTypeNames = new HashSet<string>
|
private static readonly HashSet<string> FilteredExceptionTypeNames = new HashSet<string>
|
||||||
@@ -42,10 +51,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
|||||||
"UnauthorizedAccessException",
|
"UnauthorizedAccessException",
|
||||||
|
|
||||||
// Filter out people stuck in boot loops
|
// Filter out people stuck in boot loops
|
||||||
"CorruptDatabaseException",
|
"CorruptDatabaseException"
|
||||||
|
|
||||||
// This also filters some people in boot loops
|
|
||||||
"TinyIoCResolutionException"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public static readonly List<string> FilteredExceptionMessages = new List<string>
|
public static readonly List<string> FilteredExceptionMessages = new List<string>
|
||||||
@@ -102,9 +108,6 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
|||||||
o.Dsn = dsn;
|
o.Dsn = dsn;
|
||||||
o.AttachStacktrace = true;
|
o.AttachStacktrace = true;
|
||||||
o.MaxBreadcrumbs = 200;
|
o.MaxBreadcrumbs = 200;
|
||||||
o.SendDefaultPii = false;
|
|
||||||
o.Debug = false;
|
|
||||||
o.DiagnosticLevel = SentryLevel.Debug;
|
|
||||||
o.Release = BuildInfo.Release;
|
o.Release = BuildInfo.Release;
|
||||||
o.BeforeSend = x => SentryCleanser.CleanseEvent(x);
|
o.BeforeSend = x => SentryCleanser.CleanseEvent(x);
|
||||||
o.BeforeBreadcrumb = x => SentryCleanser.CleanseBreadcrumb(x);
|
o.BeforeBreadcrumb = x => SentryCleanser.CleanseBreadcrumb(x);
|
||||||
@@ -210,7 +213,11 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
|||||||
if (ex != null)
|
if (ex != null)
|
||||||
{
|
{
|
||||||
fingerPrint.Add(ex.GetType().FullName);
|
fingerPrint.Add(ex.GetType().FullName);
|
||||||
fingerPrint.Add(ex.TargetSite.ToString());
|
if (ex.TargetSite != null)
|
||||||
|
{
|
||||||
|
fingerPrint.Add(ex.TargetSite.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
if (ex.InnerException != null)
|
if (ex.InnerException != null)
|
||||||
{
|
{
|
||||||
fingerPrint.Add(ex.InnerException.GetType().FullName);
|
fingerPrint.Add(ex.InnerException.GetType().FullName);
|
||||||
@@ -241,6 +248,19 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var pgEx = logEvent.Exception as PostgresException;
|
||||||
|
if (pgEx != null && FilteredPostgresErrorCodes.Contains(pgEx.SqlState))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't care about transient network and timeout errors
|
||||||
|
var npgEx = logEvent.Exception as NpgsqlException;
|
||||||
|
if (npgEx != null && npgEx.IsTransient)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (FilteredExceptionTypeNames.Contains(logEvent.Exception.GetType().Name))
|
if (FilteredExceptionTypeNames.Contains(logEvent.Exception.GetType().Name))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -226,7 +226,7 @@ namespace NzbDrone.Common.OAuth
|
|||||||
#if WINRT
|
#if WINRT
|
||||||
return CultureInfo.InvariantCulture.CompareInfo.Compare(left, right, CompareOptions.IgnoreCase) == 0;
|
return CultureInfo.InvariantCulture.CompareInfo.Compare(left, right, CompareOptions.IgnoreCase) == 0;
|
||||||
#else
|
#else
|
||||||
return string.Compare(left, right, StringComparison.InvariantCultureIgnoreCase) == 0;
|
return string.Equals(left, right, StringComparison.InvariantCultureIgnoreCase);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,19 +4,19 @@
|
|||||||
<DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants>
|
<DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="DryIoc.dll" Version="4.8.8" />
|
<PackageReference Include="DryIoc.dll" Version="5.3.3" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
|
||||||
<PackageReference Include="NLog.Extensions.Logging" Version="1.7.4" />
|
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.0" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
<PackageReference Include="NLog" Version="5.1.0" />
|
||||||
<PackageReference Include="NLog" Version="5.0.1" />
|
<PackageReference Include="NLog.Extensions.Logging" Version="5.2.0" />
|
||||||
<PackageReference Include="NLog.Extensions.Logging" Version="5.0.0" />
|
<PackageReference Include="Npgsql" Version="5.0.11" />
|
||||||
<PackageReference Include="Sentry" Version="3.19.0" />
|
<PackageReference Include="Sentry" Version="3.24.1" />
|
||||||
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
|
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
|
||||||
<PackageReference Include="SharpZipLib" Version="1.3.3" />
|
<PackageReference Include="SharpZipLib" Version="1.3.3" />
|
||||||
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
||||||
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
||||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.0" />
|
<PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.1" />
|
||||||
<PackageReference Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
|
<PackageReference Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
|
||||||
<PackageReference Include="System.Runtime.Loader" Version="4.3.0" />
|
<PackageReference Include="System.Runtime.Loader" Version="4.3.0" />
|
||||||
<PackageReference Include="System.ServiceProcess.ServiceController" Version="6.0.0" />
|
<PackageReference Include="System.ServiceProcess.ServiceController" Version="6.0.0" />
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ namespace NzbDrone.Common
|
|||||||
|
|
||||||
var args = $"create {serviceName} " +
|
var args = $"create {serviceName} " +
|
||||||
$"DisplayName= \"{serviceName}\" " +
|
$"DisplayName= \"{serviceName}\" " +
|
||||||
$"binpath= \"{Process.GetCurrentProcess().MainModule.FileName}\" " +
|
$"binpath= \"{Environment.ProcessPath}\" " +
|
||||||
"start= auto " +
|
"start= auto " +
|
||||||
"depend= EventLog/Tcpip/http " +
|
"depend= EventLog/Tcpip/http " +
|
||||||
"obj= \"NT AUTHORITY\\LocalService\"";
|
"obj= \"NT AUTHORITY\\LocalService\"";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@@ -19,7 +19,7 @@ namespace NzbDrone.Common.TPL
|
|||||||
private readonly int _maxDegreeOfParallelism;
|
private readonly int _maxDegreeOfParallelism;
|
||||||
|
|
||||||
/// <summary>Whether the scheduler is currently processing work items.</summary>
|
/// <summary>Whether the scheduler is currently processing work items.</summary>
|
||||||
private int _delegatesQueuedOrRunning = 0;
|
private int _delegatesQueuedOrRunning;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes an instance of the LimitedConcurrencyLevelTaskScheduler class with the
|
/// Initializes an instance of the LimitedConcurrencyLevelTaskScheduler class with the
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using FluentAssertions;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Common.Serializer;
|
||||||
|
using NzbDrone.Core.Datastore.Migration;
|
||||||
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Test.Datastore.Migration
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class orpheus_apiFixture : MigrationTest<orpheus_api>
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void should_convert_and_disable_orpheus_instance()
|
||||||
|
{
|
||||||
|
var db = WithMigrationTestDb(c =>
|
||||||
|
{
|
||||||
|
c.Insert.IntoTable("Indexers").Row(new
|
||||||
|
{
|
||||||
|
Enable = true,
|
||||||
|
Name = "Orpheus",
|
||||||
|
Priority = 25,
|
||||||
|
Added = DateTime.UtcNow,
|
||||||
|
Implementation = "Orpheus",
|
||||||
|
Settings = new GazelleIndexerSettings021
|
||||||
|
{
|
||||||
|
Username = "some name",
|
||||||
|
Password = "some pass"
|
||||||
|
}.ToJson(),
|
||||||
|
ConfigContract = "GazelleSettings"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var items = db.Query<IndexerDefinition022>("SELECT \"Id\", \"Enable\", \"ConfigContract\", \"Settings\" FROM \"Indexers\"");
|
||||||
|
|
||||||
|
items.Should().HaveCount(1);
|
||||||
|
items.First().ConfigContract.Should().Be("OrpheusSettings");
|
||||||
|
items.First().Enable.Should().Be(false);
|
||||||
|
items.First().Settings.Should().NotContain("username");
|
||||||
|
items.First().Settings.Should().NotContain("password");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class IndexerDefinition022
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public bool Enable { get; set; }
|
||||||
|
public string ConfigContract { get; set; }
|
||||||
|
public string Settings { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GazelleIndexerSettings021
|
||||||
|
{
|
||||||
|
public string Username { get; set; }
|
||||||
|
public string Password { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"response": []
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
<subcat id="5030" name="SD"/>
|
<subcat id="5030" name="SD"/>
|
||||||
<subcat id="5060" name="Sport"/>
|
<subcat id="5060" name="Sport"/>
|
||||||
<subcat id="5010" name="WEB-DL"/>
|
<subcat id="5010" name="WEB-DL"/>
|
||||||
|
<subcat id="5999" name="Other"/>
|
||||||
</category>
|
</category>
|
||||||
<category id="7000" name="Other">
|
<category id="7000" name="Other">
|
||||||
<subcat id="7010" name="Misc"/>
|
<subcat id="7010" name="Misc"/>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,144 @@
|
|||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"response": {
|
||||||
|
"currentPage": 1,
|
||||||
|
"pages": 1,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"groupId": 2497,
|
||||||
|
"groupName": "Singin' in the Rain",
|
||||||
|
"artist": "Gene Kelly & Stanley Donen",
|
||||||
|
"cover": "https:\/\/www.themoviedb.org\/t\/p\/original\/g2AaJDC2vSRcqHSDH29642xmQd.jpg",
|
||||||
|
"tags": [ "comedy", "musical", "romance" ],
|
||||||
|
"bookmarked": false,
|
||||||
|
"vanityHouse": false,
|
||||||
|
"groupYear": 1952,
|
||||||
|
"releaseType": null,
|
||||||
|
"groupTime": "1671129449",
|
||||||
|
"maxSize": 57473058680,
|
||||||
|
"totalSnatched": 25,
|
||||||
|
"totalSeeders": 9,
|
||||||
|
"totalLeechers": 0,
|
||||||
|
"torrents": [
|
||||||
|
{
|
||||||
|
"torrentId": 3599,
|
||||||
|
"editionId": 1,
|
||||||
|
"artists": [
|
||||||
|
{
|
||||||
|
"id": 126,
|
||||||
|
"name": "Gene Kelly",
|
||||||
|
"aliasid": 127
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 125,
|
||||||
|
"name": "Stanley Donen",
|
||||||
|
"aliasid": 126
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"remastered": false,
|
||||||
|
"remasterYear": 0,
|
||||||
|
"remasterCatalogueNumber": "",
|
||||||
|
"remasterTitle": "",
|
||||||
|
"media": "1080p",
|
||||||
|
"encoding": "",
|
||||||
|
"format": "",
|
||||||
|
"hasLog": false,
|
||||||
|
"logScore": 0,
|
||||||
|
"hasCue": false,
|
||||||
|
"scene": false,
|
||||||
|
"vanityHouse": false,
|
||||||
|
"fileCount": 1,
|
||||||
|
"time": "2017-09-10 11:47:27",
|
||||||
|
"size": 24724893991,
|
||||||
|
"snatches": 14,
|
||||||
|
"seeders": 1,
|
||||||
|
"leechers": 0,
|
||||||
|
"isFreeleech": true,
|
||||||
|
"isNeutralLeech": false,
|
||||||
|
"isPersonalFreeleech": false,
|
||||||
|
"canUseToken": false,
|
||||||
|
"hasSnatched": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"torrentId": 45068,
|
||||||
|
"editionId": 2,
|
||||||
|
"artists": [
|
||||||
|
{
|
||||||
|
"id": 126,
|
||||||
|
"name": "Gene Kelly",
|
||||||
|
"aliasid": 127
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 125,
|
||||||
|
"name": "Stanley Donen",
|
||||||
|
"aliasid": 126
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"remastered": false,
|
||||||
|
"remasterYear": 0,
|
||||||
|
"remasterCatalogueNumber": "",
|
||||||
|
"remasterTitle": "",
|
||||||
|
"media": "2160p",
|
||||||
|
"encoding": "",
|
||||||
|
"format": "",
|
||||||
|
"hasLog": false,
|
||||||
|
"logScore": 0,
|
||||||
|
"hasCue": false,
|
||||||
|
"scene": false,
|
||||||
|
"vanityHouse": false,
|
||||||
|
"fileCount": 1,
|
||||||
|
"time": "2022-12-15 19:37:29",
|
||||||
|
"size": 57473058680,
|
||||||
|
"snatches": 6,
|
||||||
|
"seeders": 8,
|
||||||
|
"leechers": 0,
|
||||||
|
"isFreeleech": true,
|
||||||
|
"isNeutralLeech": false,
|
||||||
|
"isPersonalFreeleech": false,
|
||||||
|
"canUseToken": false,
|
||||||
|
"hasSnatched": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"torrentId": 2726,
|
||||||
|
"editionId": 3,
|
||||||
|
"artists": [
|
||||||
|
{
|
||||||
|
"id": 126,
|
||||||
|
"name": "Gene Kelly",
|
||||||
|
"aliasid": 127
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 125,
|
||||||
|
"name": "Stanley Donen",
|
||||||
|
"aliasid": 126
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"remastered": false,
|
||||||
|
"remasterYear": 0,
|
||||||
|
"remasterCatalogueNumber": "",
|
||||||
|
"remasterTitle": "",
|
||||||
|
"media": "DVD-R",
|
||||||
|
"encoding": "",
|
||||||
|
"format": "",
|
||||||
|
"hasLog": false,
|
||||||
|
"logScore": 0,
|
||||||
|
"hasCue": false,
|
||||||
|
"scene": false,
|
||||||
|
"vanityHouse": false,
|
||||||
|
"fileCount": 37,
|
||||||
|
"time": "2017-08-26 14:58:58",
|
||||||
|
"size": 10350032896,
|
||||||
|
"snatches": 5,
|
||||||
|
"seeders": 0,
|
||||||
|
"leechers": 0,
|
||||||
|
"isFreeleech": true,
|
||||||
|
"isNeutralLeech": false,
|
||||||
|
"isPersonalFreeleech": false,
|
||||||
|
"canUseToken": false,
|
||||||
|
"hasSnatched": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<caps>
|
||||||
|
<server version="1.0" title="Anime Tosho" strapline="Anime NZB/DDL mirror" url="https://animetosho.org/"/>
|
||||||
|
<limits max="200" default="75"/>
|
||||||
|
<retention days="9999"/>
|
||||||
|
<registration available="no" open="yes" />
|
||||||
|
<searching>
|
||||||
|
<search available="yes" supportedParams="q" />
|
||||||
|
<tv-search available="no" supportedParams="q" />
|
||||||
|
<movie-search available="no" supportedParams="q" />
|
||||||
|
</searching>
|
||||||
|
<categories>
|
||||||
|
<category id="5070" name="Anime" description="Anime"/>
|
||||||
|
</categories>
|
||||||
|
</caps>
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<rss version="2.0" xmlns:torznab="http://torznab.com/schemas/2015/feed">
|
||||||
|
<channel>
|
||||||
|
<item>
|
||||||
|
<title>Out of the Past 1947 720p BluRay FLAC2.0 x264-CtrlHD.mkv</title>
|
||||||
|
<guid isPermaLink="true">https://www.morethantv.me/torrents.php?id=(removed)&torrentid=836164</guid>
|
||||||
|
<link>https://www.morethantv.me/torrents.php?action=download&id=(removed)&authkey=(removed)&torrent_pass=(removed)</link>
|
||||||
|
<comments>https://www.morethantv.me/torrents.php?id=(removed)&torrentid=836164</comments>
|
||||||
|
<pubDate>Tue, 20 Dec 2022 21:32:17 +0000</pubDate>
|
||||||
|
<size>5412993028</size>
|
||||||
|
<files>1</files>
|
||||||
|
<grabs>2</grabs>
|
||||||
|
<category>2000</category>
|
||||||
|
<category>2040</category>
|
||||||
|
<description>A private eye escapes his past to run a gas station in a small town, but his past catches up with him. Now he must return to the big city world of danger, corruption, double crosses, and duplicitous dames.</description>
|
||||||
|
<enclosure url="https://www.morethantv.me/torrents.php?action=download&id=(removed)&authkey=(removed)&torrent_pass=(removed)" length="103641" type="application/x-bittorrent" />
|
||||||
|
<torznab:attr name="size" value="5412993028" />
|
||||||
|
<torznab:attr name="poster" value="anon" />
|
||||||
|
<torznab:attr name="seeders" value="3" />
|
||||||
|
<torznab:attr name="leechers" value="0" />
|
||||||
|
<torznab:attr name="peers" value="3" />
|
||||||
|
<torznab:attr name="infohash" value="(removed)" />
|
||||||
|
<torznab:attr name="downloadvolumefactor" value="1" />
|
||||||
|
<torznab:attr name="uploadvolumefactor" value="1" />
|
||||||
|
<torznab:attr name="tag" value="anonymous" />
|
||||||
|
<torznab:attr name="imdb" value="0039689" />
|
||||||
|
<torznab:attr name="imdbid" value="tt0039689" />
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>Out of the Past 1947 1080p USA Blu-ray AVC DTS-HD MA 2.0-PCH</title>
|
||||||
|
<guid isPermaLink="true">https://www.morethantv.me/torrents.php?id=(removed)&torrentid=836165</guid>
|
||||||
|
<link>https://www.morethantv.me/torrents.php?action=download&id=(removed)&authkey=(removed)&torrent_pass=(removed)</link>
|
||||||
|
<comments>https://www.morethantv.me/torrents.php?id=(removed)&torrentid=836165</comments>
|
||||||
|
<pubDate>Tue, 20 Dec 2022 21:47:40 +0000</pubDate>
|
||||||
|
<size>30524085127</size>
|
||||||
|
<files>78</files>
|
||||||
|
<grabs>0</grabs>
|
||||||
|
<category>2000</category>
|
||||||
|
<category>2040</category>
|
||||||
|
<description>A private eye escapes his past to run a gas station in a small town, but his past catches up with him. Now he must return to the big city world of danger, corruption, double crosses, and duplicitous dames.</description>
|
||||||
|
<enclosure url="https://www.morethantv.me/torrents.php?action=download&id=(removed)&authkey=(removed)&torrent_pass=(removed)" length="150224" type="application/x-bittorrent" />
|
||||||
|
<torznab:attr name="size" value="30524085127" />
|
||||||
|
<torznab:attr name="poster" value="anon" />
|
||||||
|
<torznab:attr name="seeders" value="1" />
|
||||||
|
<torznab:attr name="leechers" value="0" />
|
||||||
|
<torznab:attr name="peers" value="1" />
|
||||||
|
<torznab:attr name="infohash" value="(removed)" />
|
||||||
|
<torznab:attr name="downloadvolumefactor" value="1" />
|
||||||
|
<torznab:attr name="uploadvolumefactor" value="1" />
|
||||||
|
<torznab:attr name="tag" value="anonymous" />
|
||||||
|
<torznab:attr name="imdb" value="0039689" />
|
||||||
|
<torznab:attr name="imdbid" value="tt0039689" />
|
||||||
|
</item>
|
||||||
|
</channel>
|
||||||
|
</rss>
|
||||||
@@ -28,15 +28,15 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
|
|||||||
[Test]
|
[Test]
|
||||||
public void should_return_warning_when_branch_not_valid()
|
public void should_return_warning_when_branch_not_valid()
|
||||||
{
|
{
|
||||||
GivenValidBranch("master");
|
GivenValidBranch("test");
|
||||||
|
|
||||||
Subject.Check().ShouldBeWarning();
|
Subject.Check().ShouldBeWarning();
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("Develop")]
|
|
||||||
[TestCase("develop")]
|
|
||||||
[TestCase("nightly")]
|
[TestCase("nightly")]
|
||||||
[TestCase("Nightly")]
|
[TestCase("Nightly")]
|
||||||
|
[TestCase("develop")]
|
||||||
|
[TestCase("master")]
|
||||||
public void should_return_no_warning_when_branch_valid(string branch)
|
public void should_return_no_warning_when_branch_valid(string branch)
|
||||||
{
|
{
|
||||||
GivenValidBranch(branch);
|
GivenValidBranch(branch);
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using FizzWare.NBuilder;
|
||||||
|
using FluentAssertions;
|
||||||
|
using Moq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Core.History;
|
||||||
|
using NzbDrone.Core.Indexers;
|
||||||
|
using NzbDrone.Core.IndexerStats;
|
||||||
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Test.IndexerStatsTests
|
||||||
|
{
|
||||||
|
public class IndexerStatisticsServiceFixture : CoreTest<IndexerStatisticsService>
|
||||||
|
{
|
||||||
|
private IndexerDefinition _indexer;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
_indexer = Builder<IndexerDefinition>.CreateNew().With(x => x.Id = 5).Build();
|
||||||
|
|
||||||
|
Mocker.GetMock<IIndexerFactory>()
|
||||||
|
.Setup(o => o.All())
|
||||||
|
.Returns(new List<IndexerDefinition> { _indexer });
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_pull_stats_if_all_events_are_failures()
|
||||||
|
{
|
||||||
|
var history = new List<History.History>
|
||||||
|
{
|
||||||
|
new History.History
|
||||||
|
{
|
||||||
|
Date = DateTime.UtcNow.AddHours(-1),
|
||||||
|
EventType = HistoryEventType.IndexerRss,
|
||||||
|
Successful = false,
|
||||||
|
Id = 8,
|
||||||
|
IndexerId = 5,
|
||||||
|
Data = new Dictionary<string, string> { { "source", "prowlarr" } }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Mocker.GetMock<IHistoryService>()
|
||||||
|
.Setup(o => o.Between(It.IsAny<DateTime>(), It.IsAny<DateTime>()))
|
||||||
|
.Returns<DateTime, DateTime>((s, f) => history);
|
||||||
|
|
||||||
|
var statistics = Subject.IndexerStatistics(DateTime.UtcNow.AddMonths(-1), DateTime.UtcNow);
|
||||||
|
|
||||||
|
statistics.IndexerStatistics.Count.Should().Be(1);
|
||||||
|
statistics.IndexerStatistics.First().AverageResponseTime.Should().Be(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -51,7 +51,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
|
|||||||
torrentInfo.InfoUrl.Should().Be("https://avistaz.to/torrent/187240-japan-sinks-people-of-hope-2021-s01e05-720p-nf-web-dl-ddp20-x264-seikel");
|
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.CommentUrl.Should().BeNullOrEmpty();
|
||||||
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-11-14 23:26:21"));
|
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-11-14 22:26:21"));
|
||||||
torrentInfo.Size.Should().Be(935127615);
|
torrentInfo.Size.Should().Be(935127615);
|
||||||
torrentInfo.InfoHash.Should().Be("a879261d4e6e792402f92401141a21de70d51bf2");
|
torrentInfo.InfoHash.Should().Be("a879261d4e6e792402f92401141a21de70d51bf2");
|
||||||
torrentInfo.MagnetUrl.Should().Be(null);
|
torrentInfo.MagnetUrl.Should().Be(null);
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
|
|||||||
torrentInfo.InfoUrl.Should().Be("https://exoticaz.to/torrent/64040-ssis-419-my-first-experience-is-yua-mikami-from-the-day-i-lost-my-virginity-i-was-devoted-to-sex");
|
torrentInfo.InfoUrl.Should().Be("https://exoticaz.to/torrent/64040-ssis-419-my-first-experience-is-yua-mikami-from-the-day-i-lost-my-virginity-i-was-devoted-to-sex");
|
||||||
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||||
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-06-11 11:04:50"));
|
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-06-11 16:04:50"));
|
||||||
torrentInfo.Size.Should().Be(7085405541);
|
torrentInfo.Size.Should().Be(7085405541);
|
||||||
torrentInfo.InfoHash.Should().Be("asdjfiasdf54asd7f4a2sdf544asdf");
|
torrentInfo.InfoHash.Should().Be("asdjfiasdf54asd7f4a2sdf544asdf");
|
||||||
torrentInfo.MagnetUrl.Should().Be(null);
|
torrentInfo.MagnetUrl.Should().Be(null);
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
|
|||||||
torrentInfo.InfoUrl.Should().Be("https://privatehd.to/torrent/78506-godzilla-2014-2160p-uhd-bluray-remux-hdr-hevc-atmos-triton");
|
torrentInfo.InfoUrl.Should().Be("https://privatehd.to/torrent/78506-godzilla-2014-2160p-uhd-bluray-remux-hdr-hevc-atmos-triton");
|
||||||
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||||
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-03-21 00:24:49"));
|
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-03-21 05:24:49"));
|
||||||
torrentInfo.Size.Should().Be(69914591044);
|
torrentInfo.Size.Should().Be(69914591044);
|
||||||
torrentInfo.InfoHash.Should().Be("a879261d4e6e792402f92401141a21de70d51bf2");
|
torrentInfo.InfoHash.Should().Be("a879261d4e6e792402f92401141a21de70d51bf2");
|
||||||
torrentInfo.MagnetUrl.Should().Be(null);
|
torrentInfo.MagnetUrl.Should().Be(null);
|
||||||
|
|||||||
@@ -71,12 +71,12 @@ namespace NzbDrone.Core.Test.IndexerTests.CardigannTests
|
|||||||
result.Should().Be(expected);
|
result.Should().Be(expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("{{ .Today.Year }}", "2022")]
|
[TestCase("{{ .Today.Year }}")]
|
||||||
public void should_handle_variables_statements(string template, string expected)
|
public void should_handle_variables_statements(string template)
|
||||||
{
|
{
|
||||||
var result = Subject.ApplyGoTemplateText(template, _variables);
|
var result = Subject.ApplyGoTemplateText(template, _variables);
|
||||||
|
|
||||||
result.Should().Be(expected);
|
result.Should().Be(DateTime.Now.Year.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("{{if .False }}0{{else}}1{{end}}", "1")]
|
[TestCase("{{if .False }}0{{else}}1{{end}}", "1")]
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ using Moq;
|
|||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NzbDrone.Common.Http;
|
using NzbDrone.Common.Http;
|
||||||
using NzbDrone.Core.Indexers;
|
using NzbDrone.Core.Indexers;
|
||||||
using NzbDrone.Core.Indexers.FileList;
|
using NzbDrone.Core.Indexers.Definitions.FileList;
|
||||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
using NzbDrone.Core.Test.Framework;
|
using NzbDrone.Core.Test.Framework;
|
||||||
@@ -21,10 +21,15 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
|
|||||||
[SetUp]
|
[SetUp]
|
||||||
public void Setup()
|
public void Setup()
|
||||||
{
|
{
|
||||||
Subject.Definition = new IndexerDefinition()
|
Subject.Definition = new IndexerDefinition
|
||||||
{
|
{
|
||||||
Name = "FileList",
|
Name = "FileList",
|
||||||
Settings = new FileListSettings() { Username = "someuser", Passkey = "somepass" }
|
Settings = new FileListSettings
|
||||||
|
{
|
||||||
|
BaseUrl = "https://filelist.io/",
|
||||||
|
Username = "someuser",
|
||||||
|
Passkey = "somepass"
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,9 +40,9 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
|
|||||||
|
|
||||||
Mocker.GetMock<IIndexerHttpClient>()
|
Mocker.GetMock<IIndexerHttpClient>()
|
||||||
.Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get), Subject.Definition))
|
.Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get), Subject.Definition))
|
||||||
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed)));
|
.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;
|
var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new[] { 2000 } })).Releases;
|
||||||
|
|
||||||
releases.Should().HaveCount(4);
|
releases.Should().HaveCount(4);
|
||||||
releases.First().Should().BeOfType<TorrentInfo>();
|
releases.First().Should().BeOfType<TorrentInfo>();
|
||||||
@@ -50,12 +55,14 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
|
|||||||
torrentInfo.InfoUrl.Should().Be("https://filelist.io/details.php?id=665873");
|
torrentInfo.InfoUrl.Should().Be("https://filelist.io/details.php?id=665873");
|
||||||
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||||
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2020-01-25 22:20:19"));
|
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2020-01-25 20:20:19"));
|
||||||
torrentInfo.Size.Should().Be(8300512414);
|
torrentInfo.Size.Should().Be(8300512414);
|
||||||
torrentInfo.InfoHash.Should().Be(null);
|
torrentInfo.InfoHash.Should().Be(null);
|
||||||
torrentInfo.MagnetUrl.Should().Be(null);
|
torrentInfo.MagnetUrl.Should().Be(null);
|
||||||
torrentInfo.Peers.Should().Be(2 + 12);
|
torrentInfo.Peers.Should().Be(2 + 12);
|
||||||
torrentInfo.Seeders.Should().Be(12);
|
torrentInfo.Seeders.Should().Be(12);
|
||||||
|
|
||||||
|
releases.Any(t => t.IndexerFlags.Contains(IndexerFlag.Internal)).Should().Be(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-21
@@ -3,7 +3,7 @@ using System.Linq;
|
|||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NzbDrone.Core.Indexers;
|
using NzbDrone.Core.Indexers;
|
||||||
using NzbDrone.Core.Indexers.FileList;
|
using NzbDrone.Core.Indexers.Definitions.FileList;
|
||||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||||
using NzbDrone.Core.Test.Framework;
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
|
||||||
@@ -16,34 +16,35 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
|
|||||||
[SetUp]
|
[SetUp]
|
||||||
public void Setup()
|
public void Setup()
|
||||||
{
|
{
|
||||||
Subject.Settings = new FileListSettings()
|
Subject.Settings = new FileListSettings
|
||||||
{
|
{
|
||||||
|
BaseUrl = "https://filelist.io/",
|
||||||
Passkey = "abcd",
|
Passkey = "abcd",
|
||||||
Username = "somename",
|
Username = "somename"
|
||||||
BaseUrl = "https://filelist.io"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Subject.Capabilities = new IndexerCapabilities
|
Subject.Capabilities = new IndexerCapabilities
|
||||||
{
|
{
|
||||||
TvSearchParams = new List<TvSearchParam>
|
TvSearchParams = new List<TvSearchParam>
|
||||||
{
|
{
|
||||||
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
|
TvSearchParam.Q, TvSearchParam.ImdbId, TvSearchParam.Season, TvSearchParam.Ep
|
||||||
},
|
},
|
||||||
MovieSearchParams = new List<MovieSearchParam>
|
MovieSearchParams = new List<MovieSearchParam>
|
||||||
{
|
{
|
||||||
MovieSearchParam.Q, MovieSearchParam.ImdbId
|
MovieSearchParam.Q, MovieSearchParam.ImdbId
|
||||||
},
|
},
|
||||||
MusicSearchParams = new List<MusicSearchParam>
|
MusicSearchParams = new List<MusicSearchParam>
|
||||||
{
|
{
|
||||||
MusicSearchParam.Q
|
MusicSearchParam.Q
|
||||||
},
|
},
|
||||||
BookSearchParams = new List<BookSearchParam>
|
BookSearchParams = new List<BookSearchParam>
|
||||||
{
|
{
|
||||||
BookSearchParam.Q
|
BookSearchParam.Q
|
||||||
},
|
},
|
||||||
Flags = new List<IndexerFlag>
|
Flags = new List<IndexerFlag>
|
||||||
{
|
{
|
||||||
IndexerFlag.FreeLeech
|
IndexerFlag.FreeLeech,
|
||||||
|
IndexerFlag.Internal,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -53,7 +54,7 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
|
|||||||
_movieSearchCriteria = new MovieSearchCriteria
|
_movieSearchCriteria = new MovieSearchCriteria
|
||||||
{
|
{
|
||||||
SearchTerm = "Star Wars",
|
SearchTerm = "Star Wars",
|
||||||
Categories = new int[] { 2000 }
|
Categories = new[] { 2000 }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,13 +66,13 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
|
|||||||
[Test]
|
[Test]
|
||||||
public void should_use_categories_for_feed()
|
public void should_use_categories_for_feed()
|
||||||
{
|
{
|
||||||
var results = Subject.GetSearchRequests(new MovieSearchCriteria { Categories = new int[] { NewznabStandardCategory.MoviesSD.Id, NewznabStandardCategory.MoviesDVD.Id } });
|
var results = Subject.GetSearchRequests(new MovieSearchCriteria { Categories = new[] { NewznabStandardCategory.MoviesSD.Id, NewznabStandardCategory.MoviesDVD.Id } });
|
||||||
|
|
||||||
results.GetAllTiers().Should().HaveCount(1);
|
results.GetAllTiers().Should().HaveCount(1);
|
||||||
|
|
||||||
var page = results.GetAllTiers().First().First();
|
var page = results.GetAllTiers().First().First();
|
||||||
|
|
||||||
page.Url.Query.Should().Contain("&category=1,2&");
|
page.Url.Query.Should().Contain("&category=1%2C2");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -100,7 +101,7 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
|
|||||||
var page = results.GetAllTiers().First().First();
|
var page = results.GetAllTiers().First().First();
|
||||||
|
|
||||||
page.Url.Query.Should().Contain("type=name");
|
page.Url.Query.Should().Contain("type=name");
|
||||||
page.Url.Query.Should().Contain("query=Star Wars");
|
page.Url.Query.Should().Contain("query=Star+Wars");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user