Compare commits

..

148 Commits

Author SHA1 Message Date
ricci2511
193335e2a8 New: Add support for search through url query params 2023-07-09 01:19:05 +03:00
Servarr
1c98727cf3 Automated API Docs update [skip ci] 2023-07-08 19:19:02 +03:00
Bogdan
ab5b321385 New: (UI) Add priority to Indexer Editor 2023-07-08 19:12:54 +03:00
Bogdan
96340909f1 Add translations to SearchFooter 2023-07-08 18:16:02 +03:00
Bogdan
bd6a37dc8c Fixed: (UI) Regain jump to character functionality for search releases 2023-07-08 17:02:01 +03:00
Bogdan
a663cebada Check indexer health checks on bulk updates 2023-07-08 03:52:21 +03:00
Bogdan
2ce5618499 Improve indexer multiple select functionality 2023-07-08 03:13:41 +03:00
Bogdan
94c91d4c3f Fix recursive call in translate() 2023-07-08 03:10:51 +03:00
Bogdan
79fbb2d0d7 New: (UI) Show advanced settings toggle in application modal content 2023-07-07 17:51:12 +03:00
Bogdan
e2e52746bb Fix repeat search when limits are empty 2023-07-07 17:26:56 +03:00
Bogdan
21cc96d683 Fixed: (History) Save limit and offset in history data 2023-07-07 16:21:20 +03:00
Bogdan
e68b45636e Minor refactoring in TorrentsCSV 2023-07-07 13:25:53 +03:00
Servarr
ce68fe4105 Automated API Docs update [skip ci] 2023-07-06 01:29:07 +03:00
Bogdan
712404ddca Show download client field only when download clients are set 2023-07-06 01:07:32 +03:00
ricci2511
826828e8ec New: Add download client per indexer setting 2023-07-06 01:07:32 +03:00
Bogdan
252740519f Remove unused prop in Stats 2023-07-06 00:39:33 +03:00
Bogdan
062fd77e1b Fixed: (UI) Prevent search results clearing when using header search with enter key 2023-07-06 00:17:16 +03:00
Bogdan
6769055b6b Fixed: (TorrentPotato) Allow use of custom APIs 2023-07-06 00:07:50 +03:00
Taloth Saldono
90e92c0b66 Ensure mousetrap instance exists in unbindShortcut
(cherry picked from commit 930742ae2c69a530afe60f76a5824f2722540df8)
2023-07-05 23:02:22 +03:00
Bogdan
7eac11f57a Fixed: (UI) Change default search results sorting to age 2023-07-05 16:52:39 +03:00
Bogdan
02a3c1b224 Align ProwlarrErrorPipeline with upstream 2023-07-04 23:51:10 +03:00
Bogdan
57efa6d0b1 Add Find() to BasicRepository 2023-07-04 22:38:52 +03:00
Qstick
cee52147bc Add package to Sentry release to ensure apps don't mix 2023-07-04 12:21:00 -05:00
Bogdan
a1abcd6c93 Fixed: (History) Reduce History Cleanup Days to 30 2023-07-04 06:56:13 +03:00
Bogdan
18e2757d37 Allow templating in JSON rows selector in Cardigann 2023-07-03 22:45:04 +03:00
Bogdan
8790a6f06a New: (HttpClient) Add HTTP/2 support 2023-07-03 18:55:13 +03:00
Bogdan
4fafdb2cd2 Add x265 categories for Movies and TV in Newznab 2023-07-03 18:54:34 +03:00
Bogdan
bfc06fc8bc Bump version to 1.7.1 2023-07-02 12:01:07 +03:00
Bogdan
9f4f6a5726 Add missing translation for query type 2023-06-29 17:42:59 +03:00
Bogdan
d9ace9a862 Fixed: (Stats) Exclude cached queries from average elapsed time 2023-06-29 16:55:46 +03:00
Bogdan
95691c7476 New: Show query type in history 2023-06-29 16:25:49 +03:00
Bogdan
90f2020e59 Fixed: Misaligned table border in history 2023-06-29 16:08:23 +03:00
Bogdan
6afa1dc8ba Fixed: (Cardigann) Don't check for captcha when captcha answer is empty 2023-06-29 14:43:11 +03:00
Bogdan
e8139f2a5b Fixed: (PornoLab) Moved to YML for Cardigann 2023-06-28 17:56:01 +03:00
Bogdan
45328db2c7 Add close reason to label actions 2023-06-28 15:25:36 +03:00
Bogdan
e55d6b827a Add ContentSummary to HDBits requests 2023-06-27 13:19:57 +03:00
Bogdan
34cd68fa07 Add ContentSummary to BeyondHD requests 2023-06-27 13:19:57 +03:00
Bogdan
aed3f9f887 Create overload for ToJson for Formatting 2023-06-27 13:19:57 +03:00
Bogdan
6880e67507 Fixed: (Apps) Ensure validation for test connection 2023-06-27 06:52:59 +03:00
Bogdan
e0e1b1494e Exclude RSS history events in migration 2023-06-27 05:20:44 +03:00
Bogdan
20df31919d Check for event type to prevent multiple runs on the same row 2023-06-26 20:49:15 +03:00
Bogdan
8785fe02e8 Execute update queries only for certain rows in migration 34 2023-06-26 18:29:14 +03:00
Bogdan
b2b877a8c3 Fix: (UI) Maintain search type and parameters on repeat search 2023-06-26 15:08:31 +03:00
Bogdan
0de302ad48 Don't save empty data in history service 2023-06-26 15:08:31 +03:00
Bogdan
06391489cf Fixed: (Apps) Use forceSave=true to avoid validation warnings 2023-06-26 10:58:14 +03:00
Qstick
8fcceb0702 Bump version to 1.7.0 2023-06-25 20:35:32 -05:00
Bogdan
f20319fff1 Bump version to 1.6.3 2023-06-25 19:15:06 +03:00
Bogdan
20bcc00662 Fix apprise server url migration 2023-06-25 08:38:39 +03:00
Bogdan
c4af3e746f Add more trace logs related info to bug_report.yml [skip ci]
Co-authored-by: Bakerboy448 <55419169+bakerboy448@users.noreply.github.com>
2023-06-24 08:38:50 +03:00
Bogdan
660a162b7e Fixed: (Cardigann) Throw exception only when all download selectors fail 2023-06-23 11:18:14 +03:00
Bogdan
20a3cad7fb Add indexer id in logs for invalid dates in Cardigann definitions 2023-06-23 10:41:25 +03:00
Bogdan
77fe3f78fe Fixed: (Cardigann) Skip to next download selector when max redirects reached
Fixes #578
2023-06-22 17:01:25 +03:00
Bogdan
d777cb8e29 Fixed: (API) Prevent NullRef when searching empty query with a non-default type 2023-06-22 10:36:53 +03:00
Bogdan
15e7cc7ea8 New: (UI) Show indexer categories in info modal 2023-06-20 13:23:06 +03:00
Shivam Dua
04cf061275 Fixed: (UI) Add New Indexer button on search page when no indexers are present
Add missing listeners and components to make add indexer button work on
search page when no indexers are present
2023-06-20 07:33:34 +03:00
Bogdan
d4cdeac69a Fixed: (Cardigann) Definitions with category mapping Other to use 8000 (Other) 2023-06-20 07:21:35 +03:00
Bogdan
e60fe05ee0 Revert "Fix typo botton to bottom"
This reverts commit e2e65627ee.
2023-06-20 05:20:26 +03:00
Bogdan
9a4c23797a Display error when search failed due to all indexers being disabled 2023-06-20 03:05:55 +03:00
Bogdan
acfdb5bae3 New: (UI) Show disabled indexers as disabled options in search page 2023-06-20 03:05:55 +03:00
Bogdan
e2e65627ee Fix typo botton to bottom 2023-06-19 14:31:37 +03:00
Bogdan
4b8906ea62 Cleanup redundant DownloadProtocol in indexers 2023-06-19 04:26:45 +03:00
Bogdan
f0c5d8ceea Minor refactoring in Cardigann definition 2023-06-19 04:08:01 +03:00
Bogdan
427802a50e Update status translations for Indexer index 2023-06-18 15:46:43 +03:00
Bogdan
0c9eae244a Add skip ci to API docs update commit 2023-06-18 15:45:04 +03:00
Bogdan
75ff2f41d3 Update description for freeleech only in BakaBT 2023-06-18 09:37:33 +03:00
Bakerboy448
d1ba208243 Fixed: (HttpIndexerBase) Better HTTP error handling 2023-06-18 08:15:23 +03:00
Bogdan
4e03ebadc4 New: (UI) Add filter by categories in add indexer modal
Fixes #872
Closes #1731
2023-06-18 08:14:39 +03:00
Bogdan
0155ff60fd Map Cardigann capabilities from meta definition 2023-06-18 08:14:35 +03:00
Bogdan
f0915638f3 New: (Apps) Sync Anime Standard Search with Sonarr
Fixes #998
Closes #1732
2023-06-18 07:05:08 +03:00
Bogdan
56eb58aed1 Bump version to 1.6.2 2023-06-18 07:01:38 +03:00
Bogdan
8a891d07cf Test eligibility of the first request in AvistazBase 2023-06-17 14:22:57 +03:00
Bogdan
40a932cd28 Improved page loading errors 2023-06-17 03:36:40 +03:00
Mark McDowall
4a81630073 Fixed: Clearing logs not updating UI once complete
(cherry picked from commit 56b3acddc9f50f59c78c03ca072fe802752b88a7)
2023-06-17 02:27:45 +03:00
Bogdan
0ff0fe2e68 Prevent NullRef when deleting missing backups 2023-06-17 02:08:40 +03:00
Bogdan
51e33740b0 Update import path in CategoryLabel 2023-06-17 01:08:06 +03:00
Bogdan
119164f729 Show indexer privacy in search results 2023-06-16 05:23:07 +03:00
Bogdan
ef0f8e25fd Sort limits in IndexerCapabilities 2023-06-16 05:23:07 +03:00
Bogdan
d21debe77f Convert to 'using' declaration in Housekeeping Tasks 2023-06-16 02:40:47 +03:00
Bogdan
a3ccc3d0cf Close database connections in housekeeping tasks 2023-06-16 02:40:39 +03:00
Bogdan
46d930e903 Apply template text to switch cases in Cardigann 2023-06-16 00:06:11 +03:00
Bogdan
4561859c2b Fixed: (UI) Case-insensitive sorting for add indexer modal 2023-06-14 10:03:02 +03:00
Bogdan
83166fb0b5 Allow array of string as value in EnhancedSelectInput 2023-06-14 07:11:50 +03:00
Bogdan
b98f9a945d Fix use of TmdbId in NewznabRequestGenerator 2023-06-14 04:11:26 +03:00
Bogdan
e658e3fe48 Fixed: (Cardigann) Skip duplicated GET requests 2023-06-12 03:58:02 +03:00
Bogdan
9042525f22 Bump version to 1.6.1 2023-06-11 09:33:46 +03:00
Bogdan
7b551a0af1 Update Anidub description 2023-06-11 07:42:41 +03:00
Bogdan
31c2917bad Fixed: (Indexers) Allow RSS searches in HttpIndexerBase 2023-06-11 03:25:00 +03:00
Bogdan
419cce53f7 Fixed: (Transmission) Set seed limits in client 2023-06-11 03:02:11 +03:00
Bogdan
48cd1d9f6b Reset ContentSummary on redirect in HttpClient 2023-06-11 01:54:13 +03:00
Bogdan
8bd6a313b7 Fixed: (FreeboxDownload) Set seed limits in client 2023-06-10 23:32:06 +03:00
Bogdan
7cb465787e Use more specific styling for kinds in ProgressBar
(cherry picked from commit dd31c913d2a974d95f3be251714ce749cfd99a72)
2023-06-10 02:10:25 +03:00
Bogdan
0b610ff9c8 Cleanse messages for TL 24h RSS feed links 2023-06-09 17:29:49 +03:00
Servarr
5187460298 Automated API Docs update 2023-06-09 16:49:58 +03:00
Bogdan
f0d9b43480 Add some API attributes 2023-06-09 04:12:57 +03:00
Bogdan
a1081cc554 Bump NLog to 5.2.0 2023-06-09 04:12:20 +03:00
Bogdan
c4bb1ba69a Catch JsonReaderException when parsing JSON in Cardigann 2023-06-09 01:37:07 +03:00
Bogdan
3a4c8db98c Add all search types in TorrentRssIndexer
For apps who don't support all categories with normal search, eg. Sonarr
2023-06-08 17:54:16 +03:00
Bogdan
a522796798 Fixed: (Apps) Change the default sync level to Full Sync 2023-06-08 17:47:13 +03:00
Bogdan
e012eda0cf Use the default IndexerCapabilities for TorrentRssIndexer 2023-06-08 00:23:58 +03:00
Bogdan
72ab2b34c4 Cleanse /Users for Mac users 2023-06-07 08:20:14 +03:00
Bogdan
aaba5b7499 Add help link to finding cookies guide 2023-06-07 05:55:13 +03:00
Bogdan
455b76c45c New: Add TorrentRssIndexer
Co-authored-by: ilike2burnthing <59480337+ilike2burnthing@users.noreply.github.com>
2023-06-07 03:57:37 +03:00
Bogdan
596d3297da Move seed configuration logic to TorrentClientBase 2023-06-07 01:32:03 +03:00
Bogdan
d05128ca33 Map seed configuration on release only when it's not null
Fixes #1720
2023-06-06 23:12:52 +03:00
Bogdan
f5b57db753 Check if release still exists in cache when grabbing release 2023-06-06 23:00:12 +03:00
Bogdan
f7d7cca982 Add extension only for known protocols in ReleaseResource 2023-06-06 21:52:44 +03:00
Bogdan
7c5409383e Fixed: (NzbIndex) Use UsenetIndexerBase 2023-06-06 20:10:01 +03:00
Bogdan
98db8f8bf8 Add default definitions for download clients 2023-06-06 19:56:35 +03:00
Bogdan
88e793d76d Fixed: (Cardigann) Allow empty inputs for login.method form/post 2023-06-06 05:59:17 +03:00
Bogdan
0f31af6b89 Fixed: (Cardigann) Allow empty inputs for login.method get 2023-06-06 01:17:27 +03:00
Bogdan
65adf30f59 Fixed: (UTorrent) Set seed limits in client 2023-06-05 17:36:20 +03:00
Bogdan
da75519524 Fixed: (Deluge) Set seed limits in client 2023-06-05 17:22:30 +03:00
Bogdan
ed1fb58242 Align QBittorrent with upstream 2023-06-05 16:41:06 +03:00
Bogdan
d5daf6791c New: Support for seed configuration in DownloadService 2023-06-05 16:41:06 +03:00
Weblate
1f1a345d25 Translations update from Servarr Weblate
Co-authored-by: MoowGlax <matthieu.derouet.pro@gmail.com>
Co-authored-by: Thodoris Kalatzis <teo.kal@hotmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: emacsdias <emacs.dias@gmail.com>
Co-authored-by: reloxx <reloxx@interia.pl>
Co-authored-by: splifter <a.strahlke@gmail.com>
Co-authored-by: victor22265 <843427709@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2023-06-05 16:37:55 +03:00
Bogdan
76a2f51533 Fixed: (HDTorrents) Add login error message 2023-06-05 02:22:49 +03:00
Bogdan
8c0bc9ab4e Filter enabled indexer proxies in Active 2023-06-05 02:22:05 +03:00
Servarr
b0c2b9119b Automated API Docs update 2023-06-05 00:02:28 +03:00
Bogdan
87fdf17926 Add HelpTextWarning support in FieldDefinition 2023-06-04 23:46:02 +03:00
Qstick
0f1b466a19 Bump version to 1.6.0 2023-06-04 15:43:52 -05:00
Bogdan
ea635e685b Fix duplicate indexers with same name in add modal 2023-06-03 05:32:42 +03:00
Bogdan
73f23d56dc Use HelpText for non-URL values 2023-06-02 18:24:14 +03:00
Bogdan
f14ccebf3a Update magnet trackers 2023-06-02 15:16:56 +03:00
Qstick
9539e4d481 More mono cleanup 2023-06-02 00:20:18 -05:00
Bogdan
e40ccc49ad Fixed: (Apps) Prevent null reference exception on sync failure 2023-06-02 07:26:25 +03:00
Qstick
9fd3eb4d6b Extract useSelectState from SelectContext
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
2023-05-31 19:41:55 -05:00
Qstick
78aab80703 New: Additional custom filter predicates for strings 2023-05-31 19:33:28 -05:00
Weblate
868394d588 Translations update from Servarr Weblate
Co-authored-by: Cc95459 <954591059@qq.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Jens <jensmahnke@me.com>
Co-authored-by: Thijs Waalen <contact@thijswaalen.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2023-06-01 01:58:57 +03:00
Bogdan
d5e5697db8 Remove Rarbg tracker from MagnetLinkBuilder 2023-05-31 21:06:14 +03:00
Bogdan
d1e39f206a New: (Rarbg) Obsolete due to tracker shutdown 2023-05-31 18:00:24 +03:00
Bogdan
b59d89f308 Fixed: Don't log handled exceptions in API 2023-05-31 06:53:51 +03:00
Bogdan
bf5855beb4 Revert "Fixed: Don't log handled exceptions in API"
This reverts commit 19ff73dad0.
2023-05-31 06:53:43 +03:00
Bogdan
2d36adf865 Fixed: (Cardigann): Use MissingAttributeEqualsNoResults for Search.Rows.Attribute 2023-05-29 17:35:37 +03:00
Bogdan
ef1ad59f59 Fixed: (Cardigann) Respect the categories from search paths 2023-05-29 00:57:12 +03:00
Servarr
59b6e8af27 Automated API Docs update 2023-05-28 22:31:36 +03:00
Bogdan
3ae1917d3b Fixed: Revert seed criteria validation to warnings
Closes #1692
2023-05-28 22:21:08 +03:00
Bogdan
5864a090e4 Fixed: Enforce validation warnings
(cherry picked from commit 48ee1158ad4213fd0690842e2672f52d08f7ad26)
2023-05-28 22:18:34 +03:00
Bogdan
fcfec1b859 Simplify ShouldHaveApiKey and HasErrors
(cherry picked from commit 7343616a47cd538bba4c9128d2c1094561f9b3a5)
2023-05-28 22:17:31 +03:00
Bogdan
65541017dd Fixed: (RuTracker) Update categories 2023-05-28 20:50:15 +03:00
SetekhZ
7fe9942c28 Fixed: (RuTracker) Use supported 200 categories per search request 2023-05-28 19:40:06 +03:00
Bogdan
360827708f Use 'var' instead of explicit type
(cherry picked from commit 12374f7f0038e5b25548f5ab3f71122410832393)
2023-05-28 18:52:10 +03:00
Bogdan
0509335387 Inline 'out' variable declarations
(cherry picked from commit 281add47de1d3940990156c841362125dea9cc7d)
2023-05-28 18:45:10 +03:00
Bogdan
f54212a809 Standardize variable declaration
(cherry picked from commit 909f2ded6b75998fa8e1addd0dcf849279e7b120)
2023-05-28 18:40:17 +03:00
Bogdan
ea0eb2efa7 Enforce rule IDE0005 on build
(cherry picked from commit 6b1e4ef81938d264a2ddc8b626b0502f799aa640)
2023-05-28 18:39:57 +03:00
Bogdan
ce430433e5 Bump version to 1.5.2 2023-05-28 18:21:49 +03:00
Matthew Strapp
5437aac346 Fixed: Use relative paths instead of absolute paths for webmanifest
(cherry picked from commit 8e771f95ade919a8f1ed7b48675f032a6c508cb2)
2023-05-28 17:33:23 +03:00
Bogdan
b02188acf4 Fixed: (HealthCheck) Check only enabled indexer proxies 2023-05-28 14:55:20 +03:00
Bogdan
6897ed0b3f Fixed: (Anidex) Search with all categories selected 2023-05-28 02:16:06 +03:00
406 changed files with 6028 additions and 4894 deletions

View File

@@ -36,12 +36,18 @@ dotnet_naming_style.instance_field_style.capitalization = camel_case
dotnet_naming_style.instance_field_style.required_prefix = _
# Prefer "var" everywhere
csharp_style_var_for_built_in_types = true:suggestion
csharp_style_var_when_type_is_apparent = true:suggestion
csharp_style_var_elsewhere = true:suggestion
csharp_style_var_for_built_in_types = true
csharp_style_var_when_type_is_apparent = true
csharp_style_var_elsewhere = true
# Prefer "out" variables to be declared inline
csharp_style_inlined_variable_declaration = true
# Using directive is unnecessary.
dotnet_diagnostic.IDE0005.severity = error
# Use var instead of explicit type
dotnet_diagnostic.IDE0007.severity = error
# Inline variable declaration
dotnet_diagnostic.IDE0018.severity = error
# Stylecop Rules
dotnet_diagnostic.SA0001.severity = none

View File

@@ -74,7 +74,7 @@ body:
- type: checkboxes
attributes:
label: Trace Logs have been provided as applicable. Reports may be closed if the required logs are not provided.
description: Trace logs are generally required for all bug reports
description: Trace logs are generally required for all bug reports and contain `trace`. Info logs are invalid for bug reports and do not contain `debug` nor `trace`
options:
- label: I have followed the steps in the wiki link above and provided the required trace logs that are relevant and show this issue.
- label: I have read and followed the steps in the wiki link above and provided the required trace logs - the logs contain `trace` - that are relevant and show this issue.
required: true

View File

@@ -7,6 +7,7 @@
to be a support request. Please hop over onto our [Discord](https://prowlarr.com/discord)
or [Subreddit](https://reddit.com/r/prowlarr)
close: true
close-reason: 'not planned'
'Type: Indexer Request':
comment: >
@@ -14,6 +15,7 @@
for bug reports and feature requests. However, this issue appears
to be a indexer request. Please use our Indexer request [site](https://requests.prowlarr.com/)
close: true
close-reason: 'not planned'
'Status: Logs Needed':
comment: >

View File

@@ -9,7 +9,7 @@ variables:
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '1.5.1'
majorVersion: '1.7.1'
minorVersion: $[counter('minorVersion', 1)]
prowlarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
@@ -362,7 +362,7 @@ stages:
- bash: |
echo "Uploading source maps to sentry"
curl -sL https://sentry.io/get-cli/ | bash
RELEASENAME="${PROWLARRVERSION}-${BUILD_SOURCEBRANCHNAME}"
RELEASENAME="Prowlarr@${PROWLARRVERSION}-${BUILD_SOURCEBRANCHNAME}"
sentry-cli releases new --finalize -p prowlarr -p prowlarr-ui -p prowlarr-update "${RELEASENAME}"
sentry-cli releases -p prowlarr-ui files "${RELEASENAME}" upload-sourcemaps _output/UI/ --rewrite
sentry-cli releases set-commits --auto "${RELEASENAME}"
@@ -1003,7 +1003,7 @@ stages:
git add .
if git status | grep -q modified
then
git commit -am 'Automated API Docs update'
git commit -am 'Automated API Docs update [skip ci]'
git push -f --set-upstream origin api-docs
curl -X POST -H "Authorization: token ${GITHUBTOKEN}" -H "Accept: application/vnd.github.v3+json" https://api.github.com/repos/prowlarr/prowlarr/pulls -d '{"head":"api-docs","base":"develop","title":"Update API docs"}'
else

View File

@@ -1,58 +1,28 @@
import { cloneDeep } from 'lodash';
import React, { useEffect } from 'react';
import areAllSelected from 'Utilities/Table/areAllSelected';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import React, { useCallback, useEffect } from 'react';
import useSelectState, { SelectState } from 'Helpers/Hooks/useSelectState';
import ModelBase from './ModelBase';
export enum SelectActionType {
Reset,
SelectAll,
UnselectAll,
ToggleSelected,
RemoveItem,
UpdateItems,
}
type SelectedState = Record<number, boolean>;
interface SelectState {
selectedState: SelectedState;
lastToggled: number | null;
allSelected: boolean;
allUnselected: boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
items: any[];
}
type SelectAction =
| { type: SelectActionType.Reset }
| { type: SelectActionType.SelectAll }
| { type: SelectActionType.UnselectAll }
export type SelectContextAction =
| { type: 'reset' }
| { type: 'selectAll' }
| { type: 'unselectAll' }
| {
type: SelectActionType.ToggleSelected;
type: 'toggleSelected';
id: number;
isSelected: boolean;
shiftKey: boolean;
}
| {
type: SelectActionType.RemoveItem;
type: 'removeItem';
id: number;
}
| {
type: SelectActionType.UpdateItems;
type: 'updateItems';
items: ModelBase[];
};
type Dispatch = (action: SelectAction) => void;
const initialState = {
selectedState: {},
lastToggled: null,
allSelected: false,
allUnselected: true,
items: [],
};
export type SelectDispatch = (action: SelectContextAction) => void;
interface SelectProviderOptions<T extends ModelBase> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -60,90 +30,40 @@ interface SelectProviderOptions<T extends ModelBase> {
items: Array<T>;
}
function getSelectedState(items: ModelBase[], existingState: SelectedState) {
return items.reduce((acc: SelectedState, item) => {
const id = item.id;
acc[id] = existingState[id] ?? false;
return acc;
}, {});
}
// TODO: Can this be reused?
const SelectContext = React.createContext<[SelectState, Dispatch] | undefined>(
cloneDeep(undefined)
);
function selectReducer(state: SelectState, action: SelectAction): SelectState {
const { items, selectedState } = state;
switch (action.type) {
case SelectActionType.Reset: {
return cloneDeep(initialState);
}
case SelectActionType.SelectAll: {
return {
items,
...selectAll(selectedState, true),
};
}
case SelectActionType.UnselectAll: {
return {
items,
...selectAll(selectedState, false),
};
}
case SelectActionType.ToggleSelected: {
const result = {
items,
...toggleSelected(
state,
items,
action.id,
action.isSelected,
action.shiftKey
),
};
return result;
}
case SelectActionType.UpdateItems: {
const nextSelectedState = getSelectedState(action.items, selectedState);
return {
...state,
...areAllSelected(nextSelectedState),
selectedState: nextSelectedState,
items: action.items,
};
}
default: {
throw new Error(`Unhandled action type: ${action.type}`);
}
}
}
const SelectContext = React.createContext<
[SelectState, SelectDispatch] | undefined
>(cloneDeep(undefined));
export function SelectProvider<T extends ModelBase>(
props: SelectProviderOptions<T>
) {
const { items } = props;
const selectedState = getSelectedState(items, {});
const [state, dispatch] = useSelectState();
const [state, dispatch] = React.useReducer(selectReducer, {
selectedState,
lastToggled: null,
allSelected: false,
allUnselected: true,
items,
});
const dispatchWrapper = useCallback(
(action: SelectContextAction) => {
switch (action.type) {
case 'reset':
case 'removeItem':
dispatch(action);
break;
const value: [SelectState, Dispatch] = [state, dispatch];
default:
dispatch({
...action,
items,
});
break;
}
},
[items, dispatch]
);
const value: [SelectState, SelectDispatch] = [state, dispatchWrapper];
useEffect(() => {
dispatch({ type: SelectActionType.UpdateItems, items });
}, [items]);
dispatch({ type: 'updateItems', items });
}, [items, dispatch]);
return (
<SelectContext.Provider value={value}>

View File

@@ -5,6 +5,7 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import sortByName from 'Utilities/Array/sortByName';
import translate from 'Utilities/String/translate';
import SelectInput from './SelectInput';
function createMapStateToProps() {
@@ -23,7 +24,7 @@ function createMapStateToProps() {
if (includeNoChange) {
values.unshift({
key: 'noChange',
value: 'No Change',
value: translate('NoChange'),
disabled: true
});
}

View File

@@ -0,0 +1,98 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchDownloadClients } from 'Store/Actions/settingsActions';
import sortByName from 'Utilities/Array/sortByName';
import EnhancedSelectInput from './EnhancedSelectInput';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.downloadClients,
(state, { includeAny }) => includeAny,
(state, { protocol }) => protocol,
(downloadClients, includeAny, protocolFilter) => {
const {
isFetching,
isPopulated,
error,
items
} = downloadClients;
const values = items
.filter((downloadClient) => downloadClient.protocol === protocolFilter)
.sort(sortByName)
.map((downloadClient) => ({
key: downloadClient.id,
value: downloadClient.name
}));
if (includeAny) {
values.unshift({
key: 0,
value: '(Any)'
});
}
return {
isFetching,
isPopulated,
error,
values
};
}
);
}
const mapDispatchToProps = {
dispatchFetchDownloadClients: fetchDownloadClients
};
class DownloadClientSelectInputConnector extends Component {
//
// Lifecycle
componentDidMount() {
if (!this.props.isPopulated) {
this.props.dispatchFetchDownloadClients();
}
}
//
// Listeners
onChange = ({ name, value }) => {
this.props.onChange({ name, value: parseInt(value) });
};
//
// Render
render() {
return (
<EnhancedSelectInput
{...this.props}
onChange={this.onChange}
/>
);
}
}
DownloadClientSelectInputConnector.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
name: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
values: PropTypes.arrayOf(PropTypes.object).isRequired,
includeAny: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired,
dispatchFetchDownloadClients: PropTypes.func.isRequired
};
DownloadClientSelectInputConnector.defaultProps = {
includeAny: false,
protocol: 'torrent'
};
export default connect(createMapStateToProps, mapDispatchToProps)(DownloadClientSelectInputConnector);

View File

@@ -578,7 +578,7 @@ EnhancedSelectInput.propTypes = {
className: PropTypes.string,
disabledClassName: PropTypes.string,
name: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.arrayOf(PropTypes.number)]).isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.arrayOf(PropTypes.string), PropTypes.arrayOf(PropTypes.number)]).isRequired,
values: PropTypes.arrayOf(PropTypes.object).isRequired,
isDisabled: PropTypes.bool.isRequired,
isFetching: PropTypes.bool.isRequired,

View File

@@ -10,6 +10,7 @@ import CaptchaInputConnector from './CaptchaInputConnector';
import CardigannCaptchaInputConnector from './CardigannCaptchaInputConnector';
import CheckInput from './CheckInput';
import DeviceInputConnector from './DeviceInputConnector';
import DownloadClientSelectInputConnector from './DownloadClientSelectInputConnector';
import EnhancedSelectInput from './EnhancedSelectInput';
import EnhancedSelectInputConnector from './EnhancedSelectInputConnector';
import FormInputHelpText from './FormInputHelpText';
@@ -72,6 +73,9 @@ function getComponent(type) {
case inputTypes.CATEGORY_SELECT:
return NewznabCategorySelectInputConnector;
case inputTypes.DOWNLOAD_CLIENT_SELECT:
return DownloadClientSelectInputConnector;
case inputTypes.INDEXER_FLAGS_SELECT:
return IndexerFlagsSelectInputConnector;
@@ -258,6 +262,8 @@ FormInputGroup.propTypes = {
values: PropTypes.arrayOf(PropTypes.any),
type: PropTypes.string.isRequired,
kind: PropTypes.oneOf(kinds.all),
min: PropTypes.number,
max: PropTypes.number,
unit: PropTypes.string,
buttons: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
helpText: PropTypes.string,

View File

@@ -12,7 +12,7 @@ function createMapStateToProps() {
(state) => state.indexers,
(value, indexers) => {
const values = [];
const groupedIndexers = _(indexers.items).groupBy((x) => x.protocol).map((val, key) => ({ protocol: key, indexers: val })).value();
const groupedIndexers = _.map(_.groupBy(indexers.items, 'protocol'), (val, key) => ({ protocol: key, indexers: val }));
groupedIndexers.forEach((element) => {
values.push({
@@ -21,10 +21,11 @@ function createMapStateToProps() {
});
if (element.indexers && element.indexers.length > 0) {
element.indexers.forEach((subCat) => {
element.indexers.forEach((indexer) => {
values.push({
key: subCat.id,
value: subCat.name,
key: indexer.id,
value: indexer.name,
isDisabled: !indexer.enable,
parentKey: element.protocol === 'usenet' ? -1 : -2
});
});

View File

@@ -67,6 +67,7 @@ function ProviderFieldFormGroup(props) {
name,
label,
helpText,
helpTextWarning,
helpLink,
placeholder,
value,
@@ -100,6 +101,7 @@ function ProviderFieldFormGroup(props) {
name={name}
label={label}
helpText={helpText}
helpTextWarning={helpTextWarning}
helpLink={helpLink}
placeholder={placeholder}
value={value}
@@ -126,6 +128,7 @@ ProviderFieldFormGroup.propTypes = {
name: PropTypes.string.isRequired,
label: PropTypes.string,
helpText: PropTypes.string,
helpTextWarning: PropTypes.string,
helpLink: PropTypes.string,
placeholder: PropTypes.string,
value: PropTypes.any,

View File

@@ -1,6 +1,8 @@
import PropTypes from 'prop-types';
import React from 'react';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import { kinds } from 'Helpers/Props';
function PageSectionContent(props) {
const {
@@ -17,7 +19,7 @@ function PageSectionContent(props) {
);
} else if (!isFetching && !!error) {
return (
<div>{errorMessage}</div>
<Alert kind={kinds.DANGER}>{errorMessage}</Alert>
);
} else if (isPopulated && !error) {
return (

View File

@@ -16,6 +16,38 @@
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
color: var(--white);
transition: width 0.6s ease;
&.primary {
background-color: var(--primaryColor);
}
&.danger {
background-color: var(--dangerColor);
&:global(.colorImpaired) {
background: repeating-linear-gradient(90deg, color(var(--dangerColor) shade(5%)), color(var(--dangerColor) shade(5%)) 5px, color(var(--dangerColor) shade(15%)) 5px, color(var(--dangerColor) shade(15%)) 10px);
}
}
&.success {
background-color: var(--successColor);
}
&.purple {
background-color: var(--purple);
}
&.warning {
background-color: var(--warningColor);
&:global(.colorImpaired) {
background: repeating-linear-gradient(45deg, var(--warningColor), var(--warningColor) 5px, color(var(--warningColor) tint(15%)) 5px, color(var(--warningColor) tint(15%)) 10px);
}
}
&.info {
background-color: var(--infoColor);
}
}
.frontTextContainer {
@@ -41,38 +73,6 @@
cursor: default;
}
.primary {
background-color: var(--primaryColor);
}
.danger {
background-color: var(--dangerColor);
&:global(.colorImpaired) {
background: repeating-linear-gradient(90deg, color(var(--dangerColor) shade(5%)), color(var(--dangerColor) shade(5%)) 5px, color(var(--dangerColor) shade(15%)) 5px, color(var(--dangerColor) shade(15%)) 10px);
}
}
.success {
background-color: var(--successColor);
}
.purple {
background-color: var(--purple);
}
.warning {
background-color: var(--warningColor);
&:global(.colorImpaired) {
background: repeating-linear-gradient(45deg, var(--warningColor), var(--warningColor) 5px, color(var(--warningColor) tint(15%)) 5px, color(var(--warningColor) tint(15%)) 10px);
}
}
.info {
background-color: var(--infoColor);
}
.small {
height: $progressBarSmallHeight;

View File

@@ -38,7 +38,7 @@ function ProgressBar(props) {
{
showText && width ?
<div
className={styles.backTextContainer}
className={classNames(styles.backTextContainer, styles[kind])}
style={{ width: actualWidth }}
>
<div className={styles.backText}>
@@ -67,7 +67,7 @@ function ProgressBar(props) {
{
showText ?
<div
className={styles.frontTextContainer}
className={classNames(styles.frontTextContainer, styles[kind])}
style={{ width: progressPercent }}
>
<div

View File

@@ -39,7 +39,8 @@ class VirtualTable extends Component {
super(props, context);
this.state = {
width: 0
width: 0,
scrollRestored: false
};
this._grid = null;
@@ -48,20 +49,25 @@ class VirtualTable extends Component {
componentDidUpdate(prevProps, prevState) {
const {
items,
scrollIndex
scrollIndex,
scrollTop
} = this.props;
const {
width
width,
scrollRestored
} = this.state;
if (this._grid &&
(prevState.width !== width ||
hasDifferentItemsOrOrder(prevProps.items, items))) {
if (this._grid && (prevState.width !== width || hasDifferentItemsOrOrder(prevProps.items, items))) {
// recomputeGridSize also forces Grid to discard its cache of rendered cells
this._grid.recomputeGridSize();
}
if (this._grid && scrollTop !== undefined && scrollTop !== 0 && !scrollRestored) {
this.setState({ scrollRestored: true });
this._grid.scrollToPosition({ scrollTop });
}
if (scrollIndex != null && scrollIndex !== prevProps.scrollIndex) {
this._grid.scrollToCell({
rowIndex: scrollIndex,
@@ -98,6 +104,7 @@ class VirtualTable extends Component {
focusScroller,
header,
headerHeight,
rowHeight,
rowRenderer,
...otherProps
} = this.props;
@@ -141,6 +148,7 @@ class VirtualTable extends Component {
{header}
<div ref={registerChild}>
<Grid
{...otherProps}
ref={this.setGridRef}
autoContainerWidth={true}
autoHeight={true}
@@ -148,7 +156,7 @@ class VirtualTable extends Component {
width={width}
height={height}
headerHeight={height - headerHeight}
rowHeight={ROW_HEIGHT}
rowHeight={rowHeight}
rowCount={items.length}
columnCount={1}
columnWidth={width}
@@ -162,7 +170,6 @@ class VirtualTable extends Component {
className={styles.tableBodyContainer}
style={gridStyle}
containerStyle={containerStyle}
{...otherProps}
/>
</div>
</Scroller>
@@ -180,16 +187,19 @@ VirtualTable.propTypes = {
className: PropTypes.string.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
scrollIndex: PropTypes.number,
scrollTop: PropTypes.number,
scroller: PropTypes.instanceOf(Element).isRequired,
focusScroller: PropTypes.bool.isRequired,
header: PropTypes.node.isRequired,
headerHeight: PropTypes.number.isRequired,
rowRenderer: PropTypes.func.isRequired
rowRenderer: PropTypes.func.isRequired,
rowHeight: PropTypes.number.isRequired
};
VirtualTable.defaultProps = {
className: styles.tableContainer,
headerHeight: 38,
rowHeight: ROW_HEIGHT,
focusScroller: true
};

View File

@@ -67,8 +67,10 @@ function keyboardShortcuts(WrappedComponent) {
};
unbindShortcut = (key) => {
delete this._mousetrapBindings[key];
this._mousetrap.unbind(key);
if (this._mousetrap != null) {
delete this._mousetrapBindings[key];
this._mousetrap.unbind(key);
}
};
unbindAllShortcuts = () => {

View File

@@ -1,18 +1,19 @@
{
"name": "",
"name": "Prowlarr",
"icons": [
{
"src": "/Content/Images/Icons/android-chrome-192x192.png",
"src": "android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/Content/Images/Icons/android-chrome-512x512.png",
"src": "android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"start_url": "../../../../",
"theme_color": "#3a3f51",
"background_color": "#3a3f51",
"display": "standalone"
}
}

View File

@@ -0,0 +1,11 @@
import { useEffect, useRef } from 'react';
export default function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}

View File

@@ -0,0 +1,113 @@
import { cloneDeep } from 'lodash';
import { useReducer } from 'react';
import ModelBase from 'App/ModelBase';
import areAllSelected from 'Utilities/Table/areAllSelected';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
export type SelectedState = Record<number, boolean>;
export interface SelectState {
selectedState: SelectedState;
lastToggled: number | null;
allSelected: boolean;
allUnselected: boolean;
}
export type SelectAction =
| { type: 'reset' }
| { type: 'selectAll'; items: ModelBase[] }
| { type: 'unselectAll'; items: ModelBase[] }
| {
type: 'toggleSelected';
id: number;
isSelected: boolean;
shiftKey: boolean;
items: ModelBase[];
}
| {
type: 'removeItem';
id: number;
}
| {
type: 'updateItems';
items: ModelBase[];
};
export type Dispatch = (action: SelectAction) => void;
const initialState = {
selectedState: {},
lastToggled: null,
allSelected: false,
allUnselected: true,
items: [],
};
function getSelectedState(items: ModelBase[], existingState: SelectedState) {
return items.reduce((acc: SelectedState, item) => {
const id = item.id;
acc[id] = existingState[id] ?? false;
return acc;
}, {});
}
function selectReducer(state: SelectState, action: SelectAction): SelectState {
const { selectedState } = state;
switch (action.type) {
case 'reset': {
return cloneDeep(initialState);
}
case 'selectAll': {
return {
...selectAll(selectedState, true),
};
}
case 'unselectAll': {
return {
...selectAll(selectedState, false),
};
}
case 'toggleSelected': {
const result = {
...toggleSelected(
state,
action.items,
action.id,
action.isSelected,
action.shiftKey
),
};
return result;
}
case 'updateItems': {
const nextSelectedState = getSelectedState(action.items, selectedState);
return {
...state,
...areAllSelected(nextSelectedState),
selectedState: nextSelectedState,
};
}
default: {
throw new Error(`Unhandled action type: ${action.type}`);
}
}
}
export default function useSelectState(): [SelectState, Dispatch] {
const selectedState = getSelectedState([], {});
const [state, dispatch] = useReducer(selectReducer, {
selectedState,
lastToggled: null,
allSelected: false,
allUnselected: true,
});
return [state, dispatch];
}

View File

@@ -1,14 +1,18 @@
import * as filterTypes from './filterTypes';
export const ARRAY = 'array';
export const CONTAINS = 'contains';
export const DATE = 'date';
export const EQUAL = 'equal';
export const EXACT = 'exact';
export const NUMBER = 'number';
export const STRING = 'string';
export const all = [
ARRAY,
CONTAINS,
DATE,
EQUAL,
EXACT,
NUMBER,
STRING
@@ -20,6 +24,10 @@ export const possibleFilterTypes = {
{ key: filterTypes.NOT_CONTAINS, value: 'does not contain' }
],
[CONTAINS]: [
{ key: filterTypes.CONTAINS, value: 'contains' }
],
[DATE]: [
{ key: filterTypes.LESS_THAN, value: 'is before' },
{ key: filterTypes.GREATER_THAN, value: 'is after' },
@@ -29,6 +37,10 @@ export const possibleFilterTypes = {
{ key: filterTypes.NOT_IN_NEXT, value: 'not in the next' }
],
[EQUAL]: [
{ key: filterTypes.EQUAL, value: 'is' }
],
[EXACT]: [
{ key: filterTypes.EQUAL, value: 'is' },
{ key: filterTypes.NOT_EQUAL, value: 'is not' }
@@ -47,6 +59,10 @@ export const possibleFilterTypes = {
{ key: filterTypes.CONTAINS, value: 'contains' },
{ key: filterTypes.NOT_CONTAINS, value: 'does not contain' },
{ key: filterTypes.EQUAL, value: 'equal' },
{ key: filterTypes.NOT_EQUAL, value: 'not equal' }
{ key: filterTypes.NOT_EQUAL, value: 'not equal' },
{ key: filterTypes.STARTS_WITH, value: 'starts with' },
{ key: filterTypes.NOT_STARTS_WITH, value: 'does not start with' },
{ key: filterTypes.ENDS_WITH, value: 'ends with' },
{ key: filterTypes.NOT_ENDS_WITH, value: 'does not end with' }
]
};

View File

@@ -39,6 +39,22 @@ const filterTypePredicates = {
[filterTypes.NOT_EQUAL]: function(itemValue, filterValue) {
return itemValue !== filterValue;
},
[filterTypes.STARTS_WITH]: function(itemValue, filterValue) {
return itemValue.toLowerCase().startsWith(filterValue.toLowerCase());
},
[filterTypes.NOT_STARTS_WITH]: function(itemValue, filterValue) {
return !itemValue.toLowerCase().startsWith(filterValue.toLowerCase());
},
[filterTypes.ENDS_WITH]: function(itemValue, filterValue) {
return itemValue.toLowerCase().endsWith(filterValue.toLowerCase());
},
[filterTypes.NOT_ENDS_WITH]: function(itemValue, filterValue) {
return !itemValue.toLowerCase().endsWith(filterValue.toLowerCase());
}
};

View File

@@ -10,6 +10,10 @@ export const LESS_THAN = 'lessThan';
export const LESS_THAN_OR_EQUAL = 'lessThanOrEqual';
export const NOT_CONTAINS = 'notContains';
export const NOT_EQUAL = 'notEqual';
export const STARTS_WITH = 'startsWith';
export const NOT_STARTS_WITH = 'notStartsWith';
export const ENDS_WITH = 'endsWith';
export const NOT_ENDS_WITH = 'notEndsWith';
export const all = [
CONTAINS,
@@ -23,5 +27,9 @@ export const all = [
IN_LAST,
NOT_IN_LAST,
IN_NEXT,
NOT_IN_NEXT
NOT_IN_NEXT,
STARTS_WITH,
NOT_STARTS_WITH,
ENDS_WITH,
NOT_ENDS_WITH
];

View File

@@ -9,6 +9,7 @@ export const KEY_VALUE_LIST = 'keyValueList';
export const INFO = 'info';
export const MOVIE_MONITORED_SELECT = 'movieMonitoredSelect';
export const CATEGORY_SELECT = 'newznabCategorySelect';
export const DOWNLOAD_CLIENT_SELECT = 'downloadClientSelect';
export const NUMBER = 'number';
export const OAUTH = 'oauth';
export const PASSWORD = 'password';

View File

@@ -18,6 +18,8 @@ function HistoryDetails(props) {
query,
queryResults,
categories,
limit,
offset,
source,
url
} = data;
@@ -31,43 +33,66 @@ function HistoryDetails(props) {
/>
{
!!indexer &&
indexer ?
<DescriptionListItem
title={translate('Indexer')}
data={indexer.name}
/>
/> :
null
}
{
!!data &&
data ?
<DescriptionListItem
title={translate('QueryResults')}
data={queryResults ? queryResults : '-'}
/>
/> :
null
}
{
!!data &&
data ?
<DescriptionListItem
title={translate('Categories')}
data={categories ? categories : '-'}
/>
/> :
null
}
{
!!data &&
limit ?
<DescriptionListItem
title={translate('Limit')}
data={limit}
/> :
null
}
{
offset ?
<DescriptionListItem
title={translate('Offset')}
data={offset}
/> :
null
}
{
data ?
<DescriptionListItem
title={translate('Source')}
data={source}
/>
/> :
null
}
{
!!data &&
data ?
<DescriptionListItem
title={translate('Url')}
data={url ? <Link to={url}>{translate('Link')}</Link> : '-'}
/>
/> :
null
}
</DescriptionList>
);
@@ -76,42 +101,46 @@ function HistoryDetails(props) {
if (eventType === 'releaseGrabbed') {
const {
source,
title,
grabTitle,
url
} = data;
return (
<DescriptionList>
{
!!indexer &&
indexer ?
<DescriptionListItem
title={translate('Indexer')}
data={indexer.name}
/>
/> :
null
}
{
!!data &&
data ?
<DescriptionListItem
title={translate('Source')}
data={source ? source : '-'}
/>
/> :
null
}
{
!!data &&
data ?
<DescriptionListItem
title={translate('Title')}
data={title ? title : '-'}
/>
title={translate('GrabTitle')}
data={grabTitle ? grabTitle : '-'}
/> :
null
}
{
!!data &&
data ?
<DescriptionListItem
title={translate('Url')}
data={url ? <Link to={url}>{translate('Link')}</Link> : '-'}
/>
/> :
null
}
</DescriptionList>
);
@@ -124,11 +153,12 @@ function HistoryDetails(props) {
title={translate('Auth')}
>
{
!!indexer &&
indexer ?
<DescriptionListItem
title={translate('Indexer')}
data={indexer.name}
/>
/> :
null
}
</DescriptionList>
);

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FilterMenu from 'Components/Menu/FilterMenu';
import ConfirmModal from 'Components/Modal/ConfirmModal';
@@ -121,9 +122,9 @@ class History extends Component {
{
!isFetchingAny && hasError &&
<div>
<Alert kind={kinds.DANGER}>
{translate('UnableToLoadHistory')}
</div>
</Alert>
}
{
@@ -131,9 +132,9 @@ class History extends Component {
// wait for the episodes to populate because they are never coming.
isPopulated && !hasError && !items.length &&
<div>
No history found
</div>
<Alert kind={kinds.INFO}>
{translate('NoHistoryFound')}
</Alert>
}
{

View File

@@ -26,9 +26,7 @@
width: 70px;
}
.parameters {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
.parametersContent {
display: flex;
flex-wrap: wrap;
}

View File

@@ -6,7 +6,7 @@ interface CssExports {
'details': string;
'elapsedTime': string;
'indexer': string;
'parameters': string;
'parametersContent': string;
'query': string;
'releaseGroup': string;
'source': string;

View File

@@ -1,17 +1,39 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import { icons } from 'Helpers/Props';
import { icons, kinds } from 'Helpers/Props';
import CapabilitiesLabel from 'Indexer/Index/Table/CapabilitiesLabel';
import translate from 'Utilities/String/translate';
import HistoryDetailsModal from './Details/HistoryDetailsModal';
import * as historyDataTypes from './historyDataTypes';
import HistoryEventTypeCell from './HistoryEventTypeCell';
import HistoryRowParameter from './HistoryRowParameter';
import styles from './HistoryRow.css';
const historyParameters = [
{ key: historyDataTypes.IMDB_ID, title: 'IMDb' },
{ key: historyDataTypes.TMDB_ID, title: 'TMDb' },
{ key: historyDataTypes.TVDB_ID, title: 'TVDb' },
{ key: historyDataTypes.TRAKT_ID, title: 'Trakt' },
{ key: historyDataTypes.R_ID, title: 'TvRage' },
{ key: historyDataTypes.TVMAZE_ID, title: 'TvMaze' },
{ key: historyDataTypes.SEASON, title: translate('Season') },
{ key: historyDataTypes.EPISODE, title: translate('Episode') },
{ key: historyDataTypes.ARTIST, title: translate('Artist') },
{ key: historyDataTypes.ALBUM, title: translate('Album') },
{ key: historyDataTypes.LABEL, title: translate('Label') },
{ key: historyDataTypes.TRACK, title: translate('Track') },
{ key: historyDataTypes.YEAR, title: translate('Year') },
{ key: historyDataTypes.GENRE, title: translate('Genre') },
{ key: historyDataTypes.AUTHOR, title: translate('Author') },
{ key: historyDataTypes.TITLE, title: translate('Title') },
{ key: historyDataTypes.PUBLISHER, title: translate('Publisher') }
];
class HistoryRow extends Component {
//
@@ -44,15 +66,52 @@ class HistoryRow extends Component {
data
} = this.props;
const { query, queryType, limit, offset } = data;
let searchQuery = query;
let categories = [];
if (data.categories) {
categories = data.categories.split(',').map((item) => {
return parseInt(item);
});
categories = data.categories.split(',').map((item) => parseInt(item));
}
this.props.onSearchPress(data.query, indexer.id, categories);
const searchParams = [
historyDataTypes.IMDB_ID,
historyDataTypes.TMDB_ID,
historyDataTypes.TVDB_ID,
historyDataTypes.TRAKT_ID,
historyDataTypes.R_ID,
historyDataTypes.TVMAZE_ID,
historyDataTypes.SEASON,
historyDataTypes.EPISODE,
historyDataTypes.ARTIST,
historyDataTypes.ALBUM,
historyDataTypes.LABEL,
historyDataTypes.TRACK,
historyDataTypes.YEAR,
historyDataTypes.GENRE,
historyDataTypes.AUTHOR,
historyDataTypes.TITLE,
historyDataTypes.PUBLISHER
]
.reduce((acc, key) => {
if (key in data && data[key].length > 0) {
const value = data[key];
acc.push({ key, value });
}
return acc;
}, [])
.map((item) => `{${item.key}:${item.value}}`)
.join('')
;
if (searchParams.length > 0) {
searchQuery += `${searchParams}`;
}
this.props.onSearchPress(searchQuery, indexer.id, categories, queryType, parseInt(limit), parseInt(offset));
};
onDetailsPress = () => {
@@ -84,6 +143,8 @@ class HistoryRow extends Component {
return null;
}
const parameters = historyParameters.filter((parameter) => parameter.key in data && data[parameter.key]);
return (
<TableRow>
{
@@ -133,162 +194,19 @@ class HistoryRow extends Component {
if (name === 'parameters') {
return (
<TableRowCell
key={name}
className={styles.parameters}
>
{
data.imdbId ?
<HistoryRowParameter
title='IMDb'
value={data.imdbId}
/> :
null
}
{
data.tmdbId ?
<HistoryRowParameter
title='TMDb'
value={data.tmdbId}
/> :
null
}
{
data.tvdbId ?
<HistoryRowParameter
title='TVDb'
value={data.tvdbId}
/> :
null
}
{
data.traktId ?
<HistoryRowParameter
title='Trakt'
value={data.traktId}
/> :
null
}
{
data.rId ?
<HistoryRowParameter
title='TvRage'
value={data.rId}
/> :
null
}
{
data.tvMazeId ?
<HistoryRowParameter
title='TvMaze'
value={data.tvMazeId}
/> :
null
}
{
data.season ?
<HistoryRowParameter
title={translate('Season')}
value={data.season}
/> :
null
}
{
data.episode ?
<HistoryRowParameter
title={translate('Episode')}
value={data.episode}
/> :
null
}
{
data.artist ?
<HistoryRowParameter
title={translate('Artist')}
value={data.artist}
/> :
null
}
{
data.album ?
<HistoryRowParameter
title={translate('Album')}
value={data.album}
/> :
null
}
{
data.label ?
<HistoryRowParameter
title={translate('Label')}
value={data.label}
/> :
null
}
{
data.track ?
<HistoryRowParameter
title={translate('Track')}
value={data.track}
/> :
null
}
{
data.year ?
<HistoryRowParameter
title={translate('Year')}
value={data.year}
/> :
null
}
{
data.genre ?
<HistoryRowParameter
title={translate('Genre')}
value={data.genre}
/> :
null
}
{
data.author ?
<HistoryRowParameter
title={translate('Author')}
value={data.author}
/> :
null
}
{
data.bookTitle ?
<HistoryRowParameter
title={translate('Book')}
value={data.bookTitle}
/> :
null
}
{
data.publisher ?
<HistoryRowParameter
title={translate('Publisher')}
value={data.publisher}
/> :
null
}
<TableRowCell key={name}>
<div className={styles.parametersContent}>
{parameters.map((parameter) => {
return (
<HistoryRowParameter
key={parameter.key}
title={parameter.title}
value={data[parameter.key]}
/>
);
}
)}
</div>
</TableRowCell>
);
}
@@ -300,8 +218,25 @@ class HistoryRow extends Component {
className={styles.indexer}
>
{
data.title ?
data.title :
data.grabTitle ?
data.grabTitle :
null
}
</TableRowCell>
);
}
if (name === 'queryType') {
return (
<TableRowCell
key={name}
className={styles.query}
>
{
data.queryType ?
<Label kind={kinds.INFO}>
{data.queryType}
</Label> :
null
}
</TableRowCell>
@@ -377,6 +312,12 @@ class HistoryRow extends Component {
key={name}
className={styles.details}
>
<IconButton
name={icons.INFO}
onPress={this.onDetailsPress}
title={translate('HistoryDetails')}
/>
{
eventType === 'indexerQuery' ?
<IconButton
@@ -386,11 +327,6 @@ class HistoryRow extends Component {
/> :
null
}
<IconButton
name={icons.INFO}
onPress={this.onDetailsPress}
title={translate('HistoryDetails')}
/>
</TableRowCell>
);
}

View File

@@ -1,4 +1,5 @@
import { push } from 'connected-react-router';
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
@@ -48,8 +49,15 @@ class HistoryRowConnector extends Component {
//
// Listeners
onSearchPress = (term, indexerId, categories) => {
this.props.setSearchDefault({ searchQuery: term, searchIndexerIds: [indexerId], searchCategories: categories });
onSearchPress = (query, indexerId, categories, type, limit, offset) => {
this.props.setSearchDefault(_.pickBy({
searchQuery: query,
searchIndexerIds: [indexerId],
searchCategories: categories,
searchType: type,
searchLimit: limit,
searchOffset: offset
}));
this.props.push(`${window.Prowlarr.urlBase}/search`);
};

View File

@@ -0,0 +1,17 @@
export const IMDB_ID = 'imdbId';
export const TMDB_ID = 'tmdbId';
export const TVDB_ID = 'tvdbId';
export const TRAKT_ID = 'traktId';
export const R_ID = 'rId';
export const TVMAZE_ID = 'tvMazeId';
export const SEASON = 'season';
export const EPISODE = 'episode';
export const ARTIST = 'artist';
export const ALBUM = 'album';
export const LABEL = 'label';
export const TRACK = 'track';
export const YEAR = 'year';
export const GENRE = 'genre';
export const AUTHOR = 'author';
export const TITLE = 'title';
export const PUBLISHER = 'publisher';

View File

@@ -40,6 +40,7 @@
flex: 1;
flex-direction: column;
margin-right: 12px;
max-width: 50%;
}
.filterContainer:last-child {

View File

@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import EnhancedSelectInput from 'Components/Form/EnhancedSelectInput';
import NewznabCategorySelectInputConnector from 'Components/Form/NewznabCategorySelectInputConnector';
import TextInput from 'Components/Form/TextInput';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
@@ -26,7 +27,7 @@ const columns = [
isVisible: true
},
{
name: 'name',
name: 'sortName',
label: translate('Name'),
isSortable: true,
isVisible: true
@@ -89,7 +90,8 @@ class AddIndexerModalContent extends Component {
filter: '',
filterProtocols: [],
filterLanguages: [],
filterPrivacyLevels: []
filterPrivacyLevels: [],
filterCategories: []
};
}
@@ -121,7 +123,13 @@ class AddIndexerModalContent extends Component {
.map((language) => ({ key: language, value: language }));
const filteredIndexers = indexers.filter((indexer) => {
const { filter, filterProtocols, filterLanguages, filterPrivacyLevels } = this.state;
const {
filter,
filterProtocols,
filterLanguages,
filterPrivacyLevels,
filterCategories
} = this.state;
if (!indexer.name.toLowerCase().includes(filter.toLocaleLowerCase()) && !indexer.description.toLowerCase().includes(filter.toLocaleLowerCase())) {
return false;
@@ -139,6 +147,18 @@ class AddIndexerModalContent extends Component {
return false;
}
if (filterCategories.length) {
const { categories = [] } = indexer.capabilities || {};
const flat = ({ id, subCategories = [] }) => [id, ...subCategories.flatMap(flat)];
const flatCategories = categories
.filter((item) => item.id < 100000)
.flatMap(flat);
if (!filterCategories.every((item) => flatCategories.includes(item))) {
return false;
}
}
return true;
});
@@ -165,7 +185,7 @@ class AddIndexerModalContent extends Component {
<div className={styles.filterRow}>
<div className={styles.filterContainer}>
<label className={styles.filterLabel}>Protocol</label>
<label className={styles.filterLabel}>{translate('Protocol')}</label>
<EnhancedSelectInput
name="indexerProtocols"
value={this.state.filterProtocols}
@@ -175,7 +195,7 @@ class AddIndexerModalContent extends Component {
</div>
<div className={styles.filterContainer}>
<label className={styles.filterLabel}>Language</label>
<label className={styles.filterLabel}>{translate('Language')}</label>
<EnhancedSelectInput
name="indexerLanguages"
value={this.state.filterLanguages}
@@ -185,7 +205,7 @@ class AddIndexerModalContent extends Component {
</div>
<div className={styles.filterContainer}>
<label className={styles.filterLabel}>Privacy</label>
<label className={styles.filterLabel}>{translate('Privacy')}</label>
<EnhancedSelectInput
name="indexerPrivacyLevels"
value={this.state.filterPrivacyLevels}
@@ -193,6 +213,15 @@ class AddIndexerModalContent extends Component {
onChange={({ value }) => this.setState({ filterPrivacyLevels: value })}
/>
</div>
<div className={styles.filterContainer}>
<label className={styles.filterLabel}>{translate('Categories')}</label>
<NewznabCategorySelectInputConnector
name="indexerCategories"
value={this.state.filterCategories}
onChange={({ value }) => this.setState({ filterCategories: value })}
/>
</div>
</div>
<Alert
@@ -212,7 +241,7 @@ class AddIndexerModalContent extends Component {
isFetching ? <LoadingIndicator /> : null
}
{
error ? <div>{errorMessage}</div> : null
error ? <Alert kind={kinds.DANGER}>{errorMessage}</Alert> : null
}
{
isPopulated && !!indexers.length ?
@@ -226,7 +255,7 @@ class AddIndexerModalContent extends Component {
{
filteredIndexers.map((indexer) => (
<SelectIndexerRowConnector
key={indexer.name}
key={`${indexer.implementation}-${indexer.name}`}
implementation={indexer.implementation}
{...indexer}
onIndexerSelect={onIndexerSelect}
@@ -237,6 +266,15 @@ class AddIndexerModalContent extends Component {
</Table> :
null
}
{
isPopulated && !!indexers.length && !filteredIndexers.length ?
<Alert
kind={kinds.WARNING}
>
{translate('NoIndexersFound')}
</Alert> :
null
}
</Scroller>
</ModalBody>

View File

@@ -26,6 +26,8 @@ function EditIndexerModalContent(props) {
isTesting,
saveError,
item,
hasUsenetDownloadClients,
hasTorrentDownloadClients,
onInputChange,
onFieldChange,
onModalClose,
@@ -48,10 +50,13 @@ function EditIndexerModalContent(props) {
appProfileId,
tags,
fields,
priority
priority,
protocol,
downloadClientId
} = item;
const indexerDisplayName = implementationName === definitionName ? implementationName : `${implementationName} (${definitionName})`;
const showDownloadClientInput = downloadClientId.value > 0 || protocol.value === 'usenet' && hasUsenetDownloadClients || protocol.value === 'torrent' && hasTorrentDownloadClients;
return (
<ModalContent onModalClose={onModalClose}>
@@ -156,6 +161,25 @@ function EditIndexerModalContent(props) {
/>
</FormGroup>
{showDownloadClientInput ?
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('DownloadClient')}</FormLabel>
<FormInputGroup
type={inputTypes.DOWNLOAD_CLIENT_SELECT}
name="downloadClientId"
helpText={translate('IndexerDownloadClientHelpText')}
{...downloadClientId}
includeAny={true}
protocol={protocol.value}
onChange={onInputChange}
/>
</FormGroup> : null
}
<FormGroup>
<FormLabel>{translate('Tags')}</FormLabel>
@@ -222,6 +246,8 @@ EditIndexerModalContent.propTypes = {
isTesting: PropTypes.bool.isRequired,
saveError: PropTypes.object,
item: PropTypes.object.isRequired,
hasUsenetDownloadClients: PropTypes.bool.isRequired,
hasTorrentDownloadClients: PropTypes.bool.isRequired,
onInputChange: PropTypes.func.isRequired,
onFieldChange: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired,

View File

@@ -3,17 +3,23 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { saveIndexer, setIndexerFieldValue, setIndexerValue, testIndexer } from 'Store/Actions/indexerActions';
import { toggleAdvancedSettings } from 'Store/Actions/settingsActions';
import { fetchDownloadClients, toggleAdvancedSettings } from 'Store/Actions/settingsActions';
import createIndexerSchemaSelector from 'Store/Selectors/createIndexerSchemaSelector';
import EditIndexerModalContent from './EditIndexerModalContent';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.advancedSettings,
(state) => state.settings.downloadClients,
createIndexerSchemaSelector(),
(advancedSettings, indexer) => {
(advancedSettings, downloadClients, indexer) => {
const usenetDownloadClients = downloadClients.items.filter((downloadClient) => downloadClient.protocol === 'usenet');
const torrentDownloadClients = downloadClients.items.filter((downloadClient) => downloadClient.protocol === 'torrent');
return {
advancedSettings,
hasUsenetDownloadClients: usenetDownloadClients.length > 0,
hasTorrentDownloadClients: torrentDownloadClients.length > 0,
...indexer
};
}
@@ -25,7 +31,8 @@ const mapDispatchToProps = {
setIndexerFieldValue,
saveIndexer,
testIndexer,
toggleAdvancedSettings
toggleAdvancedSettings,
dispatchFetchDownloadClients: fetchDownloadClients
};
class EditIndexerModalContentConnector extends Component {
@@ -33,6 +40,10 @@ class EditIndexerModalContentConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.dispatchFetchDownloadClients();
}
componentDidUpdate(prevProps, prevState) {
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
this.props.onModalClose();
@@ -90,7 +101,8 @@ EditIndexerModalContentConnector.propTypes = {
toggleAdvancedSettings: PropTypes.func.isRequired,
saveIndexer: PropTypes.func.isRequired,
testIndexer: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
onModalClose: PropTypes.func.isRequired,
dispatchFetchDownloadClients: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(EditIndexerModalContentConnector);

View File

@@ -148,17 +148,17 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
);
const jumpBarItems = useMemo(() => {
// Reset if not sorting by sortTitle
if (sortKey !== 'sortTitle') {
// Reset if not sorting by sortName
if (sortKey !== 'sortName') {
return {
order: [],
};
}
const characters = items.reduce((acc, item) => {
let char = item.sortTitle.charAt(0);
let char = item.sortName.charAt(0);
if (!isNaN(char)) {
if (!isNaN(Number(char))) {
char = '#';
}
@@ -225,7 +225,7 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
label={
isSelectMode
? translate('StopSelecting')
: translate('SelectIndexer')
: translate('SelectIndexers')
}
iconName={isSelectMode ? icons.SERIES_ENDED : icons.CHECK}
isSelectMode={isSelectMode}

View File

@@ -9,6 +9,7 @@ import ModalHeader from 'Components/Modal/ModalHeader';
import { kinds } from 'Helpers/Props';
import { bulkDeleteIndexers } from 'Store/Actions/indexerIndexActions';
import createAllIndexersSelector from 'Store/Selectors/createAllIndexersSelector';
import translate from 'Utilities/String/translate';
import styles from './DeleteIndexerModalContent.css';
interface DeleteIndexerModalContentProps {
@@ -19,16 +20,16 @@ interface DeleteIndexerModalContentProps {
function DeleteIndexerModalContent(props: DeleteIndexerModalContentProps) {
const { indexerIds, onModalClose } = props;
const allIndexer = useSelector(createAllIndexersSelector());
const allIndexers = useSelector(createAllIndexersSelector());
const dispatch = useDispatch();
const indexers = useMemo(() => {
const selectedIndexers = useMemo(() => {
const indexers = indexerIds.map((id) => {
return allIndexer.find((s) => s.id === id);
return allIndexers.find((s) => s.id === id);
});
return orderBy(indexers, ['sortTitle']);
}, [indexerIds, allIndexer]);
return orderBy(indexers, ['sortName']);
}, [indexerIds, allIndexers]);
const onDeleteIndexerConfirmed = useCallback(() => {
dispatch(
@@ -42,17 +43,19 @@ function DeleteIndexerModalContent(props: DeleteIndexerModalContentProps) {
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>Delete Selected Indexer</ModalHeader>
<ModalHeader>{translate('DeleteSelectedIndexers')}</ModalHeader>
<ModalBody>
<div className={styles.message}>
{`Are you sure you want to delete ${indexers.length} selected indexers?`}
{translate('DeleteSelectedIndexersMessageText', [
selectedIndexers.length,
])}
</div>
<ul>
{indexers.map((s) => {
{selectedIndexers.map((s) => {
return (
<li key={s.name}>
<li key={s.id}>
<span>{s.name}</span>
</li>
);
@@ -61,10 +64,10 @@ function DeleteIndexerModalContent(props: DeleteIndexerModalContentProps) {
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>Cancel</Button>
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
<Button kind={kinds.DANGER} onPress={onDeleteIndexerConfirmed}>
Delete
{translate('Delete')}
</Button>
</ModalFooter>
</ModalContent>

View File

@@ -14,6 +14,7 @@ import styles from './EditIndexerModalContent.css';
interface SavePayload {
enable?: boolean;
appProfileId?: number;
priority?: number;
}
interface EditIndexerModalContentProps {
@@ -35,6 +36,7 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
const [enable, setEnable] = useState(NO_CHANGE);
const [appProfileId, setAppProfileId] = useState<string | number>(NO_CHANGE);
const [priority, setPriority] = useState<null | string | number>(null);
const save = useCallback(() => {
let hasChanges = false;
@@ -50,12 +52,17 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
payload.appProfileId = appProfileId as number;
}
if (priority !== null) {
hasChanges = true;
payload.priority = priority as number;
}
if (hasChanges) {
onSavePress(payload);
}
onModalClose();
}, [enable, appProfileId, onSavePress, onModalClose]);
}, [enable, appProfileId, priority, onSavePress, onModalClose]);
const onInputChange = useCallback(
({ name, value }) => {
@@ -66,8 +73,11 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
case 'appProfileId':
setAppProfileId(value);
break;
case 'priority':
setPriority(value);
break;
default:
console.warn('EditIndexerModalContent Unknown Input');
console.warn(`EditIndexersModalContent Unknown Input: '${name}'`);
}
},
[setEnable]
@@ -81,7 +91,7 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>{translate('Edit Selected Indexer')}</ModalHeader>
<ModalHeader>{translate('EditSelectedIndexers')}</ModalHeader>
<ModalBody>
<FormGroup>
@@ -108,18 +118,31 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Priority')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="priority"
value={priority}
min={1}
max={50}
onChange={onInputChange}
/>
</FormGroup>
</ModalBody>
<ModalFooter className={styles.modalFooter}>
<div className={styles.selected}>
{translate('{0} indexers selected', selectedCount.toString())}
{translate('CountIndexersSelected', [selectedCount])}
</div>
<div>
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
<Button onPress={onSavePressWrapper}>
{translate('Apply Changes')}
{translate('ApplyChanges')}
</Button>
</div>
</ModalFooter>

View File

@@ -1,5 +1,5 @@
import React, { useCallback } from 'react';
import { SelectActionType, useSelect } from 'App/SelectContext';
import { useSelect } from 'App/SelectContext';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import { icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
@@ -25,9 +25,7 @@ function IndexerIndexSelectAllButton(props: IndexerIndexSelectAllButtonProps) {
const onPress = useCallback(() => {
selectDispatch({
type: allSelected
? SelectActionType.UnselectAll
: SelectActionType.SelectAll,
type: allSelected ? 'unselectAll' : 'selectAll',
});
}, [allSelected, selectDispatch]);

View File

@@ -1,5 +1,5 @@
import React, { useCallback } from 'react';
import { SelectActionType, useSelect } from 'App/SelectContext';
import { useSelect } from 'App/SelectContext';
import PageToolbarOverflowMenuItem from 'Components/Page/Toolbar/PageToolbarOverflowMenuItem';
import { icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
@@ -26,9 +26,7 @@ function IndexerIndexSelectAllMenuItem(
const onPressWrapper = useCallback(() => {
selectDispatch({
type: allSelected
? SelectActionType.UnselectAll
: SelectActionType.SelectAll,
type: allSelected ? 'unselectAll' : 'selectAll',
});
}, [allSelected, selectDispatch]);

View File

@@ -1,9 +1,10 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import { SelectActionType, useSelect } from 'App/SelectContext';
import { useSelect } from 'App/SelectContext';
import SpinnerButton from 'Components/Link/SpinnerButton';
import PageContentFooter from 'Components/Page/PageContentFooter';
import usePrevious from 'Helpers/Hooks/usePrevious';
import { kinds } from 'Helpers/Props';
import { saveIndexerEditor } from 'Store/Actions/indexerIndexActions';
import translate from 'Utilities/String/translate';
@@ -13,7 +14,7 @@ import EditIndexerModal from './Edit/EditIndexerModal';
import TagsModal from './Tags/TagsModal';
import styles from './IndexerIndexSelectFooter.css';
const seriesEditorSelector = createSelector(
const indexersEditorSelector = createSelector(
(state) => state.indexers,
(indexers) => {
const { isSaving, isDeleting, deleteError } = indexers;
@@ -27,8 +28,9 @@ const seriesEditorSelector = createSelector(
);
function IndexerIndexSelectFooter() {
const { isSaving, isDeleting, deleteError } =
useSelector(seriesEditorSelector);
const { isSaving, isDeleting, deleteError } = useSelector(
indexersEditorSelector
);
const dispatch = useDispatch();
@@ -37,6 +39,7 @@ function IndexerIndexSelectFooter() {
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [isSavingIndexer, setIsSavingIndexer] = useState(false);
const [isSavingTags, setIsSavingTags] = useState(false);
const previousIsDeleting = usePrevious(isDeleting);
const [selectState, selectDispatch] = useSelect();
const { selectedState } = selectState;
@@ -110,10 +113,10 @@ function IndexerIndexSelectFooter() {
}, [isSaving]);
useEffect(() => {
if (!isDeleting && !deleteError) {
selectDispatch({ type: SelectActionType.UnselectAll });
if (previousIsDeleting && !isDeleting && !deleteError) {
selectDispatch({ type: 'unselectAll' });
}
}, [isDeleting, deleteError, selectDispatch]);
}, [previousIsDeleting, isDeleting, deleteError, selectDispatch]);
const anySelected = selectedCount > 0;
@@ -134,7 +137,7 @@ function IndexerIndexSelectFooter() {
isDisabled={!anySelected}
onPress={onTagsPress}
>
{translate('Set Tags')}
{translate('SetTags')}
</SpinnerButton>
</div>
@@ -151,7 +154,7 @@ function IndexerIndexSelectFooter() {
</div>
<div className={styles.selected}>
{translate('{0} indexers selected', selectedCount.toString())}
{translate('CountIndexersSelected', [selectedCount])}
</div>
<EditIndexerModal

View File

@@ -1,6 +1,6 @@
import { IconDefinition } from '@fortawesome/fontawesome-common-types';
import React, { useCallback } from 'react';
import { SelectActionType, useSelect } from 'App/SelectContext';
import { useSelect } from 'App/SelectContext';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
interface IndexerIndexSelectModeButtonProps {
@@ -20,7 +20,7 @@ function IndexerIndexSelectModeButton(
const onPressWrapper = useCallback(() => {
if (isSelectMode) {
selectDispatch({
type: SelectActionType.Reset,
type: 'reset',
});
}

View File

@@ -1,6 +1,6 @@
import { IconDefinition } from '@fortawesome/fontawesome-common-types';
import React, { useCallback } from 'react';
import { SelectActionType, useSelect } from 'App/SelectContext';
import { useSelect } from 'App/SelectContext';
import PageToolbarOverflowMenuItem from 'Components/Page/Toolbar/PageToolbarOverflowMenuItem';
interface IndexerIndexSelectModeMenuItemProps {
@@ -19,7 +19,7 @@ function IndexerIndexSelectModeMenuItem(
const onPressWrapper = useCallback(() => {
if (isSelectMode) {
selectDispatch({
type: SelectActionType.Reset,
type: 'reset',
});
}

View File

@@ -59,14 +59,14 @@ function TagsModalContent(props: TagsModalContentProps) {
}, [tags, applyTags, onApplyTagsPress]);
const applyTagsOptions = [
{ key: 'add', value: 'Add' },
{ key: 'remove', value: 'Remove' },
{ key: 'replace', value: 'Replace' },
{ key: 'add', value: translate('Add') },
{ key: 'remove', value: translate('Remove') },
{ key: 'replace', value: translate('Replace') },
];
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>Tags</ModalHeader>
<ModalHeader>{translate('Tags')}</ModalHeader>
<ModalBody>
<Form>
@@ -119,8 +119,8 @@ function TagsModalContent(props: TagsModalContentProps) {
key={tag.id}
title={
removeTag
? translate('RemoveTagRemovingTag')
: translate('RemoveTagExistingTag')
? translate('RemovingTag')
: translate('ExistingTag')
}
kind={removeTag ? kinds.INVERSE : kinds.INFO}
size={sizes.LARGE}
@@ -159,10 +159,10 @@ function TagsModalContent(props: TagsModalContentProps) {
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>Cancel</Button>
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
<Button kind={kinds.PRIMARY} onPress={onApplyPress}>
Apply
{translate('Apply')}
</Button>
</ModalFooter>
</ModalContent>

View File

@@ -1,6 +1,6 @@
import React, { useCallback, useState } from 'react';
import { useSelector } from 'react-redux';
import { SelectActionType, useSelect } from 'App/SelectContext';
import { useSelect } from 'App/SelectContext';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
@@ -90,7 +90,7 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
const onSelectedChange = useCallback(
({ id, value, shiftKey }) => {
selectDispatch({
type: SelectActionType.ToggleSelected,
type: 'toggleSelected',
id,
isSelected: value,
shiftKey,

View File

@@ -92,11 +92,9 @@ function IndexerIndexTable(props: IndexerIndexTableProps) {
const columns = useSelector(columnsSelector);
const { showBanners } = useSelector(selectTableOptions);
const listRef: React.MutableRefObject<List> = useRef();
const listRef = useRef<List>(null);
const [measureRef, bounds] = useMeasure();
const [size, setSize] = useState({ width: 0, height: 0 });
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
const rowHeight = useMemo(() => {
return showBanners ? 70 : 38;
@@ -107,8 +105,8 @@ function IndexerIndexTable(props: IndexerIndexTableProps) {
if (isSmallScreen) {
setSize({
width: windowWidth,
height: windowHeight,
width: window.innerWidth,
height: window.innerHeight,
});
return;
@@ -121,14 +119,14 @@ function IndexerIndexTable(props: IndexerIndexTableProps) {
setSize({
width: width - padding * 2,
height: windowHeight,
height: window.innerHeight,
});
}
}, [isSmallScreen, windowWidth, windowHeight, scrollerRef, bounds]);
}, [isSmallScreen, scrollerRef, bounds]);
useEffect(() => {
const currentScrollListener = isSmallScreen ? window : scrollerRef.current;
const currentScrollerRef = scrollerRef.current;
const currentScrollerRef = scrollerRef.current as HTMLElement;
const currentScrollListener = isSmallScreen ? window : currentScrollerRef;
const handleScroll = throttle(() => {
const { offsetTop = 0 } = currentScrollerRef;
@@ -137,7 +135,7 @@ function IndexerIndexTable(props: IndexerIndexTableProps) {
? getWindowScrollTopPosition()
: currentScrollerRef.scrollTop) - offsetTop;
listRef.current.scrollTo(scrollTop);
listRef.current?.scrollTo(scrollTop);
}, 10);
currentScrollListener.addEventListener('scroll', handleScroll);
@@ -166,8 +164,8 @@ function IndexerIndexTable(props: IndexerIndexTableProps) {
scrollTop += offset;
}
listRef.current.scrollTo(scrollTop);
scrollerRef.current.scrollTo(0, scrollTop);
listRef.current?.scrollTo(scrollTop);
scrollerRef.current?.scrollTo(0, scrollTop);
}
}
}, [jumpToCharacter, rowHeight, items, scrollerRef, listRef]);

View File

@@ -1,7 +1,7 @@
import classNames from 'classnames';
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { SelectActionType, useSelect } from 'App/SelectContext';
import { useSelect } from 'App/SelectContext';
import IconButton from 'Components/Link/IconButton';
import Column from 'Components/Table/Column';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
@@ -47,7 +47,7 @@ function IndexerIndexTableHeader(props: IndexerIndexTableHeaderProps) {
const onSelectAllChange = useCallback(
({ value }) => {
selectDispatch({
type: value ? SelectActionType.SelectAll : SelectActionType.UnselectAll,
type: value ? 'selectAll' : 'unselectAll',
});
},
[selectDispatch]

View File

@@ -43,7 +43,7 @@ function IndexerStatusCell(props: IndexerStatusCellProps) {
className={styles.statusIcon}
kind={enabled ? enableKind : kinds.DEFAULT}
name={enabled ? enableIcon : icons.BLOCKLIST}
title={enabled ? enableTitle : translate('EnabledIndexerIsDisabled')}
title={enabled ? enableTitle : translate('Disabled')}
/>
}
{status ? (

View File

@@ -13,6 +13,10 @@ 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 TableRowCell from 'Components/Table/Cells/TableRowCell';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import TableRow from 'Components/Table/TableRow';
import TagListConnector from 'Components/TagListConnector';
import { kinds } from 'Helpers/Props';
import DeleteIndexerModal from 'Indexer/Delete/DeleteIndexerModal';
@@ -149,6 +153,7 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
</DescriptionList>
</div>
</FieldSet>
<FieldSet legend={translate('SearchCapabilities')}>
<div>
<DescriptionList>
@@ -237,6 +242,54 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
</DescriptionList>
</div>
</FieldSet>
{capabilities.categories !== null &&
capabilities.categories.length > 0 ? (
<FieldSet legend={translate('IndexerCategories')}>
<Table
columns={[
{
name: 'id',
label: translate('Id'),
isVisible: true,
},
{
name: 'name',
label: translate('Name'),
isVisible: true,
},
]}
>
{capabilities.categories
.sort((a, b) => a.id - b.id)
.map((category) => {
return (
<TableBody key={category.id}>
<TableRow key={category.id}>
<TableRowCell>{category.id}</TableRowCell>
<TableRowCell>{category.name}</TableRowCell>
</TableRow>
{category.subCategories !== null &&
category.subCategories.length > 0
? category.subCategories
.sort((a, b) => a.id - b.id)
.map((subCategory) => {
return (
<TableRow key={subCategory.id}>
<TableRowCell>{subCategory.id}</TableRowCell>
<TableRowCell>
{subCategory.name}
</TableRowCell>
</TableRow>
);
})
: null}
</TableBody>
);
})}
</Table>
</FieldSet>
) : null}
</ModalBody>
<ModalFooter>
<Button

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React from 'react';
import Alert from 'Components/Alert';
import BarChart from 'Components/Chart/BarChart';
import DoughnutChart from 'Components/Chart/DoughnutChart';
import StackedBarChart from 'Components/Chart/StackedBarChart';
@@ -178,9 +179,9 @@ function Stats(props) {
{
!isFetching && !!error &&
<div className={styles.errorMessage}>
<Alert kind={kinds.DANGER}>
{getErrorMessage(error, 'Failed to load indexer stats from API')}
</div>
</Alert>
}
{
@@ -252,7 +253,6 @@ Stats.propTypes = {
isPopulated: PropTypes.bool.isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
selectedFilterKey: PropTypes.string.isRequired,
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
onFilterSelect: PropTypes.func.isRequired,
error: PropTypes.object,
data: PropTypes.object

View File

@@ -27,7 +27,9 @@ class SearchFooter extends Component {
defaultIndexerIds,
defaultCategories,
defaultSearchQuery,
defaultSearchType
defaultSearchType,
defaultSearchLimit,
defaultSearchOffset
} = props;
this.state = {
@@ -38,8 +40,8 @@ class SearchFooter extends Component {
searchQuery: defaultSearchQuery || '',
searchIndexerIds: defaultIndexerIds,
searchCategories: defaultCategories,
searchLimit: 100,
searchOffset: 0,
searchLimit: defaultSearchLimit,
searchOffset: defaultSearchOffset,
newSearch: true
};
}
@@ -55,7 +57,9 @@ class SearchFooter extends Component {
this.onSearchPress();
}
this.props.bindShortcut('enter', this.onSearchPress, { isGlobal: true });
setTimeout(() => {
this.props.bindShortcut('enter', this.onSearchPress, { isGlobal: true });
});
}
componentDidUpdate(prevProps) {
@@ -120,7 +124,6 @@ class SearchFooter extends Component {
};
onSearchPress = () => {
const {
searchLimit,
searchOffset,
@@ -188,10 +191,10 @@ class SearchFooter extends Component {
icon = icons.SEARCH;
}
let footerLabel = `Search ${searchIndexerIds.length === 0 ? 'all' : searchIndexerIds.length} Indexers`;
let footerLabel = searchIndexerIds.length === 0 ? translate('SearchAllIndexers') : translate('SearchCountIndexers', [searchIndexerIds.length]);
if (isPopulated) {
footerLabel = selectedCount === 0 ? `Found ${itemCount} releases` : `Selected ${selectedCount} of ${itemCount} releases`;
footerLabel = selectedCount === 0 ? translate('FoundCountReleases', [itemCount]) : translate('SelectedCountOfCountReleases', [selectedCount, itemCount]);
}
return (
@@ -302,6 +305,8 @@ SearchFooter.propTypes = {
defaultCategories: PropTypes.arrayOf(PropTypes.number).isRequired,
defaultSearchQuery: PropTypes.string.isRequired,
defaultSearchType: PropTypes.string.isRequired,
defaultSearchLimit: PropTypes.number.isRequired,
defaultSearchOffset: PropTypes.number.isRequired,
selectedCount: PropTypes.number.isRequired,
itemCount: PropTypes.number.isRequired,
isFetching: PropTypes.bool.isRequired,

View File

@@ -3,24 +3,58 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { setSearchDefault } from 'Store/Actions/releaseActions';
import parseUrl from 'Utilities/String/parseUrl';
import SearchFooter from './SearchFooter';
function createMapStateToProps() {
return createSelector(
(state) => state.releases,
(releases) => {
(state) => state.router.location,
(releases, location) => {
const {
searchQuery: defaultSearchQuery,
searchIndexerIds: defaultIndexerIds,
searchCategories: defaultCategories,
searchType: defaultSearchType
searchType: defaultSearchType,
searchLimit: defaultSearchLimit,
searchOffset: defaultSearchOffset
} = releases.defaults;
const { params } = parseUrl(location.search);
const defaultSearchQueryParams = {};
if (params.query && !defaultSearchQuery) {
defaultSearchQueryParams.searchQuery = params.query;
}
if (params.indexerIds && !defaultIndexerIds.length) {
defaultSearchQueryParams.searchIndexerIds = params.indexerIds.split(',').filter(Boolean).map((id) => Number(id));
}
if (params.categories && !defaultCategories.length) {
defaultSearchQueryParams.searchCategories = params.categories.split(',').filter(Boolean).map((id) => Number(id));
}
if (params.type && defaultSearchType === 'search') {
defaultSearchQueryParams.searchType = params.type;
}
if (params.limit && defaultSearchLimit === 100 && !isNaN(params.limit)) {
defaultSearchQueryParams.searchLimit = Number(params.limit);
}
if (params.offset && !defaultSearchOffset && !isNaN(params.offset)) {
defaultSearchQueryParams.searchOffset = Number(params.offset);
}
return {
defaultSearchQuery,
defaultIndexerIds,
defaultCategories,
defaultSearchType
defaultSearchQueryParams,
defaultSearchQuery: defaultSearchQueryParams.searchQuery ?? defaultSearchQuery,
defaultIndexerIds: defaultSearchQueryParams.searchIndexerIds ?? defaultIndexerIds,
defaultCategories: defaultSearchQueryParams.searchCategories ?? defaultCategories,
defaultSearchType: defaultSearchQueryParams.searchType ?? defaultSearchType,
defaultSearchLimit: defaultSearchQueryParams.searchLimit ?? defaultSearchLimit,
defaultSearchOffset: defaultSearchQueryParams.searchOffset ?? defaultSearchOffset
};
}
);
@@ -32,6 +66,16 @@ const mapDispatchToProps = {
class SearchFooterConnector extends Component {
//
// Lifecycle
componentDidMount() {
// Set defaults from query parameters
Object.entries(this.props.defaultSearchQueryParams).forEach(([name, value]) => {
this.onInputChange({ name, value });
});
}
//
// Listeners
@@ -43,9 +87,14 @@ class SearchFooterConnector extends Component {
// Render
render() {
const {
defaultSearchQueryParams,
...otherProps
} = this.props;
return (
<SearchFooter
{...this.props}
{...otherProps}
onInputChange={this.onInputChange}
/>
);
@@ -53,6 +102,7 @@ class SearchFooterConnector extends Component {
}
SearchFooterConnector.propTypes = {
defaultSearchQueryParams: PropTypes.object.isRequired,
setSearchDefault: PropTypes.func.isRequired
};

View File

@@ -1,6 +1,7 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
@@ -10,7 +11,9 @@ import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import { align, icons, sortDirections } from 'Helpers/Props';
import { align, icons, kinds, sortDirections } from 'Helpers/Props';
import AddIndexerModal from 'Indexer/Add/AddIndexerModal';
import EditIndexerModalConnector from 'Indexer/Edit/EditIndexerModalConnector';
import NoIndexer from 'Indexer/NoIndexer';
import * as keyCodes from 'Utilities/Constants/keyCodes';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
@@ -53,7 +56,9 @@ class SearchIndex extends Component {
lastToggled: null,
allSelected: false,
allUnselected: false,
selectedState: {}
selectedState: {},
isAddIndexerModalOpen: false,
isEditIndexerModalOpen: false
};
}
@@ -141,7 +146,7 @@ class SearchIndex extends Component {
} = this.props;
// Reset if not sorting by sortTitle
if (sortKey !== 'title') {
if (sortKey !== 'sortTitle') {
this.setState({ jumpBarItems: { order: [] } });
return;
}
@@ -149,7 +154,7 @@ class SearchIndex extends Component {
const characters = _.reduce(items, (acc, item) => {
let char = item.sortTitle.charAt(0);
if (!isNaN(char)) {
if (!isNaN(Number(char))) {
char = '#';
}
@@ -180,6 +185,22 @@ class SearchIndex extends Component {
//
// Listeners
onAddIndexerPress = () => {
this.setState({ isAddIndexerModalOpen: true });
};
onAddIndexerModalClose = () => {
this.setState({ isAddIndexerModalOpen: false });
};
onAddIndexerSelectIndexer = () => {
this.setState({ isEditIndexerModalOpen: true });
};
onEditIndexerModalClose = () => {
this.setState({ isEditIndexerModalOpen: false });
};
onJumpBarItemPress = (jumpToCharacter) => {
this.setState({ jumpToCharacter });
};
@@ -251,7 +272,9 @@ class SearchIndex extends Component {
jumpToCharacter,
selectedState,
allSelected,
allUnselected
allUnselected,
isAddIndexerModalOpen,
isEditIndexerModalOpen
} = this.state;
const selectedIndexerIds = this.getSelectedIds();
@@ -309,9 +332,9 @@ class SearchIndex extends Component {
{
!isFetching && !!error &&
<div className={styles.errorMessage}>
<Alert kind={kinds.DANGER}>
{getErrorMessage(error, 'Failed to load search results from API')}
</div>
</Alert>
}
{
@@ -347,6 +370,17 @@ class SearchIndex extends Component {
!error && !isFetching && hasIndexers && !items.length &&
<NoSearchResults totalItems={totalItems} />
}
<AddIndexerModal
isOpen={isAddIndexerModalOpen}
onModalClose={this.onAddIndexerModalClose}
onSelectIndexer={this.onAddIndexerSelectIndexer}
/>
<EditIndexerModalConnector
isOpen={isEditIndexerModalOpen}
onModalClose={this.onEditIndexerModalClose}
/>
</PageContentBody>
{

View File

@@ -1,8 +1,8 @@
import PropTypes from 'prop-types';
import React from 'react';
import Label from 'Components/Label';
import Tooltip from 'Components/Tooltip/Tooltip';
import { kinds, tooltipPositions } from 'Helpers/Props';
import Tooltip from '../../Components/Tooltip/Tooltip';
function CategoryLabel({ categories }) {
const sortedCategories = categories.filter((cat) => cat.name !== undefined).sort((c) => c.id);

View File

@@ -17,7 +17,7 @@ function AdvancedSettingsButton(props) {
return (
<Link
className={styles.button}
title={advancedSettings ? translate('ShownClickToHide') : translate('HiddenClickToShow')}
title={advancedSettings ? translate('AdvancedSettingsShownClickToHide') : translate('AdvancedSettingsHiddenClickToShow')}
onPress={onAdvancedSettingsPress}
>
<Icon

View File

@@ -14,6 +14,7 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds } from 'Helpers/Props';
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
import translate from 'Utilities/String/translate';
import styles from './EditApplicationModalContent.css';
@@ -38,6 +39,7 @@ function EditApplicationModalContent(props) {
onSavePress,
onTestPress,
onDeleteApplicationPress,
onAdvancedSettingsPress,
...otherProps
} = props;
@@ -149,6 +151,12 @@ function EditApplicationModalContent(props) {
</Button>
}
<AdvancedSettingsButton
advancedSettings={advancedSettings}
onAdvancedSettingsPress={onAdvancedSettingsPress}
showLabel={false}
/>
<SpinnerErrorButton
isSpinning={isTesting}
error={saveError}
@@ -188,7 +196,8 @@ EditApplicationModalContent.propTypes = {
onModalClose: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
onTestPress: PropTypes.func.isRequired,
onDeleteApplicationPress: PropTypes.func
onDeleteApplicationPress: PropTypes.func,
onAdvancedSettingsPress: PropTypes.func.isRequired
};
export default EditApplicationModalContent;

View File

@@ -2,7 +2,13 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { saveApplication, setApplicationFieldValue, setApplicationValue, testApplication } from 'Store/Actions/settingsActions';
import {
saveApplication,
setApplicationFieldValue,
setApplicationValue,
testApplication,
toggleAdvancedSettings
} from 'Store/Actions/settingsActions';
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
import EditApplicationModalContent from './EditApplicationModalContent';
@@ -23,7 +29,8 @@ const mapDispatchToProps = {
setApplicationValue,
setApplicationFieldValue,
saveApplication,
testApplication
testApplication,
toggleAdvancedSettings
};
class EditApplicationModalContentConnector extends Component {
@@ -56,6 +63,10 @@ class EditApplicationModalContentConnector extends Component {
this.props.testApplication({ id: this.props.id });
};
onAdvancedSettingsPress = () => {
this.props.toggleAdvancedSettings();
};
//
// Render
@@ -67,6 +78,7 @@ class EditApplicationModalContentConnector extends Component {
onTestPress={this.onTestPress}
onInputChange={this.onInputChange}
onFieldChange={this.onFieldChange}
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
/>
);
}
@@ -82,7 +94,8 @@ EditApplicationModalContentConnector.propTypes = {
setApplicationFieldValue: PropTypes.func,
saveApplication: PropTypes.func,
testApplication: PropTypes.func,
onModalClose: PropTypes.func.isRequired
onModalClose: PropTypes.func.isRequired,
toggleAdvancedSettings: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(EditApplicationModalContentConnector);

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import FieldSet from 'Components/FieldSet';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
@@ -8,7 +9,7 @@ import FormLabel from 'Components/Form/FormLabel';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import { inputTypes } from 'Helpers/Props';
import { inputTypes, kinds } from 'Helpers/Props';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import translate from 'Utilities/String/translate';
@@ -49,9 +50,9 @@ class DevelopmentSettings extends Component {
{
!isFetching && error &&
<div>
<Alert kind={kinds.DANGER}>
{translate('UnableToLoadDevelopmentSettings')}
</div>
</Alert>
}
{

View File

@@ -1,6 +1,7 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import Form from 'Components/Form/Form';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ConfirmModal from 'Components/Modal/ConfirmModal';
@@ -123,9 +124,9 @@ class GeneralSettings extends Component {
{
!isFetching && error &&
<div>
<Alert kind={kinds.DANGER}>
{translate('UnableToLoadGeneralSettings')}
</div>
</Alert>
}
{

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import FieldSet from 'Components/FieldSet';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
@@ -8,7 +9,7 @@ import FormLabel from 'Components/Form/FormLabel';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import { inputTypes } from 'Helpers/Props';
import { inputTypes, kinds } from 'Helpers/Props';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import themes from 'Styles/Themes';
import titleCase from 'Utilities/String/titleCase';
@@ -80,9 +81,9 @@ class UISettings extends Component {
{
!isFetching && error &&
<div>
<Alert kind={kinds.DANGER}>
{translate('UnableToLoadUISettings')}
</div>
</Alert>
}
{

View File

@@ -32,9 +32,9 @@ function createSaveProviderHandler(section, url, options = {}) {
const params = { ...queryParams };
// If the user is re-saving the same provider without changes
// force it to be saved. Only applies to editing existing providers.
// force it to be saved.
if (id && _.isEqual(saveData, lastSaveData)) {
if (_.isEqual(saveData, lastSaveData)) {
params.forceSave = true;
}

View File

@@ -58,6 +58,12 @@ export const defaultState = {
isSortable: false,
isVisible: false
},
{
name: 'queryType',
label: translate('QueryType'),
isSortable: false,
isVisible: false
},
{
name: 'categories',
label: translate('Categories'),

View File

@@ -29,6 +29,8 @@ export const defaultState = {
isFetching: false,
isPopulated: false,
error: null,
isDeleting: false,
deleteError: null,
selectedSchema: {},
isSaving: false,
saveError: null,
@@ -41,7 +43,7 @@ export const defaultState = {
isFetching: false,
isPopulated: false,
error: null,
sortKey: 'name',
sortKey: 'sortName',
sortDirection: sortDirections.ASCENDING,
items: []
}

View File

@@ -36,7 +36,7 @@ export const defaultState = {
columns: [
{
name: 'status',
columnLabel: translate('ReleaseStatus'),
columnLabel: translate('IndexerStatus'),
isSortable: true,
isVisible: true,
isModifiable: false

View File

@@ -31,16 +31,18 @@ export const defaultState = {
error: null,
grabError: null,
items: [],
sortKey: 'title',
sortKey: 'age',
sortDirection: sortDirections.ASCENDING,
secondarySortKey: 'title',
secondarySortKey: 'sortTitle',
secondarySortDirection: sortDirections.ASCENDING,
defaults: {
searchType: 'search',
searchQuery: '',
searchIndexerIds: [],
searchCategories: []
searchCategories: [],
searchLimit: 100,
searchOffset: 0
},
columns: [

View File

@@ -9,12 +9,12 @@ function createUnoptimizedSelector(uiSection) {
const items = indexers.items.map((s) => {
const {
id,
name
sortName
} = s;
return {
id,
sortTitle: name
sortName
};
});
@@ -38,7 +38,7 @@ const createMovieEqualSelector = createSelectorCreator(
function createIndexerClientSideCollectionItemsSelector(uiSection) {
return createMovieEqualSelector(
createUnoptimizedSelector(uiSection),
(movies) => movies
(indexers) => indexers
);
}

View File

@@ -9,13 +9,13 @@ function createUnoptimizedSelector(uiSection) {
const items = releases.items.map((s) => {
const {
guid,
title,
sortTitle,
indexerId
} = s;
return {
guid,
sortTitle: title,
sortTitle,
indexerId
};
});
@@ -40,7 +40,7 @@ const createMovieEqualSelector = createSelectorCreator(
function createReleaseClientSideCollectionItemsSelector(uiSection) {
return createMovieEqualSelector(
createUnoptimizedSelector(uiSection),
(movies) => movies
(releases) => releases
);
}

View File

@@ -162,7 +162,7 @@ module.exports = {
inputHoverBackgroundColor: 'rgba(255, 255, 255, 0.20)',
inputSelectedBackgroundColor: 'rgba(255, 255, 255, 0.05)',
advancedFormLabelColor: '#ff902b',
disabledCheckInputColor: '#ddd',
disabledCheckInputColor: '#999',
disabledInputColor: '#808080',
//

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
@@ -8,7 +9,7 @@ import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { icons } from 'Helpers/Props';
import { icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import BackupRow from './BackupRow';
import RestoreBackupModalConnector from './RestoreBackupModalConnector';
@@ -107,16 +108,16 @@ class Backups extends Component {
{
!isFetching && !!error &&
<div>
<Alert kind={kinds.DANGER}>
{translate('UnableToLoadBackups')}
</div>
</Alert>
}
{
noBackups &&
<div>
<Alert kind={kinds.INFO}>
{translate('NoBackupsAreAvailable')}
</div>
</Alert>
}
{

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React from 'react';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FilterMenu from 'Components/Menu/FilterMenu';
import PageContent from 'Components/Page/PageContent';
@@ -11,7 +12,7 @@ import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import TablePager from 'Components/Table/TablePager';
import { align, icons } from 'Helpers/Props';
import { align, icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import LogsTableRow from './LogsTableRow';
@@ -82,9 +83,9 @@ function LogsTable(props) {
{
isPopulated && !error && !items.length &&
<div>
<Alert kind={kinds.INFO}>
No events found
</div>
</Alert>
}
{

View File

@@ -96,7 +96,14 @@ class LogsTableConnector extends Component {
};
onClearLogsPress = () => {
this.props.executeCommand({ name: commandNames.CLEAR_LOGS });
this.props.executeCommand({
name: commandNames.CLEAR_LOGS,
commandFinished: this.onCommandFinished
});
};
onCommandFinished = () => {
this.props.gotoLogsFirstPage();
};
//

View File

@@ -11,7 +11,7 @@ import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { icons } from 'Helpers/Props';
import { icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import LogsNavMenu from '../LogsNavMenu';
import LogFilesTableRow from './LogFilesTableRow';
@@ -118,9 +118,9 @@ class LogFiles extends Component {
{
!isFetching && !items.length &&
<div>
<Alert kind={kinds.INFO}>
{translate('NoLogFiles')}
</div>
</Alert>
}
</PageContentBody>
</PageContent>

View File

@@ -50,12 +50,6 @@ class LogFilesConnector extends Component {
this.props.fetchLogFiles();
}
componentDidUpdate(prevProps) {
if (prevProps.deleteFilesExecuting && !this.props.deleteFilesExecuting) {
this.props.fetchLogFiles();
}
}
//
// Listeners
@@ -64,7 +58,14 @@ class LogFilesConnector extends Component {
};
onDeleteFilesPress = () => {
this.props.executeCommand({ name: commandNames.DELETE_LOG_FILES });
this.props.executeCommand({
name: commandNames.DELETE_LOG_FILES,
commandFinished: this.onCommandFinished
});
};
onCommandFinished = () => {
this.props.fetchLogFiles();
};
//

View File

@@ -1,6 +1,7 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component, Fragment } from 'react';
import Alert from 'Components/Alert';
import Icon from 'Components/Icon';
import Label from 'Components/Label';
import SpinnerButton from 'Components/Link/SpinnerButton';
@@ -61,9 +62,9 @@ class Updates extends Component {
{
noUpdates &&
<div>
<Alert kind={kinds.INFO}>
{translate('NoUpdatesAreAvailable')}
</div>
</Alert>
}
{

View File

@@ -1,9 +1,9 @@
export default function getIndexOfFirstCharacter(items, character) {
return items.findIndex((item) => {
const firstCharacter = item.sortTitle.charAt(0);
const firstCharacter = 'sortName' in item ? item.sortName.charAt(0) : item.sortTitle.charAt(0);
if (character === '#') {
return !isNaN(firstCharacter);
return !isNaN(Number(firstCharacter));
}
return firstCharacter === character;

View File

@@ -18,13 +18,14 @@ function getTranslations() {
const translations = getTranslations();
export default function translate(key, args = '') {
export default function translate(key, args = []) {
const translation = translations[key] || key;
if (args) {
const translatedKey = translate(key);
return translatedKey.replace(/\{(\d+)\}/g, (match, index) => {
return translation.replace(/\{(\d+)\}/g, (match, index) => {
return args[index];
});
}
return translations[key] || key;
return translation;
}

View File

@@ -73,7 +73,7 @@
"react-router-dom": "5.2.0",
"react-text-truncate": "0.19.0",
"react-use-measure": "2.1.1",
"react-virtualized": "9.22.3",
"react-virtualized": "9.21.1",
"react-window": "1.8.8",
"redux": "4.2.1",
"redux-actions": "2.6.5",

View File

@@ -30,6 +30,13 @@
<!-- A test project gets the test sdk packages automatically added -->
<TestProject>false</TestProject>
<TestProject Condition="$(MSBuildProjectName.EndsWith('.Test'))">true</TestProject>
<!-- XML documentation comments are needed to enforce rule IDE0005 on build -->
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<!--
CS1591: Missing XML comment for publicly visible type or member 'Type_or_Member'
-->
<NoWarn>$(NoWarn);CS1591</NoWarn>
</PropertyGroup>
<PropertyGroup>
@@ -102,7 +109,7 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
<PackageReference Include="NunitXml.TestLogger" Version="3.0.117" />
<PackageReference Include="NunitXml.TestLogger" Version="3.0.131" />
<PackageReference Include="coverlet.collector" Version="3.0.4-preview.27.ge7cb7c3b40" />
</ItemGroup>

View File

@@ -67,7 +67,7 @@ namespace NzbDrone.Automation.Test
{
try
{
Screenshot image = ((ITakesScreenshot)driver).GetScreenshot();
var image = ((ITakesScreenshot)driver).GetScreenshot();
image.SaveAsFile($"./{name}_test_screenshot.png", ScreenshotImageFormat.Png);
}
catch (Exception ex)

View File

@@ -36,7 +36,7 @@ namespace NzbDrone.Automation.Test.PageModel
{
try
{
IWebElement element = d.FindElement(By.ClassName("followingBalls"));
var element = d.FindElement(By.ClassName("followingBalls"));
return !element.Displayed;
}
catch (NoSuchElementException)

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Threading;
using FluentAssertions;
using NUnit.Framework;
@@ -65,9 +65,9 @@ namespace NzbDrone.Common.Test.CacheTests
[Test]
public void should_store_null()
{
int hitCount = 0;
var hitCount = 0;
for (int i = 0; i < 10; i++)
for (var i = 0; i < 10; i++)
{
_cachedString.Get("key", () =>
{
@@ -83,10 +83,10 @@ namespace NzbDrone.Common.Test.CacheTests
[Platform(Exclude = "MacOsX")]
public void should_honor_ttl()
{
int hitCount = 0;
var hitCount = 0;
_cachedString = new Cached<string>();
for (int i = 0; i < 10; i++)
for (var i = 0; i < 10; i++)
{
_cachedString.Get("key",
() =>

View File

@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using FluentAssertions;
using Moq;
using NUnit.Framework;
@@ -142,7 +142,7 @@ namespace NzbDrone.Common.Test
[Test]
public void SaveDictionary_should_save_proper_value()
{
int port = 20555;
var port = 20555;
var dic = Subject.GetConfigDictionary();
dic["Port"] = 20555;
@@ -155,9 +155,9 @@ namespace NzbDrone.Common.Test
[Test]
public void SaveDictionary_should_only_save_specified_values()
{
int port = 20555;
int origSslPort = 20551;
int sslPort = 20552;
var port = 20555;
var origSslPort = 20551;
var sslPort = 20552;
var dic = Subject.GetConfigDictionary();
dic["Port"] = port;

View File

@@ -42,7 +42,7 @@ namespace NzbDrone.Common.Test.DiskTests
[Test]
public void should_not_contain_recycling_bin_for_root_of_drive()
{
string root = @"C:\".AsOsAgnostic();
var root = @"C:\".AsOsAgnostic();
SetupFolders(root);
Mocker.GetMock<IDiskProvider>()
@@ -55,7 +55,7 @@ namespace NzbDrone.Common.Test.DiskTests
[Test]
public void should_not_contain_system_volume_information()
{
string root = @"C:\".AsOsAgnostic();
var root = @"C:\".AsOsAgnostic();
SetupFolders(root);
Mocker.GetMock<IDiskProvider>()
@@ -68,7 +68,7 @@ namespace NzbDrone.Common.Test.DiskTests
[Test]
public void should_not_contain_recycling_bin_or_system_volume_information_for_root_of_drive()
{
string root = @"C:\".AsOsAgnostic();
var root = @"C:\".AsOsAgnostic();
SetupFolders(root);
Mocker.GetMock<IDiskProvider>()

View File

@@ -788,7 +788,7 @@ namespace NzbDrone.Common.Test.Http
try
{
// the date is bad in the below - should be 13-Jul-2026
string malformedCookie = @"__cfduid=d29e686a9d65800021c66faca0a29b4261436890790; expires=Mon, 13-Jul-26 16:19:50 GMT; path=/; HttpOnly";
var malformedCookie = @"__cfduid=d29e686a9d65800021c66faca0a29b4261436890790; expires=Mon, 13-Jul-26 16:19:50 GMT; path=/; HttpOnly";
var requestSet = new HttpRequestBuilder($"https://{_httpBinHost}/response-headers")
.AddQueryParam("Set-Cookie", malformedCookie)
.Build();
@@ -822,7 +822,7 @@ namespace NzbDrone.Common.Test.Http
{
try
{
string url = $"https://{_httpBinHost}/response-headers?Set-Cookie={Uri.EscapeDataString(malformedCookie)}";
var url = $"https://{_httpBinHost}/response-headers?Set-Cookie={Uri.EscapeDataString(malformedCookie)}";
var requestSet = new HttpRequest(url);
requestSet.AllowAutoRedirect = false;

View File

@@ -10,7 +10,9 @@ namespace NzbDrone.Common.Test.InstrumentationTests
// Indexer Urls
[TestCase(@"https://iptorrents.com/torrents/rss?u=mySecret;tp=mySecret;l5;download")]
[TestCase(@"http://rss.torrentleech.org/mySecret")]
[TestCase(@"http://rss.torrentleech.org/rss/download/12345/01233210/filename.torrent")]
[TestCase(@"https://rss24h.torrentleech.org/mySecret")]
[TestCase(@"http://rss.torrentleech.org/rss/download/12345/01233210/file.name-RLSGRP.torrent")]
[TestCase(@"https://www.torrentleech.org/rss/download/12345/01233210/file.name-RLSGRP.torrent")]
[TestCase(@"http://www.bitmetv.org/rss.php?uid=mySecret&passkey=mySecret")]
[TestCase(@"https://rss.omgwtfnzbs.org/rss-search.php?catid=19,20&user=sonarr&api=mySecret&eng=1")]
[TestCase(@"https://dognzb.cr/fetch/2b51db35e1912ffc138825a12b9933d2/2b51db35e1910123321025a12b9933d2")]
@@ -79,6 +81,7 @@ namespace NzbDrone.Common.Test.InstrumentationTests
// Deluge
[TestCase(@",{""download_location"": ""C:\Users\\mySecret mySecret\\Downloads""}")]
[TestCase(@",{""download_location"": ""/home/mySecret/Downloads""}")]
[TestCase(@",{""download_location"": ""/Users/mySecret/Downloads""}")]
[TestCase(@"auth.login(""mySecret"")")]
// Download Station

View File

@@ -74,17 +74,17 @@ namespace NzbDrone.Common
continue; // Ignore directories
}
string entryFileName = zipEntry.Name;
var entryFileName = zipEntry.Name;
// to remove the folder from the entry:- entryFileName = Path.GetFileName(entryFileName);
// Optionally match entrynames against a selection list here to skip as desired.
// The unpacked length is available in the zipEntry.Size property.
byte[] buffer = new byte[4096]; // 4K is optimum
Stream zipStream = zipFile.GetInputStream(zipEntry);
var buffer = new byte[4096]; // 4K is optimum
var zipStream = zipFile.GetInputStream(zipEntry);
// Manipulate the output filename here as desired.
string fullZipToPath = Path.Combine(destination, entryFileName);
string directoryName = Path.GetDirectoryName(fullZipToPath);
var fullZipToPath = Path.Combine(destination, entryFileName);
var directoryName = Path.GetDirectoryName(fullZipToPath);
if (directoryName.Length > 0)
{
Directory.CreateDirectory(directoryName);
@@ -93,7 +93,7 @@ namespace NzbDrone.Common
// Unzip file in buffered chunks. This is just as fast as unpacking to a buffer the full size
// of the file, but does not waste memory.
// The "using" will close the stream even if an exception occurs.
using (FileStream streamWriter = File.Create(fullZipToPath))
using (var streamWriter = File.Create(fullZipToPath))
{
StreamUtils.Copy(zipStream, streamWriter, buffer);
}
@@ -106,7 +106,7 @@ namespace NzbDrone.Common
Stream inStream = File.OpenRead(compressedFile);
Stream gzipStream = new GZipInputStream(inStream);
TarArchive tarArchive = TarArchive.CreateInputTarArchive(gzipStream, null);
var tarArchive = TarArchive.CreateInputTarArchive(gzipStream, null);
tarArchive.ExtractContents(destination);
tarArchive.Close();

View File

@@ -47,8 +47,7 @@ namespace NzbDrone.Common.Cache
public T Find(string key)
{
CacheItem cacheItem;
if (!_store.TryGetValue(key, out cacheItem))
if (!_store.TryGetValue(key, out var cacheItem))
{
return default(T);
}
@@ -76,8 +75,7 @@ namespace NzbDrone.Common.Cache
public void Remove(string key)
{
CacheItem value;
_store.TryRemove(key, out value);
_store.TryRemove(key, out _);
}
public int Count => _store.Count;
@@ -88,9 +86,7 @@ namespace NzbDrone.Common.Cache
lifeTime = lifeTime ?? _defaultLifeTime;
CacheItem cacheItem;
if (_store.TryGetValue(key, out cacheItem) && !cacheItem.IsExpired())
if (_store.TryGetValue(key, out var cacheItem) && !cacheItem.IsExpired())
{
if (_rollingExpiry && lifeTime.HasValue)
{

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
@@ -86,9 +86,7 @@ namespace NzbDrone.Common.Cache
{
RefreshIfExpired();
TValue result;
if (!_items.TryGetValue(key, out result))
if (!_items.TryGetValue(key, out var result))
{
throw new KeyNotFoundException(string.Format("Item {0} not found in cache.", key));
}
@@ -100,9 +98,7 @@ namespace NzbDrone.Common.Cache
{
RefreshIfExpired();
TValue result;
_items.TryGetValue(key, out result);
_items.TryGetValue(key, out var result);
return result;
}
@@ -128,8 +124,7 @@ namespace NzbDrone.Common.Cache
public void Remove(string key)
{
TValue item;
_items.TryRemove(key, out item);
_items.TryRemove(key, out _);
}
}
}

View File

@@ -255,7 +255,7 @@ namespace NzbDrone.Common.Disk
var stringComparison = (Kind == OsPathKind.Windows || other.Kind == OsPathKind.Windows) ? StringComparison.InvariantCultureIgnoreCase : StringComparison.InvariantCulture;
for (int i = 0; i < leftFragments.Length; i++)
for (var i = 0; i < leftFragments.Length; i++)
{
if (!string.Equals(leftFragments[i], rightFragments[i], stringComparison))
{
@@ -372,12 +372,12 @@ namespace NzbDrone.Common.Disk
var newFragments = new List<string>();
for (int j = i; j < rightFragments.Length; j++)
for (var j = i; j < rightFragments.Length; j++)
{
newFragments.Add("..");
}
for (int j = i; j < leftFragments.Length; j++)
for (var j = i; j < leftFragments.Length; j++)
{
newFragments.Add(leftFragments[j]);
}

View File

@@ -36,14 +36,14 @@ namespace NzbDrone.Common.Extensions
public static bool IsValidDate(this string dateTime)
{
DateTime.TryParse(dateTime, out DateTime result);
DateTime.TryParse(dateTime, out var result);
return !result.Equals(default(DateTime));
}
public static bool IsFutureDate(this string dateTime)
{
DateTime.TryParse(dateTime, out DateTime result);
DateTime.TryParse(dateTime, out var result);
return !result.Equals(default(DateTime)) && result.After(DateTime.Now);
}

View File

@@ -126,9 +126,9 @@ namespace NzbDrone.Common.Extensions
private static IEnumerable<T> InternalDropLast<T>(IEnumerable<T> source, int n)
{
Queue<T> buffer = new Queue<T>(n + 1);
var buffer = new Queue<T>(n + 1);
foreach (T x in source)
foreach (var x in source)
{
buffer.Enqueue(x);

View File

@@ -21,7 +21,7 @@ namespace NzbDrone.Common.Extensions
return text.Length * costDelete;
}
int[] matrix = new int[other.Length + 1];
var matrix = new int[other.Length + 1];
for (var i = 1; i < matrix.Length; i++)
{
@@ -30,13 +30,13 @@ namespace NzbDrone.Common.Extensions
for (var i = 0; i < text.Length; i++)
{
int topLeft = matrix[0];
var topLeft = matrix[0];
matrix[0] = matrix[0] + costDelete;
for (var j = 0; j < other.Length; j++)
{
int top = matrix[j];
int left = matrix[j + 1];
var top = matrix[j];
var left = matrix[j + 1];
var sumIns = top + costInsert;
var sumDel = left + costDelete;

View File

@@ -198,13 +198,13 @@ namespace NzbDrone.Common.Extensions
public static string CleanFileName(this string name)
{
string result = name;
var result = name;
string[] badCharacters = { "\\", "/", "<", ">", "?", "*", ":", "|", "\"" };
string[] goodCharacters = { "+", "+", "", "", "!", "-", "-", "", "" };
result = result.Replace(": ", " - ");
for (int i = 0; i < badCharacters.Length; i++)
for (var i = 0; i < badCharacters.Length; i++)
{
result = result.Replace(badCharacters[i], goodCharacters[i]);
}

View File

@@ -6,9 +6,7 @@ namespace NzbDrone.Common.Extensions
{
public static int? ParseInt32(this string source)
{
int result;
if (int.TryParse(source, out result))
if (int.TryParse(source, out var result))
{
return result;
}
@@ -18,9 +16,7 @@ namespace NzbDrone.Common.Extensions
public static long? ParseInt64(this string source)
{
long result;
if (long.TryParse(source, out result))
if (long.TryParse(source, out var result))
{
return result;
}
@@ -30,9 +26,7 @@ namespace NzbDrone.Common.Extensions
public static double? ParseDouble(this string source)
{
double result;
if (double.TryParse(source.Replace(',', '.'), NumberStyles.Number, CultureInfo.InvariantCulture, out result))
if (double.TryParse(source.Replace(',', '.'), NumberStyles.Number, CultureInfo.InvariantCulture, out var result))
{
return result;
}

View File

@@ -8,9 +8,9 @@ namespace NzbDrone.Common
{
public static string CalculateCrc(string input)
{
uint mCrc = 0xffffffff;
byte[] bytes = Encoding.UTF8.GetBytes(input);
foreach (byte myByte in bytes)
var mCrc = 0xffffffff;
var bytes = Encoding.UTF8.GetBytes(input);
foreach (var myByte in bytes)
{
mCrc ^= (uint)myByte << 24;
for (var i = 0; i < 8; i++)

View File

@@ -46,7 +46,11 @@ namespace NzbDrone.Common.Http.Dispatchers
public async Task<HttpResponse> GetResponseAsync(HttpRequest request, CookieContainer cookies)
{
var requestMessage = new HttpRequestMessage(request.Method, (Uri)request.Url);
var requestMessage = new HttpRequestMessage(request.Method, (Uri)request.Url)
{
Version = HttpVersion.Version20,
VersionPolicy = HttpVersionPolicy.RequestVersionOrLower
};
requestMessage.Headers.UserAgent.ParseAdd(_userAgentBuilder.GetUserAgent(request.UseSimplifiedUserAgent));
requestMessage.Headers.ConnectionClose = !request.ConnectionKeepAlive;
@@ -127,7 +131,7 @@ namespace NzbDrone.Common.Http.Dispatchers
headers.Add(responseMessage.Content.Headers.ToNameValueCollection());
CookieContainer responseCookies = new CookieContainer();
var responseCookies = new CookieContainer();
if (responseMessage.Headers.TryGetValues("Set-Cookie", out var cookieHeaders))
{
@@ -148,7 +152,7 @@ namespace NzbDrone.Common.Http.Dispatchers
sw.Stop();
return new HttpResponse(request, new HttpHeader(headers), cookieCollection, data, sw.ElapsedMilliseconds, responseMessage.StatusCode);
return new HttpResponse(request, new HttpHeader(headers), cookieCollection, data, sw.ElapsedMilliseconds, responseMessage.StatusCode, responseMessage.Version);
}
}
@@ -186,6 +190,8 @@ namespace NzbDrone.Common.Http.Dispatchers
var client = new System.Net.Http.HttpClient(handler)
{
DefaultRequestVersion = HttpVersion.Version20,
DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrLower,
Timeout = Timeout.InfiniteTimeSpan
};

View File

@@ -91,6 +91,7 @@ namespace NzbDrone.Common.Http
{
request.Method = HttpMethod.Get;
request.ContentData = null;
request.ContentSummary = null;
}
// Save to add to final response

View File

@@ -9,9 +9,9 @@ namespace NzbDrone.Common.Http
{
public class HttpResponse
{
private static readonly Regex RegexRefresh = new Regex("^(.*?url)=(.*?)(?:;|$)", RegexOptions.Compiled);
private static readonly Regex RegexRefresh = new ("^(.*?url)=(.*?)(?:;|$)", RegexOptions.Compiled);
public HttpResponse(HttpRequest request, HttpHeader headers, CookieCollection cookies, byte[] binaryData, long elapsedTime = 0, HttpStatusCode statusCode = HttpStatusCode.OK)
public HttpResponse(HttpRequest request, HttpHeader headers, CookieCollection cookies, byte[] binaryData, long elapsedTime = 0, HttpStatusCode statusCode = HttpStatusCode.OK, Version version = null)
{
Request = request;
Headers = headers;
@@ -19,9 +19,10 @@ namespace NzbDrone.Common.Http
ResponseData = binaryData;
StatusCode = statusCode;
ElapsedTime = elapsedTime;
Version = version;
}
public HttpResponse(HttpRequest request, HttpHeader headers, CookieCollection cookies, string content, long elapsedTime = 0, HttpStatusCode statusCode = HttpStatusCode.OK)
public HttpResponse(HttpRequest request, HttpHeader headers, CookieCollection cookies, string content, long elapsedTime = 0, HttpStatusCode statusCode = HttpStatusCode.OK, Version version = null)
{
Request = request;
Headers = headers;
@@ -30,6 +31,7 @@ namespace NzbDrone.Common.Http
_content = content;
StatusCode = statusCode;
ElapsedTime = elapsedTime;
Version = version;
}
public HttpRequest Request { get; private set; }
@@ -37,6 +39,7 @@ namespace NzbDrone.Common.Http
public CookieCollection Cookies { get; private set; }
public HttpStatusCode StatusCode { get; private set; }
public long ElapsedTime { get; private set; }
public Version Version { get; private set; }
public byte[] ResponseData { get; private set; }
private string _content;
@@ -63,6 +66,8 @@ namespace NzbDrone.Common.Http
public bool HasHttpError => (int)StatusCode >= 400;
public bool HasHttpServerError => (int)StatusCode >= 500;
public bool HasHttpRedirect => StatusCode == HttpStatusCode.Moved ||
StatusCode == HttpStatusCode.Found ||
StatusCode == HttpStatusCode.SeeOther ||
@@ -119,7 +124,7 @@ namespace NzbDrone.Common.Http
public override string ToString()
{
var result = string.Format("Res: [{0}] {1}: {2}.{3} ({4} bytes)", Request.Method, Request.Url, (int)StatusCode, StatusCode, ResponseData?.Length ?? 0);
var result = $"Res: HTTP/{Version} [{Request.Method}] {Request.Url}: {(int)StatusCode}.{StatusCode} ({ResponseData?.Length ?? 0} bytes)";
if (HasHttpError && Headers.ContentType.IsNotNullOrWhiteSpace() && !Headers.ContentType.Equals("text/html", StringComparison.InvariantCultureIgnoreCase))
{
@@ -134,7 +139,7 @@ namespace NzbDrone.Common.Http
where T : new()
{
public HttpResponse(HttpResponse response)
: base(response.Request, response.Headers, response.Cookies, response.ResponseData, response.ElapsedTime, response.StatusCode)
: base(response.Request, response.Headers, response.Cookies, response.ResponseData, response.ElapsedTime, response.StatusCode, response.Version)
{
Resource = Json.Deserialize<T>(response.Content);
}

View File

@@ -22,7 +22,7 @@ namespace NzbDrone.Common.Http
public HttpUri(string scheme, string host, int? port, string path, string query, string fragment)
{
StringBuilder builder = new StringBuilder();
var builder = new StringBuilder();
if (scheme.IsNotNullOrWhiteSpace())
{

View File

@@ -31,7 +31,7 @@ namespace NzbDrone.Common.Http.Proxy
if (!string.IsNullOrWhiteSpace(BypassFilter))
{
var hostlist = BypassFilter.Split(',');
for (int i = 0; i < hostlist.Length; i++)
for (var i = 0; i < hostlist.Length; i++)
{
if (hostlist[i].StartsWith("*"))
{

View File

@@ -9,61 +9,61 @@ namespace NzbDrone.Common.Instrumentation
{
private static readonly Regex[] CleansingRules =
{
// Url
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(@"rss\.torrentleech\.org/(?!rss)(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"rss\.torrentleech\.org/rss/download/[0-9]+/(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"iptorrents\.com/[/a-z0-9?&;]*?(?:[?&;](u|tp)=(?<secret>[^&=;]+?))+(?= |;|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"/fetch/[a-z0-9]{32}/(?<secret>[a-z0-9]{32})", RegexOptions.Compiled),
new Regex(@"getnzb.*?(?<=\?|&)(r)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"\b(\w*)?(_?(?<!use|get_)token|username|passwo?rd)=(?<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]+/torrent/download/[\w\d-]+[.]\d+[.])(?<secret>[a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Url
new (@"(?<=[?&: ;])(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 (@"(?<=[?& ;])[^=]*?(_?(?<!use|get_)token|username|passwo?rd)=(?<secret>[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new (@"rss(24h)?\.torrentleech\.org/(?!rss)(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new (@"torrentleech\.org/rss/download/[0-9]+/(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new (@"iptorrents\.com/[/a-z0-9?&;]*?(?:[?&;](u|tp)=(?<secret>[^&=;]+?))+(?= |;|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new (@"/fetch/[a-z0-9]{32}/(?<secret>[a-z0-9]{32})", RegexOptions.Compiled),
new (@"getnzb.*?(?<=\?|&)(r)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new (@"\b(\w*)?(_?(?<!use|get_)token|username|passwo?rd)=(?<secret>[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new (@"(?<=authkey = "")(?<secret>[^&=]+?)(?="")", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new (@"(?<=beyond-hd\.[a-z]+/api/torrents/)(?<secret>[^&=][a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new (@"(?<=beyond-hd\.[a-z]+/torrent/download/[\w\d-]+[.]\d+[.])(?<secret>[a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// UNIT3D
new Regex(@"(?<=[a-z0-9-]+\.[a-z]+/torrent/download/\d+\.)(?<secret>[^&=][a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// UNIT3D
new (@"(?<=[a-z0-9-]+\.[a-z]+/torrent/download/\d+\.)(?<secret>[^&=][a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Path
new Regex(@"""C:\\Users\\(?<secret>[^\""]+?)(\\|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"""/home/(?<secret>[^/""]+?)(/|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Path
new (@"""C:\\Users\\(?<secret>[^\""]+?)(\\|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new (@"""/(home|Users)/(?<secret>[^/""]+?)(/|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Trackers Announce Keys; Designed for Qbit Json; should work for all in theory
new Regex(@"announce(\.php)?(/|%2f|%3fpasskey%3d)(?<secret>[a-z0-9]{16,})|(?<secret>[a-z0-9]{16,})(/|%2f)announce"),
// Trackers Announce Keys; Designed for Qbit Json; should work for all in theory
new (@"announce(\.php)?(/|%2f|%3fpasskey%3d)(?<secret>[a-z0-9]{16,})|(?<secret>[a-z0-9]{16,})(/|%2f)announce"),
// NzbGet
new Regex(@"""Name""\s*:\s*""[^""]*(username|password)""\s*,\s*""Value""\s*:\s*""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// NzbGet
new (@"""Name""\s*:\s*""[^""]*(username|password)""\s*,\s*""Value""\s*:\s*""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Sabnzbd
new Regex(@"""[^""]*(username|password|api_?key|nzb_key)""\s*:\s*""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"""email_(account|to|from|pwd)""\s*:\s*""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Sabnzbd
new (@"""[^""]*(username|password|api_?key|nzb_key)""\s*:\s*""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new (@"""email_(account|to|from|pwd)""\s*:\s*""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// uTorrent
new Regex(@"\[""[a-z._]*(username|password)"",\d,""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"\[""(boss_key|boss_key_salt|proxy\.proxy)"",\d,""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// uTorrent
new (@"\[""[a-z._]*(username|password)"",\d,""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new (@"\[""(boss_key|boss_key_salt|proxy\.proxy)"",\d,""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Deluge
new Regex(@"auth.login\(""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Deluge
new (@"auth.login\(""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// BroadcastheNet (;torrent_pass|torrents_notify_ is for MTV)
new Regex(@"""?method""?\s*:\s*""(getTorrents)"",\s*""?params""?\s*:\s*\[\s*""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"getTorrents\(""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?<=\?|&|;|=)(authkey|torrent_pass|torrents_notify)[_=](?<secret>[^&=]+?)(?=""|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// BroadcastheNet (;torrent_pass|torrents_notify_ is for MTV)
new (@"""?method""?\s*:\s*""(getTorrents)"",\s*""?params""?\s*:\s*\[\s*""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new (@"getTorrents\(""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new (@"(?<=\?|&|;|=)(authkey|torrent_pass|torrents_notify)[_=](?<secret>[^&=]+?)(?=""|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Plex
new Regex(@"(?<=\?|&)(X-Plex-Client-Identifier|X-Plex-Token)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Plex
new (@"(?<=\?|&)(X-Plex-Client-Identifier|X-Plex-Token)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Indexer Responses
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(@",""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(@",""rss[- _]?key"":""(?<secret>[^&=]+?)"",", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Indexer Responses
new (@"(?:avistaz|exoticaz|cinemaz|privatehd)\.[a-z]{2,3}/rss/download/(?<secret>[^&=]+?)/(?<secret>[^&=]+?)\.torrent", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new (@"(?:animebytes)\.[a-z]{2,3}/torrent/[0-9]+/download/(?<secret>[^&=]+?)[""]", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new (@",""info_hash"":""(?<secret>[^&=]+?)"",", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new (@"""token"":""(?<secret>[^&=]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new (@",""pass[- _]?key"":""(?<secret>[^&=]+?)"",", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new (@",""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 (@"(?:Auth-\w+(?<!Failure|Unauthorized) ip|from) (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})", RegexOptions.Compiled);
public static string Cleanse(string message)
{
@@ -75,15 +75,15 @@ namespace NzbDrone.Common.Instrumentation
foreach (var regex in CleansingRules)
{
message = regex.Replace(message, m =>
{
var value = m.Value;
foreach (var capture in m.Groups["secret"].Captures.OfType<Capture>().Reverse())
{
var value = m.Value;
foreach (var capture in m.Groups["secret"].Captures.OfType<Capture>().Reverse())
{
value = value.Replace(capture.Index - m.Index, capture.Length, "(removed)");
}
value = value.Replace(capture.Index - m.Index, capture.Length, "(removed)");
}
return value;
});
return value;
});
}
message = CleanseRemoteIPRegex.Replace(message, CleanseRemoteIP);

Some files were not shown because too many files have changed in this diff Show More