mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2026-04-16 21:35:04 -04:00
Compare commits
210 Commits
v1.3.2.298
...
v1.5.1.342
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3ddf2f9cd | ||
|
|
d9ce9eb0b2 | ||
|
|
29ab1801db | ||
|
|
19ff73dad0 | ||
|
|
c455f1a113 | ||
|
|
b8793d8783 | ||
|
|
ce34940287 | ||
|
|
dcb19a66b0 | ||
|
|
b3bc92e60e | ||
|
|
1b17d38564 | ||
|
|
d8c7361205 | ||
|
|
7a0dd0bc0d | ||
|
|
c02bfb5930 | ||
|
|
d0fbb1f49a | ||
|
|
aafdefe2f0 | ||
|
|
96234c0fe1 | ||
|
|
8b5648d7bd | ||
|
|
1fc79f9e9b | ||
|
|
ec40761757 | ||
|
|
0a8e4eb092 | ||
|
|
ade961fad5 | ||
|
|
81b1c0e445 | ||
|
|
0fe54ed36a | ||
|
|
337828ff9c | ||
|
|
fb34294d2e | ||
|
|
931e3cf42d | ||
|
|
051930455e | ||
|
|
eba5413250 | ||
|
|
cc2f50544b | ||
|
|
450c6d7af5 | ||
|
|
bdc0178e44 | ||
|
|
aa9705846e | ||
|
|
7559a87bc8 | ||
|
|
6a7fe30171 | ||
|
|
2b0f4e18e7 | ||
|
|
4a5a986220 | ||
|
|
38ae17a99f | ||
|
|
9a72da2803 | ||
|
|
3bba76caab | ||
|
|
47ceabc834 | ||
|
|
48bb3196dd | ||
|
|
4c4ebdf17c | ||
|
|
b5706a0d55 | ||
|
|
d946ef4a9e | ||
|
|
48ec5bbaa1 | ||
|
|
2bcdae44c7 | ||
|
|
541b8b4f7f | ||
|
|
8dd79c38d5 | ||
|
|
615b85fffe | ||
|
|
ceab19caf9 | ||
|
|
3d61719a2c | ||
|
|
befb354913 | ||
|
|
10bbaee55d | ||
|
|
131550b92d | ||
|
|
5f83da9725 | ||
|
|
1ca8ff5012 | ||
|
|
061a0c0da8 | ||
|
|
4cc2706ee5 | ||
|
|
32691832a5 | ||
|
|
48977de3b8 | ||
|
|
34fbb3e135 | ||
|
|
d38f2614d3 | ||
|
|
ecc5439464 | ||
|
|
795274e7e1 | ||
|
|
eb96fbe956 | ||
|
|
2f1fb396a5 | ||
|
|
20c085a979 | ||
|
|
4990e537eb | ||
|
|
f8111ac7ba | ||
|
|
cb1fd39cb3 | ||
|
|
5e9094b54c | ||
|
|
746d84cf83 | ||
|
|
bbe3241b83 | ||
|
|
a86aa4c5d3 | ||
|
|
a753f721d1 | ||
|
|
202836110e | ||
|
|
1a5e41d831 | ||
|
|
e1d0e2c799 | ||
|
|
92e7a38bd0 | ||
|
|
008f238dda | ||
|
|
40125046fa | ||
|
|
1fd188fe7a | ||
|
|
5e5699fbbe | ||
|
|
d61275e6db | ||
|
|
dca3e939f0 | ||
|
|
26ac66c0e1 | ||
|
|
649b301444 | ||
|
|
78ed2a1af0 | ||
|
|
a4854b7b5f | ||
|
|
97edf495bd | ||
|
|
d10bdf4676 | ||
|
|
03647143e3 | ||
|
|
8090dc9983 | ||
|
|
5bc1f345c0 | ||
|
|
4ef01f5640 | ||
|
|
f13d5c5a14 | ||
|
|
dc8773cf79 | ||
|
|
cad774e250 | ||
|
|
b28eee578a | ||
|
|
5b8c7d0b79 | ||
|
|
8bdc7a6db7 | ||
|
|
cb189b8f61 | ||
|
|
24468db376 | ||
|
|
9b10cea556 | ||
|
|
d8fb71d501 | ||
|
|
fc39a11ece | ||
|
|
40dc4de47d | ||
|
|
a0e2f3324c | ||
|
|
1bcc3b426e | ||
|
|
66f5fd2a26 | ||
|
|
b5e5701791 | ||
|
|
1a9b202afe | ||
|
|
309f42bac5 | ||
|
|
ed330ea657 | ||
|
|
fc6a31ea78 | ||
|
|
25ba9195cf | ||
|
|
681f06e321 | ||
|
|
af7fb442d2 | ||
|
|
2061b9142f | ||
|
|
b97f6f8ddf | ||
|
|
f31c0bb1de | ||
|
|
65e6aa05c3 | ||
|
|
fb20b3e61b | ||
|
|
b8a77830aa | ||
|
|
d2ba52cdce | ||
|
|
43f881c442 | ||
|
|
4a5e923999 | ||
|
|
57e1b6b4a0 | ||
|
|
9cc60760c3 | ||
|
|
2811feb14e | ||
|
|
46af9223bc | ||
|
|
025156978b | ||
|
|
d3ca861aea | ||
|
|
c9249ed583 | ||
|
|
94cc56d0f6 | ||
|
|
c8addc0d62 | ||
|
|
2015156061 | ||
|
|
742c680014 | ||
|
|
b1add3f649 | ||
|
|
65d6d518d7 | ||
|
|
bc8ba5ca02 | ||
|
|
6aebc4ee01 | ||
|
|
9bbe51253b | ||
|
|
88fbc30be2 | ||
|
|
5fdc6ee25d | ||
|
|
4eb5a2d613 | ||
|
|
122883053a | ||
|
|
28d09cd384 | ||
|
|
17be8bb68a | ||
|
|
c5baded3d6 | ||
|
|
349cfacdca | ||
|
|
788fa6d96a | ||
|
|
fbea5bbc06 | ||
|
|
d667c7d853 | ||
|
|
a9e1204a9b | ||
|
|
88e3f86262 | ||
|
|
1c173fc984 | ||
|
|
6e8f3d814a | ||
|
|
14e105e37e | ||
|
|
9e0deb8f74 | ||
|
|
245e573089 | ||
|
|
5e8bfa2ffb | ||
|
|
555c924e50 | ||
|
|
8404b85624 | ||
|
|
dc5e6d29e1 | ||
|
|
8c42b7a69b | ||
|
|
3a6ebdef8a | ||
|
|
5f57957462 | ||
|
|
12526c1bb3 | ||
|
|
29f049f766 | ||
|
|
40f4e1b82a | ||
|
|
065fbb30bf | ||
|
|
ea24a81ef7 | ||
|
|
451f60319f | ||
|
|
c6ed5d65e0 | ||
|
|
4e5cd05bbd | ||
|
|
6b2b953686 | ||
|
|
31c05be9de | ||
|
|
bc852c0b55 | ||
|
|
18651d8be1 | ||
|
|
1608095345 | ||
|
|
7700014ceb | ||
|
|
3fbc2912f0 | ||
|
|
3192990874 | ||
|
|
fb908e8e19 | ||
|
|
8e60c707b2 | ||
|
|
a184bb0784 | ||
|
|
e5ccbaaf24 | ||
|
|
362e0acad1 | ||
|
|
54d06460d0 | ||
|
|
c11bcf4c41 | ||
|
|
2e58583263 | ||
|
|
bf7f769f13 | ||
|
|
7820a83a5d | ||
|
|
d937bdac69 | ||
|
|
ebca32af46 | ||
|
|
21bda07510 | ||
|
|
f638cf34d1 | ||
|
|
b7fcdb5356 | ||
|
|
2e4fa9d06d | ||
|
|
9b50fc40ca | ||
|
|
3c60159df0 | ||
|
|
e075003c8b | ||
|
|
b19202d9f5 | ||
|
|
2784ee8ce6 | ||
|
|
5aa4a5faaa | ||
|
|
1d00b40f90 | ||
|
|
93dd378ade | ||
|
|
534ca73bf8 | ||
|
|
bceebc34c1 |
@@ -40,6 +40,9 @@ csharp_style_var_for_built_in_types = true:suggestion
|
||||
csharp_style_var_when_type_is_apparent = true:suggestion
|
||||
csharp_style_var_elsewhere = true:suggestion
|
||||
|
||||
# Using directive is unnecessary.
|
||||
dotnet_diagnostic.IDE0005.severity = error
|
||||
|
||||
# Stylecop Rules
|
||||
dotnet_diagnostic.SA0001.severity = none
|
||||
dotnet_diagnostic.SA1005.severity = none
|
||||
|
||||
7
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
7
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -71,3 +71,10 @@ body:
|
||||
Additionally, any additional info? Screenshots? References? Anything that will give us more context about the issue you are encountering!
|
||||
validations:
|
||||
required: true
|
||||
- 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
|
||||
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.
|
||||
required: true
|
||||
|
||||
9
.github/label-actions.yml
vendored
9
.github/label-actions.yml
vendored
@@ -13,4 +13,11 @@
|
||||
:wave: @{issue-author}, we use the issue tracker exclusively
|
||||
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: true
|
||||
|
||||
'Status: Logs Needed':
|
||||
comment: >
|
||||
:wave: @{issue-author}, in order to help you further we'll need to see logs.
|
||||
You'll need to enable trace logging and replicate the problem that you encountered.
|
||||
Guidance on how to enable trace logging can be found in
|
||||
our [troubleshooting guide](https://wiki.servarr.com/prowlarr/troubleshooting#logging-and-log-files).
|
||||
19
.github/labeler.yml
vendored
Normal file
19
.github/labeler.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
'Area: API':
|
||||
- src/Prowlarr.Api.V1/**/*
|
||||
|
||||
'Area: Db-migration':
|
||||
- src/NzbDrone.Core/Datastore/Migration/*
|
||||
|
||||
'Area: Download Clients':
|
||||
- src/NzbDrone.Core/Download/Clients/**/*
|
||||
|
||||
'Area: Indexer':
|
||||
- src/NzbDrone.Core/Indexers/**/*
|
||||
|
||||
'Area: Notifications':
|
||||
- src/NzbDrone.Core/Notifications/**/*
|
||||
|
||||
'Area: UI':
|
||||
- frontend/**/*
|
||||
- package.json
|
||||
- yarn.lock
|
||||
12
.github/workflows/labeler.yml
vendored
Normal file
12
.github/workflows/labeler.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
name: "Pull Request Labeler"
|
||||
on:
|
||||
- pull_request_target
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@v4
|
||||
@@ -9,13 +9,13 @@ variables:
|
||||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '1.3.2'
|
||||
majorVersion: '1.5.1'
|
||||
minorVersion: $[counter('minorVersion', 1)]
|
||||
prowlarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
|
||||
sentryOrg: 'servarr'
|
||||
sentryUrl: 'https://sentry.servarr.com'
|
||||
dotnetVersion: '6.0.405'
|
||||
dotnetVersion: '6.0.408'
|
||||
innoVersion: '6.2.0'
|
||||
nodeVersion: '16.x'
|
||||
windowsImage: 'windows-2022'
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"remove-empty-rulesets": true,
|
||||
"always-semicolon": true,
|
||||
"color-case": "lower",
|
||||
"block-indent": " ",
|
||||
"color-shorthand": false,
|
||||
"element-case": "lower",
|
||||
"eof-newline": true,
|
||||
"leading-zero": true,
|
||||
"quotes": "double",
|
||||
"sort-order-fallback": "abc",
|
||||
"space-before-colon": "",
|
||||
"space-after-colon": " ",
|
||||
"space-before-combinator": " ",
|
||||
"space-after-combinator": " ",
|
||||
"space-between-declarations": "\n",
|
||||
"space-before-opening-brace": " ",
|
||||
"space-after-opening-brace": "\n",
|
||||
"space-after-selector-delimiter": " ",
|
||||
"space-before-selector-delimiter": "",
|
||||
"space-before-closing-brace": "\n",
|
||||
"strip-spaces": true,
|
||||
"tab-size": true,
|
||||
"unitless-zero": false
|
||||
}
|
||||
@@ -1,335 +0,0 @@
|
||||
{
|
||||
"indent": {
|
||||
"value": " ",
|
||||
"FunctionExpression": 1,
|
||||
"ArrayExpression": 1,
|
||||
"ObjectExpression": 1
|
||||
},
|
||||
"lineBreak": {
|
||||
"value": "\n",
|
||||
|
||||
"before": {
|
||||
"ArrayPatternClosing": 0,
|
||||
"ArrayPatternComma": 0,
|
||||
"ArrayPatternOpening": 0,
|
||||
"ArrowFunctionExpressionArrow": 0,
|
||||
"ArrowFunctionExpressionClosingBrace": ">=1",
|
||||
"ArrowFunctionExpressionOpeningBrace": 0,
|
||||
"AssignmentExpression": ">=1",
|
||||
"AssignmentOperator": 0,
|
||||
"BlockStatement": 0,
|
||||
"BreakKeyword": ">=1",
|
||||
"CallExpression": -1,
|
||||
"CallExpressionClosingParentheses": -1,
|
||||
"CallExpressionOpeningParentheses": 0,
|
||||
"CatchClosingBrace": ">=1",
|
||||
"CatchKeyword": 0,
|
||||
"CatchOpeningBrace": 0,
|
||||
"ClassDeclaration": ">=1",
|
||||
"ClassDeclarationClosingBrace": ">=1",
|
||||
"ClassDeclarationOpeningBrace": 0,
|
||||
"ConditionalExpression": ">=1",
|
||||
"DeleteOperator": ">=1",
|
||||
"DoWhileStatement": ">=1",
|
||||
"DoWhileStatementClosingBrace": ">=1",
|
||||
"DoWhileStatementOpeningBrace": 0,
|
||||
"ElseIfStatement": 0,
|
||||
"ElseIfStatementClosingBrace": ">=1",
|
||||
"ElseIfStatementOpeningBrace": 0,
|
||||
"ElseStatement": 0,
|
||||
"ElseStatementClosingBrace": ">=1",
|
||||
"ElseStatementOpeningBrace": 0,
|
||||
"EmptyStatement": -1,
|
||||
"EndOfFile": -1,
|
||||
"FinallyClosingBrace": ">=1",
|
||||
"FinallyKeyword": -1,
|
||||
"FinallyOpeningBrace": 0,
|
||||
"ForInStatement": ">=1",
|
||||
"ForInStatementClosingBrace": ">=1",
|
||||
"ForInStatementExpressionClosing": 0,
|
||||
"ForInStatementExpressionOpening": 0,
|
||||
"ForInStatementOpeningBrace": 0,
|
||||
"ForStatement": ">=1",
|
||||
"ForStatementClosingBrace": ">=1",
|
||||
"ForStatementExpressionClosing": "<2",
|
||||
"ForStatementExpressionOpening": 0,
|
||||
"ForStatementOpeningBrace": 0,
|
||||
"FunctionDeclaration": ">=1",
|
||||
"FunctionDeclarationClosingBrace": ">=1",
|
||||
"FunctionDeclarationOpeningBrace": 0,
|
||||
"FunctionExpression": 0,
|
||||
"FunctionExpressionClosingBrace": 1,
|
||||
"FunctionExpressionOpeningBrace":0,
|
||||
"IIFEClosingParentheses": 0,
|
||||
"IfStatement": ">=1",
|
||||
"IfStatementClosingBrace": ">=1",
|
||||
"IfStatementOpeningBrace": 0,
|
||||
"LogicalExpression": -1,
|
||||
"MemberExpressionClosing": 0,
|
||||
"MemberExpressionOpening": 0,
|
||||
"MemberExpressionPeriod": -1,
|
||||
"MethodDefinition": ">=1",
|
||||
"ObjectExpressionClosingBrace": "<=1",
|
||||
"ObjectPatternClosingBrace": 0,
|
||||
"ObjectPatternComma": 0,
|
||||
"ObjectPatternOpeningBrace": 0,
|
||||
"ParameterDefault": 0,
|
||||
"Property": "<=2",
|
||||
"PropertyValue": 0,
|
||||
"ReturnStatement": -1,
|
||||
"SwitchClosingBrace": ">=1",
|
||||
"SwitchOpeningBrace": 0,
|
||||
"ThisExpression": -1,
|
||||
"ThrowStatement": ">=1",
|
||||
"TryClosingBrace": ">=1",
|
||||
"TryKeyword": -1,
|
||||
"TryOpeningBrace": 0,
|
||||
"VariableDeclaration": ">=1",
|
||||
"VariableDeclarationSemiColon": 0,
|
||||
"VariableDeclarationWithoutInit": ">=1",
|
||||
"VariableName": ">=1",
|
||||
"VariableValue": 0,
|
||||
"WhileStatement": ">=1",
|
||||
"WhileStatementClosingBrace": ">=1",
|
||||
"WhileStatementOpeningBrace": 0
|
||||
},
|
||||
|
||||
"after": {
|
||||
"ArrayPatternClosing": 0,
|
||||
"ArrayPatternComma": 0,
|
||||
"ArrayPatternOpening": 0,
|
||||
"ArrowFunctionExpressionArrow": 0,
|
||||
"ArrowFunctionExpressionClosingBrace": -1,
|
||||
"ArrowFunctionExpressionOpeningBrace": ">=1",
|
||||
"AssignmentExpression": ">=1",
|
||||
"AssignmentOperator": 0,
|
||||
"BlockStatement": 0,
|
||||
"BreakKeyword": -1,
|
||||
"CallExpression": -1,
|
||||
"CallExpressionClosingParentheses": -1,
|
||||
"CallExpressionOpeningParentheses": -1,
|
||||
"CatchClosingBrace": ">=0",
|
||||
"CatchKeyword": 0,
|
||||
"CatchOpeningBrace": ">=1",
|
||||
"ClassDeclaration": ">=1",
|
||||
"ClassDeclarationClosingBrace": ">=1",
|
||||
"ClassDeclarationOpeningBrace": ">=1",
|
||||
"ConditionalExpression": ">=1",
|
||||
"DeleteOperator": ">=1",
|
||||
"DoWhileStatement": ">=1",
|
||||
"DoWhileStatementClosingBrace": 0,
|
||||
"DoWhileStatementOpeningBrace": ">=1",
|
||||
"ElseIfStatement": ">=1",
|
||||
"ElseIfStatementClosingBrace": ">=1",
|
||||
"ElseIfStatementOpeningBrace": ">=1",
|
||||
"ElseStatement": ">=1",
|
||||
"ElseStatementClosingBrace": ">=1",
|
||||
"ElseStatementOpeningBrace": ">=1",
|
||||
"EmptyStatement": -1,
|
||||
"FinallyClosingBrace": ">=1",
|
||||
"FinallyKeyword": -1,
|
||||
"FinallyOpeningBrace": ">=1",
|
||||
"ForInStatement": ">=1",
|
||||
"ForInStatementClosingBrace": ">=1",
|
||||
"ForInStatementExpressionClosing": -1,
|
||||
"ForInStatementExpressionOpening": "<2",
|
||||
"ForInStatementOpeningBrace": ">=1",
|
||||
"ForStatement": ">=1",
|
||||
"ForStatementClosingBrace": ">=1",
|
||||
"ForStatementExpressionClosing": -1,
|
||||
"ForStatementExpressionOpening": "<2",
|
||||
"ForStatementOpeningBrace": ">=1",
|
||||
"FunctionDeclaration": ">=1",
|
||||
"FunctionDeclarationClosingBrace": ">=1",
|
||||
"FunctionDeclarationOpeningBrace": ">=1",
|
||||
"FunctionExpression": 0,
|
||||
"FunctionExpressionClosingBrace": -1,
|
||||
"FunctionExpressionOpeningBrace": 1,
|
||||
"IIFEOpeningParentheses": 0,
|
||||
"IfStatement": ">=1",
|
||||
"IfStatementClosingBrace": ">=1",
|
||||
"IfStatementOpeningBrace": ">=1",
|
||||
"LogicalExpression": -1,
|
||||
"MemberExpressionClosing": 0,
|
||||
"MemberExpressionOpening": 0,
|
||||
"MemberExpressionPeriod": 0,
|
||||
"MethodDefinition": ">=1",
|
||||
"ObjectExpressionOpeningBrace": "<=1",
|
||||
"ObjectPatternClosingBrace": 0,
|
||||
"ObjectPatternComma": 0,
|
||||
"ObjectPatternOpeningBrace": 0,
|
||||
"ParameterDefault": 0,
|
||||
"Property": -1,
|
||||
"PropertyName": 0,
|
||||
"ReturnStatement": -1,
|
||||
"SwitchCaseColon": ">=1",
|
||||
"SwitchClosingBrace": ">=1",
|
||||
"SwitchOpeningBrace": ">=1",
|
||||
"ThisExpression": 0,
|
||||
"ThrowStatement": ">=1",
|
||||
"TryClosingBrace": 0,
|
||||
"TryKeyword": -1,
|
||||
"TryOpeningBrace": ">=1",
|
||||
"VariableDeclaration": ">=1",
|
||||
"VariableDeclarationSemiColon": ">=1",
|
||||
"VariableValue": -1,
|
||||
"WhileStatement": ">=1",
|
||||
"WhileStatementClosingBrace": ">=1",
|
||||
"WhileStatementOpeningBrace": ">=1"
|
||||
}
|
||||
},
|
||||
"whiteSpace": {
|
||||
"value": " ",
|
||||
"removeTrailing": 1,
|
||||
"before": {
|
||||
"ArgumentComma": 0,
|
||||
"ArgumentList": 0,
|
||||
"ArgumentListArrayExpression": 0,
|
||||
"ArgumentListFunctionExpression": 1,
|
||||
"ArgumentListObjectExpression": 0,
|
||||
"ArrayExpressionClosing": 0,
|
||||
"ArrayExpressionComma": 0,
|
||||
"ArrayExpressionOpening": 1,
|
||||
"AssignmentOperator": 1,
|
||||
"BinaryExpression": 0,
|
||||
"BinaryExpressionOperator": 1,
|
||||
"BlockComment": 1,
|
||||
"CallExpression": 1,
|
||||
"CatchClosingBrace": 1,
|
||||
"CatchKeyword": 1,
|
||||
"CatchOpeningBrace": 1,
|
||||
"CatchParameterList": 0,
|
||||
"CommaOperator": 0,
|
||||
"ConditionalExpressionAlternate": 1,
|
||||
"ConditionalExpressionConsequent": 1,
|
||||
"DoWhileStatementClosingBrace": 1,
|
||||
"DoWhileStatementConditional": 1,
|
||||
"DoWhileStatementOpeningBrace": 1,
|
||||
"ElseIfStatementClosingBrace": 1,
|
||||
"ElseIfStatementOpeningBrace": 1,
|
||||
"ElseStatementClosingBrace": 1,
|
||||
"ElseStatementOpeningBrace": 1,
|
||||
"EmptyStatement": 0,
|
||||
"ExpressionClosingParentheses": 0,
|
||||
"FinallyClosingBrace": 1,
|
||||
"FinallyKeyword": -1,
|
||||
"FinallyOpeningBrace": 1,
|
||||
"ForInStatement": 1,
|
||||
"ForInStatementClosingBrace": 1,
|
||||
"ForInStatementExpressionClosing": 0,
|
||||
"ForInStatementExpressionOpening": 1,
|
||||
"ForInStatementOpeningBrace": 1,
|
||||
"ForStatement": 1,
|
||||
"ForStatementClosingBrace": 1,
|
||||
"ForStatementExpressionClosing": 0,
|
||||
"ForStatementExpressionOpening": 1,
|
||||
"ForStatementOpeningBrace": 1,
|
||||
"ForStatementSemicolon": 0,
|
||||
"FunctionDeclarationClosingBrace": 1,
|
||||
"FunctionDeclarationOpeningBrace": 1,
|
||||
"FunctionExpressionClosingBrace": 1,
|
||||
"FunctionExpressionOpeningBrace": 1,
|
||||
"IfStatementClosingBrace": 1,
|
||||
"IfStatementConditionalClosing": 0,
|
||||
"IfStatementConditionalOpening": 1,
|
||||
"IfStatementOpeningBrace": 1,
|
||||
"LineComment": 1,
|
||||
"LogicalExpressionOperator": 1,
|
||||
"MemberExpressionClosing": 0,
|
||||
"ObjectExpressionClosingBrace": 1,
|
||||
"ParameterComma": 0,
|
||||
"ParameterList": 0,
|
||||
"Property": 1,
|
||||
"PropertyName": 1,
|
||||
"PropertyValue": 1,
|
||||
"SwitchDiscriminantClosing": 0,
|
||||
"SwitchDiscriminantOpening": 1,
|
||||
"ThrowKeyword": 1,
|
||||
"TryClosingBrace": 1,
|
||||
"TryKeyword": -1,
|
||||
"TryOpeningBrace": 1,
|
||||
"UnaryExpressionOperator": 0,
|
||||
"VariableName": 1,
|
||||
"VariableValue": 1,
|
||||
"WhileStatementClosingBrace": 1,
|
||||
"WhileStatementConditionalClosing": 0,
|
||||
"WhileStatementConditionalOpening": 1,
|
||||
"WhileStatementOpeningBrace": 1
|
||||
},
|
||||
"after": {
|
||||
"ArgumentComma": 1,
|
||||
"ArgumentList": 0,
|
||||
"ArgumentListArrayExpression": 1,
|
||||
"ArgumentListFunctionExpression": 1,
|
||||
"ArgumentListObjectExpression": 0,
|
||||
"ArrayExpressionClosing": 0,
|
||||
"ArrayExpressionComma": 1,
|
||||
"ArrayExpressionOpening": 0,
|
||||
"AssignmentOperator": 1,
|
||||
"BinaryExpression": 0,
|
||||
"BinaryExpressionOperator": 1,
|
||||
"BlockComment": 1,
|
||||
"CallExpression": 0,
|
||||
"CatchClosingBrace": 1,
|
||||
"CatchKeyword": 1,
|
||||
"CatchOpeningBrace": 1,
|
||||
"CatchParameterList": 0,
|
||||
"CommaOperator": 1,
|
||||
"ConditionalExpressionConsequent": 1,
|
||||
"ConditionalExpressionTest": 1,
|
||||
"DoWhileStatementBody": 1,
|
||||
"DoWhileStatementClosingBrace": 1,
|
||||
"DoWhileStatementOpeningBrace": 1,
|
||||
"ElseIfStatementClosingBrace": 1,
|
||||
"ElseIfStatementOpeningBrace": 1,
|
||||
"ElseStatementClosingBrace": 1,
|
||||
"ElseStatementOpeningBrace": 1,
|
||||
"EmptyStatement": 0,
|
||||
"ExpressionOpeningParentheses": 0,
|
||||
"FinallyClosingBrace": 1,
|
||||
"FinallyKeyword": -1,
|
||||
"FinallyOpeningBrace": 1,
|
||||
"ForInStatement": 1,
|
||||
"ForInStatementClosingBrace": 1,
|
||||
"ForInStatementExpressionClosing": 1,
|
||||
"ForInStatementExpressionOpening": 0,
|
||||
"ForInStatementOpeningBrace": 1,
|
||||
"ForStatement": 1,
|
||||
"ForStatementClosingBrace": 1,
|
||||
"ForStatementExpressionClosing": 1,
|
||||
"ForStatementExpressionOpening": 0,
|
||||
"ForStatementOpeningBrace": 1,
|
||||
"ForStatementSemicolon": 1,
|
||||
"FunctionDeclarationClosingBrace": 0,
|
||||
"FunctionDeclarationOpeningBrace": 0,
|
||||
"FunctionExpressionClosingBrace": 0,
|
||||
"FunctionExpressionOpeningBrace": 0,
|
||||
"FunctionName": 0,
|
||||
"FunctionReservedWord": 0,
|
||||
"IfStatementClosingBrace": 1,
|
||||
"IfStatementConditionalClosing": 0,
|
||||
"IfStatementConditionalOpening": 0,
|
||||
"IfStatementOpeningBrace": 1,
|
||||
"LogicalExpressionOperator": 1,
|
||||
"MemberExpressionOpening": 0,
|
||||
"ObjectExpressionClosingBrace": 0,
|
||||
"ObjectExpressionOpeningBrace": 1,
|
||||
"ParameterComma": 1,
|
||||
"ParameterList": 0,
|
||||
"PropertyName": 0,
|
||||
"PropertyValue": 0,
|
||||
"SwitchDiscriminantClosing": 1,
|
||||
"SwitchDiscriminantOpening": 0,
|
||||
"ThrowKeyword": 1,
|
||||
"TryClosingBrace": 1,
|
||||
"TryKeyword": -1,
|
||||
"TryOpeningBrace": 1,
|
||||
"UnaryExpressionOperator": 0,
|
||||
"VariableName": 1,
|
||||
"WhileStatementClosingBrace": 1,
|
||||
"WhileStatementConditionalClosing": 1,
|
||||
"WhileStatementConditionalOpening": 0,
|
||||
"WhileStatementOpeningBrace": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,8 @@ const dirs = fs
|
||||
.join('|');
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
|
||||
parser: '@babel/eslint-parser',
|
||||
|
||||
env: {
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"js": {
|
||||
"indent_size": 2,
|
||||
"indent_char": " ",
|
||||
"indent_level": 2,
|
||||
"indent_with_tabs": false,
|
||||
"preserve_newlines": true,
|
||||
"brace_style": "collapse",
|
||||
"max_preserve_newlines": 2,
|
||||
"jslint_happy": true
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"plugins": [
|
||||
"stylelint-order"
|
||||
],
|
||||
"ignoreFiles": [
|
||||
"frontend/src/Styles/scaffolding.css",
|
||||
"**/*.js"
|
||||
],
|
||||
"rules": {
|
||||
"plugins": [
|
||||
"stylelint-order"
|
||||
],
|
||||
"ignoreFiles": [
|
||||
"frontend/src/Styles/scaffolding.css",
|
||||
"**/*.js"
|
||||
],
|
||||
"rules": {
|
||||
"at-rule-empty-line-before": [
|
||||
"always",
|
||||
{
|
||||
@@ -15,9 +15,6 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"at-rule-name-case": "lower",
|
||||
"at-rule-name-newline-after": "always-multi-line",
|
||||
"at-rule-name-space-after": "always",
|
||||
"at-rule-no-unknown": [
|
||||
true,
|
||||
{
|
||||
@@ -28,83 +25,36 @@
|
||||
}
|
||||
],
|
||||
"at-rule-no-vendor-prefix": true,
|
||||
"at-rule-semicolon-newline-after": "always",
|
||||
"at-rule-semicolon-space-before": "never",
|
||||
"block-closing-brace-empty-line-before": "never",
|
||||
"block-closing-brace-newline-after": "always",
|
||||
"block-closing-brace-newline-before": "always",
|
||||
"block-closing-brace-space-after": "always-single-line",
|
||||
"block-closing-brace-space-before": "always-single-line",
|
||||
"block-no-empty": true,
|
||||
"block-opening-brace-newline-after": "always",
|
||||
"block-opening-brace-newline-before": "never-single-line",
|
||||
"block-opening-brace-space-after": "always-single-line",
|
||||
"block-opening-brace-space-before": "always",
|
||||
"color-hex-case": "lower",
|
||||
"color-hex-length": "short",
|
||||
"color-named": "never",
|
||||
"color-no-invalid-hex": true,
|
||||
"comment-whitespace-inside": "always",
|
||||
"declaration-bang-space-after": "never",
|
||||
"declaration-bang-space-before": "always",
|
||||
"declaration-block-no-duplicate-properties": [
|
||||
true,
|
||||
{
|
||||
"ignoreProperties": [
|
||||
"composes"
|
||||
"composes"
|
||||
]
|
||||
}
|
||||
],
|
||||
"declaration-block-no-redundant-longhand-properties": true,
|
||||
"declaration-block-no-shorthand-property-overrides": true,
|
||||
"declaration-block-semicolon-newline-after": "always",
|
||||
"declaration-block-semicolon-newline-before": "never-multi-line",
|
||||
"declaration-block-semicolon-space-before": "never",
|
||||
"declaration-block-single-line-max-declarations": 1,
|
||||
"declaration-block-trailing-semicolon": "always",
|
||||
"declaration-colon-space-after": "always",
|
||||
"declaration-colon-space-before": "never",
|
||||
"font-family-name-quotes": "always-unless-keyword",
|
||||
"function-calc-no-unspaced-operator": true,
|
||||
"function-comma-newline-after": "never-multi-line",
|
||||
"function-comma-newline-before": "never-multi-line",
|
||||
"function-comma-space-after": "always",
|
||||
"function-comma-space-before": "never",
|
||||
"function-linear-gradient-no-nonstandard-direction": true,
|
||||
"function-name-case": "lower",
|
||||
"function-parentheses-newline-inside": "never-multi-line",
|
||||
"function-parentheses-space-inside": "never",
|
||||
"function-url-quotes": "always",
|
||||
"function-url-scheme-disallowed-list": [
|
||||
"data"
|
||||
],
|
||||
"function-whitespace-after": "always",
|
||||
"indentation": 2,
|
||||
"keyframe-declaration-no-important": true,
|
||||
"length-zero-no-unit": true,
|
||||
"max-empty-lines": 1,
|
||||
"max-line-length": [
|
||||
100,
|
||||
{
|
||||
"ignore": [
|
||||
"non-comments"
|
||||
]
|
||||
}
|
||||
],
|
||||
"max-nesting-depth": 2,
|
||||
"media-feature-colon-space-after": "always",
|
||||
"media-feature-colon-space-before": "never",
|
||||
"media-feature-name-case": "lower",
|
||||
"media-feature-name-no-vendor-prefix": true,
|
||||
"media-feature-range-operator-space-after": "always",
|
||||
"media-feature-range-operator-space-before": "always",
|
||||
"no-empty-source": true,
|
||||
"no-eol-whitespace": true,
|
||||
"no-extra-semicolons": true,
|
||||
"no-invalid-double-slash-comments": true,
|
||||
"no-missing-end-of-source-newline": true,
|
||||
"number-leading-zero": "always",
|
||||
"number-no-trailing-zeros": true,
|
||||
"order/order": [
|
||||
"custom-properties",
|
||||
"dollar-variables",
|
||||
@@ -132,6 +82,7 @@
|
||||
"right",
|
||||
"bottom",
|
||||
"left",
|
||||
"inset",
|
||||
"z-index",
|
||||
"display",
|
||||
"visibility",
|
||||
@@ -343,54 +294,33 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"property-case": "lower",
|
||||
"property-no-vendor-prefix": true,
|
||||
"rule-empty-line-before": [
|
||||
"always",
|
||||
{
|
||||
"except": [
|
||||
"first-nested"
|
||||
"first-nested"
|
||||
],
|
||||
"ignore": [
|
||||
"after-comment"
|
||||
"after-comment"
|
||||
]
|
||||
}
|
||||
],
|
||||
"selector-attribute-brackets-space-inside": "never",
|
||||
"selector-attribute-operator-space-after": "never",
|
||||
"selector-attribute-operator-space-before": "never",
|
||||
"selector-attribute-quotes": "never",
|
||||
"selector-class-pattern": "^[A-Za-z0-9]+$",
|
||||
"selector-combinator-space-after": "always",
|
||||
"selector-combinator-space-before": "always",
|
||||
"selector-descendant-combinator-no-non-space": true,
|
||||
"selector-list-comma-newline-after": "always",
|
||||
"selector-list-comma-newline-before": "never-multi-line",
|
||||
"selector-list-comma-space-before": "never",
|
||||
"selector-max-attribute": 0,
|
||||
"selector-max-class": 3,
|
||||
"selector-max-compound-selectors": 3,
|
||||
"selector-max-empty-lines": 0,
|
||||
"selector-max-id": 0,
|
||||
"selector-max-universal": 0,
|
||||
"selector-pseudo-class-case": "lower",
|
||||
"selector-pseudo-class-parentheses-space-inside": "never",
|
||||
"selector-pseudo-element-case": "lower",
|
||||
"selector-pseudo-element-colon-notation": "double",
|
||||
"selector-pseudo-element-no-unknown": true,
|
||||
"selector-type-case": "lower",
|
||||
"selector-type-no-unknown": true,
|
||||
"shorthand-property-no-redundant-values": true,
|
||||
"string-no-newline": true,
|
||||
"string-quotes": "single",
|
||||
"time-min-milliseconds": 100,
|
||||
"unit-case": "lower",
|
||||
"unit-no-unknown": true,
|
||||
"value-list-comma-newline-after": "never-multi-line",
|
||||
"value-list-comma-newline-before": "never-multi-line",
|
||||
"value-list-comma-space-after": "always",
|
||||
"value-list-comma-space-before": "never",
|
||||
"value-list-max-empty-lines": 0,
|
||||
"value-no-vendor-prefix": true
|
||||
}
|
||||
}
|
||||
}
|
||||
7
frontend/.vscode/extensions.json
vendored
Normal file
7
frontend/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"stylelint.vscode-stylelint",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode"
|
||||
]
|
||||
}
|
||||
@@ -50,7 +50,7 @@ module.exports = (env) => {
|
||||
'node_modules'
|
||||
],
|
||||
alias: {
|
||||
jquery: 'jquery/src/jquery'
|
||||
jquery: 'jquery/dist/jquery.min'
|
||||
},
|
||||
fallback: {
|
||||
buffer: false,
|
||||
@@ -251,18 +251,19 @@ module.exports = (env) => {
|
||||
config.resolve.alias['react-dom$'] = 'react-dom/profiling';
|
||||
config.resolve.alias['scheduler/tracing'] = 'scheduler/tracing-profiling';
|
||||
|
||||
config.optimization.minimizer = [
|
||||
new TerserPlugin({
|
||||
cache: true,
|
||||
parallel: true,
|
||||
sourceMap: true, // Must be set to true if using source-maps in production
|
||||
terserOptions: {
|
||||
mangle: false,
|
||||
keep_classnames: true,
|
||||
keep_fnames: true
|
||||
}
|
||||
})
|
||||
];
|
||||
config.optimization = {
|
||||
minimize: true,
|
||||
minimizer: [
|
||||
new TerserPlugin({
|
||||
terserOptions: {
|
||||
sourceMap: true, // Must be set to true if using source-maps in production
|
||||
mangle: false,
|
||||
keep_classnames: true,
|
||||
keep_fnames: true
|
||||
}
|
||||
})
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
return config;
|
||||
|
||||
@@ -17,7 +17,7 @@ class DescriptionListItem extends Component {
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<span>
|
||||
<div>
|
||||
<DescriptionListItemTitle
|
||||
className={titleClassName}
|
||||
>
|
||||
@@ -29,7 +29,7 @@ class DescriptionListItem extends Component {
|
||||
>
|
||||
{data}
|
||||
</DescriptionListItemDescription>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import Alert from 'Components/Alert';
|
||||
import PathInput from 'Components/Form/PathInput';
|
||||
import Button from 'Components/Link/Button';
|
||||
@@ -39,7 +38,7 @@ class FileBrowserModalContent extends Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this._scrollerNode = null;
|
||||
this._scrollerRef = React.createRef();
|
||||
|
||||
this.state = {
|
||||
isFileBrowserModalOpen: false,
|
||||
@@ -57,21 +56,10 @@ class FileBrowserModalContent extends Component {
|
||||
currentPath !== prevState.currentPath
|
||||
) {
|
||||
this.setState({ currentPath });
|
||||
this._scrollerNode.scrollTop = 0;
|
||||
this._scrollerRef.current.scrollTop = 0;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
setScrollerRef = (ref) => {
|
||||
if (ref) {
|
||||
this._scrollerNode = ReactDOM.findDOMNode(ref);
|
||||
} else {
|
||||
this._scrollerNode = null;
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
@@ -145,7 +133,7 @@ class FileBrowserModalContent extends Component {
|
||||
/>
|
||||
|
||||
<Scroller
|
||||
ref={this.setScrollerRef}
|
||||
ref={this._scrollerRef}
|
||||
className={styles.scroller}
|
||||
scrollDirection={scrollDirections.BOTH}
|
||||
>
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
.inputContainer {
|
||||
top: -1px;
|
||||
right: -1px;
|
||||
bottom: -1px;
|
||||
left: -1px;
|
||||
inset: -1px;
|
||||
display: flex;
|
||||
align-items: start;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -112,6 +112,12 @@ class TextInput extends Component {
|
||||
this._isMouseTarget = false;
|
||||
};
|
||||
|
||||
onWheel = () => {
|
||||
if (this.props.type === 'number') {
|
||||
this._input.blur();
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -161,6 +167,7 @@ class TextInput extends Component {
|
||||
onKeyUp={this.onKeyUp}
|
||||
onMouseDown={this.onMouseDown}
|
||||
onMouseUp={this.onMouseUp}
|
||||
onWheel={this.onWheel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ function IconButton(props) {
|
||||
className,
|
||||
isDisabled && styles.isDisabled
|
||||
)}
|
||||
aria-label="Table Options Button"
|
||||
isDisabled={isDisabled}
|
||||
{...otherProps}
|
||||
>
|
||||
|
||||
@@ -13,7 +13,7 @@ const messages = [
|
||||
'Loading humorous message... Please Wait',
|
||||
'I could\'ve been faster in Python',
|
||||
'Don\'t forget to rewind your tracks',
|
||||
'Congratulations! you are the 1000th visitor.',
|
||||
'Congratulations! You are the 1000th visitor.',
|
||||
'HELP! I\'m being held hostage and forced to write these stupid lines!',
|
||||
'RE-calibrating the internet...',
|
||||
'I\'ll be here all week',
|
||||
|
||||
@@ -56,6 +56,7 @@ class PageHeader extends Component {
|
||||
<img
|
||||
className={styles.logo}
|
||||
src={`${window.Prowlarr.urlBase}/Content/Images/logo.png`}
|
||||
alt="Prowlarr Logo"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
@@ -74,6 +75,7 @@ class PageHeader extends Component {
|
||||
<IconButton
|
||||
className={styles.donate}
|
||||
name={icons.HEART}
|
||||
aria-label="Donate"
|
||||
to="https://prowlarr.com/donate"
|
||||
size={14}
|
||||
/>
|
||||
|
||||
@@ -21,7 +21,7 @@ function PageHeaderActionsMenu(props) {
|
||||
return (
|
||||
<div>
|
||||
<Menu alignMenu={align.RIGHT}>
|
||||
<MenuButton className={styles.menuButton}>
|
||||
<MenuButton className={styles.menuButton} aria-label="Menu Button">
|
||||
<Icon
|
||||
name={icons.INTERACTIVE}
|
||||
/>
|
||||
|
||||
@@ -56,7 +56,9 @@ function ProgressBar(props) {
|
||||
styles[kind],
|
||||
enableColorImpairedMode && 'colorImpaired'
|
||||
)}
|
||||
aria-valuenow={progress}
|
||||
role="meter"
|
||||
aria-label={`Progress Bar at ${progress.toFixed(0)}%`}
|
||||
aria-valuenow={progress.toFixed(0)}
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
style={{ width: progressPercent }}
|
||||
|
||||
@@ -54,7 +54,7 @@ function Logger(minimumLogLevel) {
|
||||
}
|
||||
|
||||
Logger.prototype.cleanse = function(message) {
|
||||
const apikey = new RegExp(`access_token=${window.Prowlarr.apiKey}`, 'g');
|
||||
const apikey = new RegExp(`access_token=${encodeURIComponent(window.Prowlarr.apiKey)}`, 'g');
|
||||
return message.replace(apikey, 'access_token=(removed)');
|
||||
};
|
||||
|
||||
@@ -98,7 +98,7 @@ class SignalRConnector extends Component {
|
||||
|
||||
this.connection = new signalR.HubConnectionBuilder()
|
||||
.configureLogging(new Logger(signalR.LogLevel.Information))
|
||||
.withUrl(`${url}?access_token=${window.Prowlarr.apiKey}`)
|
||||
.withUrl(`${url}?access_token=${encodeURIComponent(window.Prowlarr.apiKey)}`)
|
||||
.withAutomaticReconnect({
|
||||
nextRetryDelayInMilliseconds: (retryContext) => {
|
||||
if (retryContext.elapsedMilliseconds > 180000) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import CapabilitiesLabel from 'Indexer/Index/Table/CapabilitiesLabel';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import HistoryDetailsModal from './Details/HistoryDetailsModal';
|
||||
import HistoryEventTypeCell from './HistoryEventTypeCell';
|
||||
import HistoryRowParameter from './HistoryRowParameter';
|
||||
@@ -193,7 +194,7 @@ class HistoryRow extends Component {
|
||||
{
|
||||
data.season ?
|
||||
<HistoryRowParameter
|
||||
title='Season'
|
||||
title={translate('Season')}
|
||||
value={data.season}
|
||||
/> :
|
||||
null
|
||||
@@ -202,7 +203,7 @@ class HistoryRow extends Component {
|
||||
{
|
||||
data.episode ?
|
||||
<HistoryRowParameter
|
||||
title='Episode'
|
||||
title={translate('Episode')}
|
||||
value={data.episode}
|
||||
/> :
|
||||
null
|
||||
@@ -211,7 +212,7 @@ class HistoryRow extends Component {
|
||||
{
|
||||
data.artist ?
|
||||
<HistoryRowParameter
|
||||
title='Artist'
|
||||
title={translate('Artist')}
|
||||
value={data.artist}
|
||||
/> :
|
||||
null
|
||||
@@ -220,7 +221,7 @@ class HistoryRow extends Component {
|
||||
{
|
||||
data.album ?
|
||||
<HistoryRowParameter
|
||||
title='Album'
|
||||
title={translate('Album')}
|
||||
value={data.album}
|
||||
/> :
|
||||
null
|
||||
@@ -229,7 +230,7 @@ class HistoryRow extends Component {
|
||||
{
|
||||
data.label ?
|
||||
<HistoryRowParameter
|
||||
title='Label'
|
||||
title={translate('Label')}
|
||||
value={data.label}
|
||||
/> :
|
||||
null
|
||||
@@ -238,7 +239,7 @@ class HistoryRow extends Component {
|
||||
{
|
||||
data.track ?
|
||||
<HistoryRowParameter
|
||||
title='Track'
|
||||
title={translate('Track')}
|
||||
value={data.track}
|
||||
/> :
|
||||
null
|
||||
@@ -247,7 +248,7 @@ class HistoryRow extends Component {
|
||||
{
|
||||
data.year ?
|
||||
<HistoryRowParameter
|
||||
title='Year'
|
||||
title={translate('Year')}
|
||||
value={data.year}
|
||||
/> :
|
||||
null
|
||||
@@ -256,7 +257,7 @@ class HistoryRow extends Component {
|
||||
{
|
||||
data.genre ?
|
||||
<HistoryRowParameter
|
||||
title='Genre'
|
||||
title={translate('Genre')}
|
||||
value={data.genre}
|
||||
/> :
|
||||
null
|
||||
@@ -265,7 +266,7 @@ class HistoryRow extends Component {
|
||||
{
|
||||
data.author ?
|
||||
<HistoryRowParameter
|
||||
title='Author'
|
||||
title={translate('Author')}
|
||||
value={data.author}
|
||||
/> :
|
||||
null
|
||||
@@ -274,7 +275,7 @@ class HistoryRow extends Component {
|
||||
{
|
||||
data.bookTitle ?
|
||||
<HistoryRowParameter
|
||||
title='Book'
|
||||
title={translate('Book')}
|
||||
value={data.bookTitle}
|
||||
/> :
|
||||
null
|
||||
@@ -283,7 +284,7 @@ class HistoryRow extends Component {
|
||||
{
|
||||
data.publisher ?
|
||||
<HistoryRowParameter
|
||||
title='Publisher'
|
||||
title={translate('Publisher')}
|
||||
value={data.publisher}
|
||||
/> :
|
||||
null
|
||||
@@ -351,6 +352,11 @@ class HistoryRow extends Component {
|
||||
`${data.elapsedTime}ms` :
|
||||
null
|
||||
}
|
||||
{
|
||||
data.cached === '1' ?
|
||||
' (cached)' :
|
||||
null
|
||||
}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
@@ -376,14 +382,14 @@ class HistoryRow extends Component {
|
||||
<IconButton
|
||||
name={icons.SEARCH}
|
||||
onPress={this.onSearchPress}
|
||||
title='Repeat Search'
|
||||
title={translate('RepeatSearch')}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
<IconButton
|
||||
name={icons.INFO}
|
||||
onPress={this.onDetailsPress}
|
||||
title='History Details'
|
||||
title={translate('HistoryDetails')}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
|
||||
@@ -222,7 +222,11 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<IndexerIndexSelectModeButton
|
||||
label={isSelectMode ? 'Stop Selecting' : 'Select Indexer'}
|
||||
label={
|
||||
isSelectMode
|
||||
? translate('StopSelecting')
|
||||
: translate('SelectIndexer')
|
||||
}
|
||||
iconName={isSelectMode ? icons.SERIES_ENDED : icons.CHECK}
|
||||
isSelectMode={isSelectMode}
|
||||
overflowComponent={IndexerIndexSelectModeMenuItem}
|
||||
@@ -230,7 +234,7 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
|
||||
/>
|
||||
|
||||
<IndexerIndexSelectAllButton
|
||||
label="SelectAll"
|
||||
label={translate('SelectAll')}
|
||||
isSelectMode={isSelectMode}
|
||||
overflowComponent={IndexerIndexSelectAllMenuItem}
|
||||
/>
|
||||
@@ -245,7 +249,10 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
|
||||
optionsComponent={IndexerIndexTableOptions}
|
||||
onTableOptionChange={onTableOptionChange}
|
||||
>
|
||||
<PageToolbarButton label="Options" iconName={icons.TABLE} />
|
||||
<PageToolbarButton
|
||||
label={translate('Options')}
|
||||
iconName={icons.TABLE}
|
||||
/>
|
||||
</TableOptionsModalWrapper>
|
||||
|
||||
<PageToolbarSeparator />
|
||||
@@ -276,7 +283,9 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
|
||||
>
|
||||
{isFetching && !isPopulated ? <LoadingIndicator /> : null}
|
||||
|
||||
{!isFetching && !!error ? <div>Unable to load indexers</div> : null}
|
||||
{!isFetching && !!error ? (
|
||||
<div>{translate('UnableToLoadIndexers')}</div>
|
||||
) : null}
|
||||
|
||||
{isLoaded ? (
|
||||
<div className={styles.contentBodyContainer}>
|
||||
@@ -295,7 +304,10 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
|
||||
) : null}
|
||||
|
||||
{!error && isPopulated && !items.length ? (
|
||||
<NoIndexer totalItems={totalItems} />
|
||||
<NoIndexer
|
||||
totalItems={totalItems}
|
||||
onAddIndexerPress={onAddIndexerPress}
|
||||
/>
|
||||
) : null}
|
||||
</PageContentBody>
|
||||
{isLoaded && !!jumpBarItems.order.length ? (
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useCallback } from 'react';
|
||||
import { SelectActionType, useSelect } from 'App/SelectContext';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
interface IndexerIndexSelectAllButtonProps {
|
||||
label: string;
|
||||
@@ -32,7 +33,7 @@ function IndexerIndexSelectAllButton(props: IndexerIndexSelectAllButtonProps) {
|
||||
|
||||
return isSelectMode ? (
|
||||
<PageToolbarButton
|
||||
label={allSelected ? 'Unselect All' : 'Select All'}
|
||||
label={allSelected ? translate('UnselectAll') : translate('SelectAll')}
|
||||
iconName={icon}
|
||||
onPress={onPress}
|
||||
/>
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useCallback } from 'react';
|
||||
import { SelectActionType, useSelect } from 'App/SelectContext';
|
||||
import PageToolbarOverflowMenuItem from 'Components/Page/Toolbar/PageToolbarOverflowMenuItem';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
interface IndexerIndexSelectAllMenuItemProps {
|
||||
label: string;
|
||||
@@ -33,7 +34,7 @@ function IndexerIndexSelectAllMenuItem(
|
||||
|
||||
return isSelectMode ? (
|
||||
<PageToolbarOverflowMenuItem
|
||||
label={allSelected ? 'Unselect All' : 'Select All'}
|
||||
label={allSelected ? translate('UnselectAll') : translate('SelectAll')}
|
||||
iconName={iconName}
|
||||
onPress={onPressWrapper}
|
||||
/>
|
||||
|
||||
@@ -14,6 +14,7 @@ import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||
import createAllIndexersSelector from 'Store/Selectors/createAllIndexersSelector';
|
||||
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './TagsModalContent.css';
|
||||
|
||||
interface TagsModalContentProps {
|
||||
@@ -70,7 +71,7 @@ function TagsModalContent(props: TagsModalContentProps) {
|
||||
<ModalBody>
|
||||
<Form>
|
||||
<FormGroup>
|
||||
<FormLabel>Tags</FormLabel>
|
||||
<FormLabel>{translate('Tags')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TAG}
|
||||
@@ -81,7 +82,7 @@ function TagsModalContent(props: TagsModalContentProps) {
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Apply Tags</FormLabel>
|
||||
<FormLabel>{translate('ApplyTags')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
@@ -89,17 +90,17 @@ function TagsModalContent(props: TagsModalContentProps) {
|
||||
value={applyTags}
|
||||
values={applyTagsOptions}
|
||||
helpTexts={[
|
||||
'How to apply tags to the selected series',
|
||||
'Add: Add the tags the existing list of tags',
|
||||
'Remove: Remove the entered tags',
|
||||
'Replace: Replace the tags with the entered tags (enter no tags to clear all tags)',
|
||||
translate('ApplyTagsHelpTexts1'),
|
||||
translate('ApplyTagsHelpTexts2'),
|
||||
translate('ApplyTagsHelpTexts3'),
|
||||
translate('ApplyTagsHelpTexts4'),
|
||||
]}
|
||||
onChange={onApplyTagsChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Result</FormLabel>
|
||||
<FormLabel>{translate('Result')}</FormLabel>
|
||||
|
||||
<div className={styles.result}>
|
||||
{indexerTags.map((id) => {
|
||||
@@ -116,7 +117,11 @@ function TagsModalContent(props: TagsModalContentProps) {
|
||||
return (
|
||||
<Label
|
||||
key={tag.id}
|
||||
title={removeTag ? 'Removing tag' : 'Existing tag'}
|
||||
title={
|
||||
removeTag
|
||||
? translate('RemoveTagRemovingTag')
|
||||
: translate('RemoveTagExistingTag')
|
||||
}
|
||||
kind={removeTag ? kinds.INVERSE : kinds.INFO}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
@@ -140,7 +145,7 @@ function TagsModalContent(props: TagsModalContentProps) {
|
||||
return (
|
||||
<Label
|
||||
key={tag.id}
|
||||
title={'Adding tag'}
|
||||
title={translate('AddingTag')}
|
||||
kind={kinds.SUCCESS}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
|
||||
@@ -55,7 +55,11 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
|
||||
const vipExpiration =
|
||||
fields.find((field) => field.name === 'vipExpiration')?.value ?? '';
|
||||
|
||||
const rssUrl = `${window.location.origin}${window.Prowlarr.urlBase}/${id}/api?t=search&extended=1&apikey=${window.Prowlarr.apiKey}`;
|
||||
const rssUrl = `${window.location.origin}${
|
||||
window.Prowlarr.urlBase
|
||||
}/${id}/api?apikey=${encodeURIComponent(
|
||||
window.Prowlarr.apiKey
|
||||
)}&extended=1&t=search`;
|
||||
|
||||
const [isEditIndexerModalOpen, setIsEditIndexerModalOpen] = useState(false);
|
||||
const [isDeleteIndexerModalOpen, setIsDeleteIndexerModalOpen] =
|
||||
|
||||
@@ -4,6 +4,7 @@ import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import selectTableOptions from './selectTableOptions';
|
||||
|
||||
interface IndexerIndexTableOptionsProps {
|
||||
@@ -32,13 +33,13 @@ function IndexerIndexTableOptions(props: IndexerIndexTableOptionsProps) {
|
||||
return (
|
||||
<Fragment>
|
||||
<FormGroup>
|
||||
<FormLabel>Show Search</FormLabel>
|
||||
<FormLabel>{translate('ShowSearch')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showSearchAction"
|
||||
value={showSearchAction}
|
||||
helpText="Show search button on hover"
|
||||
helpText={translate('ShowSearchHelpText')}
|
||||
onChange={onTableOptionChangeWrapper}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
@@ -33,8 +33,8 @@ function IndexerStatusCell(props: IndexerStatusCellProps) {
|
||||
const enableKind = redirect ? kinds.INFO : kinds.SUCCESS;
|
||||
const enableIcon = redirect ? icons.REDIRECT : icons.CHECK;
|
||||
const enableTitle = redirect
|
||||
? 'Indexer is Enabled, Redirect is Enabled'
|
||||
: 'Indexer is Enabled';
|
||||
? translate('EnabledRedirected')
|
||||
: translate('Enabled');
|
||||
|
||||
return (
|
||||
<Component className={className} {...otherProps}>
|
||||
@@ -43,7 +43,7 @@ function IndexerStatusCell(props: IndexerStatusCellProps) {
|
||||
className={styles.statusIcon}
|
||||
kind={enabled ? enableKind : kinds.DEFAULT}
|
||||
name={enabled ? enableIcon : icons.BLOCKLIST}
|
||||
title={enabled ? enableTitle : 'Indexer is Disabled'}
|
||||
title={enabled ? enableTitle : translate('EnabledIndexerIsDisabled')}
|
||||
/>
|
||||
}
|
||||
{status ? (
|
||||
|
||||
@@ -10,6 +10,7 @@ import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import { align, kinds } from 'Helpers/Props';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import StatsFilterMenu from './StatsFilterMenu';
|
||||
import styles from './Stats.css';
|
||||
|
||||
@@ -188,53 +189,53 @@ function Stats(props) {
|
||||
<div className={styles.fullWidthChart}>
|
||||
<BarChart
|
||||
data={getAverageResponseTimeData(item.indexers)}
|
||||
title='Average Response Times (ms)'
|
||||
title={translate('AverageResponseTimesMs')}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.fullWidthChart}>
|
||||
<BarChart
|
||||
data={getFailureRateData(item.indexers)}
|
||||
title='Indexer Failure Rate'
|
||||
title={translate('IndexerFailureRate')}
|
||||
kind={kinds.WARNING}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.halfWidthChart}>
|
||||
<StackedBarChart
|
||||
data={getTotalRequestsData(item.indexers)}
|
||||
title='Total Indexer Queries'
|
||||
title={translate('TotalIndexerQueries')}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.halfWidthChart}>
|
||||
<BarChart
|
||||
data={getNumberGrabsData(item.indexers)}
|
||||
title='Total Indexer Successful Grabs'
|
||||
title={translate('TotalIndexerSuccessfulGrabs')}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.halfWidthChart}>
|
||||
<BarChart
|
||||
data={getUserAgentQueryData(item.userAgents)}
|
||||
title='Total User Agent Queries'
|
||||
title={translate('TotalUserAgentQueries')}
|
||||
horizontal={true}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.halfWidthChart}>
|
||||
<BarChart
|
||||
data={getUserAgentGrabsData(item.userAgents)}
|
||||
title='Total User Agent Grabs'
|
||||
title={translate('TotalUserAgentGrabs')}
|
||||
horizontal={true}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.halfWidthChart}>
|
||||
<DoughnutChart
|
||||
data={getHostQueryData(item.hosts)}
|
||||
title='Total Host Queries'
|
||||
title={translate('TotalHostQueries')}
|
||||
horizontal={true}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.halfWidthChart}>
|
||||
<DoughnutChart
|
||||
data={getHostGrabsData(item.hosts)}
|
||||
title='Total Host Grabs'
|
||||
title={translate('TotalHostGrabs')}
|
||||
horizontal={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ import React, { Component } from 'react';
|
||||
import Card from 'Components/Card';
|
||||
import Label from 'Components/Label';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import TagList from 'Components/TagList';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EditApplicationModalConnector from './EditApplicationModalConnector';
|
||||
@@ -55,7 +56,9 @@ class Application extends Component {
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
syncLevel
|
||||
syncLevel,
|
||||
tags,
|
||||
tagList
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
@@ -92,6 +95,11 @@ class Application extends Component {
|
||||
</Label>
|
||||
}
|
||||
|
||||
<TagList
|
||||
tags={tags}
|
||||
tagList={tagList}
|
||||
/>
|
||||
|
||||
<EditApplicationModalConnector
|
||||
id={id}
|
||||
isOpen={this.state.isEditApplicationModalOpen}
|
||||
@@ -117,6 +125,8 @@ Application.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
syncLevel: PropTypes.string.isRequired,
|
||||
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onConfirmDeleteApplication: PropTypes.func
|
||||
};
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ class Applications extends Component {
|
||||
render() {
|
||||
const {
|
||||
items,
|
||||
tagList,
|
||||
onConfirmDeleteApplication,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
@@ -71,6 +72,7 @@ class Applications extends Component {
|
||||
<Application
|
||||
key={item.id}
|
||||
{...item}
|
||||
tagList={tagList}
|
||||
onConfirmDeleteApplication={onConfirmDeleteApplication}
|
||||
/>
|
||||
);
|
||||
@@ -109,6 +111,7 @@ Applications.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onConfirmDeleteApplication: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -4,13 +4,20 @@ import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { deleteApplication, fetchApplications } from 'Store/Actions/settingsActions';
|
||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import Applications from './Applications';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createSortedSectionSelector('settings.applications', sortByName),
|
||||
(applications) => applications
|
||||
createTagsSelector(),
|
||||
(applications, tagList) => {
|
||||
return {
|
||||
...applications,
|
||||
tagList
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ function HostSettings(props) {
|
||||
port,
|
||||
urlBase,
|
||||
instanceName,
|
||||
applicationUrl,
|
||||
enableSsl,
|
||||
sslPort,
|
||||
sslCertPath,
|
||||
@@ -89,6 +90,21 @@ function HostSettings(props) {
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>{translate('ApplicationURL')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="applicationUrl"
|
||||
helpText={translate('ApplicationUrlHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...applicationUrl}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
|
||||
@@ -3,6 +3,7 @@ import React, { Component } from 'react';
|
||||
import Card from 'Components/Card';
|
||||
import Label from 'Components/Label';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import TagList from 'Components/TagList';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EditNotificationModalConnector from './EditNotificationModalConnector';
|
||||
@@ -57,10 +58,14 @@ class Notification extends Component {
|
||||
name,
|
||||
onGrab,
|
||||
onHealthIssue,
|
||||
onHealthRestored,
|
||||
onApplicationUpdate,
|
||||
supportsOnGrab,
|
||||
supportsOnHealthIssue,
|
||||
supportsOnApplicationUpdate
|
||||
supportsOnHealthRestored,
|
||||
supportsOnApplicationUpdate,
|
||||
tags,
|
||||
tagList
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
@@ -74,17 +79,27 @@ class Notification extends Component {
|
||||
</div>
|
||||
|
||||
{
|
||||
supportsOnGrab && onGrab &&
|
||||
supportsOnGrab && onGrab ?
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
{translate('OnGrab')}
|
||||
</Label>
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
supportsOnHealthIssue && onHealthIssue &&
|
||||
supportsOnHealthIssue && onHealthIssue ?
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
{translate('OnHealthIssue')}
|
||||
</Label>
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
supportsOnHealthRestored && onHealthRestored ?
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
{translate('OnHealthRestored')}
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
@@ -96,7 +111,7 @@ class Notification extends Component {
|
||||
}
|
||||
|
||||
{
|
||||
!onGrab && !onHealthIssue && !onApplicationUpdate ?
|
||||
!onGrab && !onHealthIssue && !onHealthRestored && !onApplicationUpdate ?
|
||||
<Label
|
||||
kind={kinds.DISABLED}
|
||||
outline={true}
|
||||
@@ -106,6 +121,11 @@ class Notification extends Component {
|
||||
null
|
||||
}
|
||||
|
||||
<TagList
|
||||
tags={tags}
|
||||
tagList={tagList}
|
||||
/>
|
||||
|
||||
<EditNotificationModalConnector
|
||||
id={id}
|
||||
isOpen={this.state.isEditNotificationModalOpen}
|
||||
@@ -132,10 +152,14 @@ Notification.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
onGrab: PropTypes.bool.isRequired,
|
||||
onHealthIssue: PropTypes.bool.isRequired,
|
||||
onHealthRestored: PropTypes.bool.isRequired,
|
||||
onApplicationUpdate: PropTypes.bool.isRequired,
|
||||
supportsOnGrab: PropTypes.bool.isRequired,
|
||||
supportsOnHealthIssue: PropTypes.bool.isRequired,
|
||||
supportsOnHealthRestored: PropTypes.bool.isRequired,
|
||||
supportsOnApplicationUpdate: PropTypes.bool.isRequired,
|
||||
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onConfirmDeleteNotification: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -17,10 +17,12 @@ function NotificationEventItems(props) {
|
||||
const {
|
||||
onGrab,
|
||||
onHealthIssue,
|
||||
onHealthRestored,
|
||||
onApplicationUpdate,
|
||||
supportsOnGrab,
|
||||
includeManualGrabs,
|
||||
supportsOnHealthIssue,
|
||||
supportsOnHealthRestored,
|
||||
includeHealthWarnings,
|
||||
supportsOnApplicationUpdate
|
||||
} = item;
|
||||
@@ -70,8 +72,19 @@ function NotificationEventItems(props) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="onHealthRestored"
|
||||
helpText={translate('OnHealthRestoredHelpText')}
|
||||
isDisabled={!supportsOnHealthRestored.value}
|
||||
{...onHealthRestored}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{
|
||||
onHealthIssue.value &&
|
||||
(onHealthIssue.value || onHealthRestored.value) &&
|
||||
<div>
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
|
||||
@@ -49,6 +49,7 @@ class Notifications extends Component {
|
||||
render() {
|
||||
const {
|
||||
items,
|
||||
tagList,
|
||||
onConfirmDeleteNotification,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
@@ -71,6 +72,7 @@ class Notifications extends Component {
|
||||
<Notification
|
||||
key={item.id}
|
||||
{...item}
|
||||
tagList={tagList}
|
||||
onConfirmDeleteNotification={onConfirmDeleteNotification}
|
||||
/>
|
||||
);
|
||||
@@ -109,6 +111,7 @@ Notifications.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onConfirmDeleteNotification: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -4,13 +4,20 @@ import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { deleteNotification, fetchNotifications } from 'Store/Actions/settingsActions';
|
||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import Notifications from './Notifications';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createSortedSectionSelector('settings.notifications', sortByName),
|
||||
(notifications) => notifications
|
||||
createTagsSelector(),
|
||||
(notifications, tagList) => {
|
||||
return {
|
||||
...notifications,
|
||||
tagList
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ function TagDetailsModalContent(props) {
|
||||
indexers,
|
||||
notifications,
|
||||
indexerProxies,
|
||||
applications,
|
||||
onModalClose,
|
||||
onDeleteTagPress
|
||||
} = props;
|
||||
@@ -79,6 +80,21 @@ function TagDetailsModalContent(props) {
|
||||
}
|
||||
</FieldSet>
|
||||
}
|
||||
|
||||
{
|
||||
!!applications.length &&
|
||||
<FieldSet legend={translate('Applications')}>
|
||||
{
|
||||
applications.map((item) => {
|
||||
return (
|
||||
<div key={item.id}>
|
||||
{item.name}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</FieldSet>
|
||||
}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
@@ -110,6 +126,7 @@ TagDetailsModalContent.propTypes = {
|
||||
indexers: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
notifications: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
indexerProxies: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
applications: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onDeleteTagPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -18,16 +18,24 @@ function createMatchingIndexersSelector() {
|
||||
|
||||
function createMatchingIndexerProxiesSelector() {
|
||||
return createSelector(
|
||||
(state, { notificationIds }) => notificationIds,
|
||||
(state) => state.settings.notifications.items,
|
||||
(state, { indexerProxyIds }) => indexerProxyIds,
|
||||
(state) => state.settings.indexerProxies.items,
|
||||
findMatchingItems
|
||||
);
|
||||
}
|
||||
|
||||
function createMatchingNotificationsSelector() {
|
||||
return createSelector(
|
||||
(state, { indexerProxyIds }) => indexerProxyIds,
|
||||
(state) => state.settings.indexerProxies.items,
|
||||
(state, { notificationIds }) => notificationIds,
|
||||
(state) => state.settings.notifications.items,
|
||||
findMatchingItems
|
||||
);
|
||||
}
|
||||
|
||||
function createMatchingApplicationsSelector() {
|
||||
return createSelector(
|
||||
(state, { applicationIds }) => applicationIds,
|
||||
(state) => state.settings.applications.items,
|
||||
findMatchingItems
|
||||
);
|
||||
}
|
||||
@@ -37,11 +45,13 @@ function createMapStateToProps() {
|
||||
createMatchingIndexersSelector(),
|
||||
createMatchingIndexerProxiesSelector(),
|
||||
createMatchingNotificationsSelector(),
|
||||
(indexers, indexerProxies, notifications) => {
|
||||
createMatchingApplicationsSelector(),
|
||||
(indexers, indexerProxies, notifications, applications) => {
|
||||
return {
|
||||
indexers,
|
||||
indexerProxies,
|
||||
notifications
|
||||
notifications,
|
||||
applications
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@@ -55,7 +55,8 @@ class Tag extends Component {
|
||||
label,
|
||||
notificationIds,
|
||||
indexerIds,
|
||||
indexerProxyIds
|
||||
indexerProxyIds,
|
||||
applicationIds
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@@ -66,7 +67,8 @@ class Tag extends Component {
|
||||
const isTagUsed = !!(
|
||||
indexerIds.length ||
|
||||
notificationIds.length ||
|
||||
indexerProxyIds.length
|
||||
indexerProxyIds.length ||
|
||||
applicationIds.length
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -102,6 +104,13 @@ class Tag extends Component {
|
||||
{indexerProxyIds.length} {indexerProxyIds.length > 1 ? translate('IndexerProxies') : translate('IndexerProxy')}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
!!applicationIds.length &&
|
||||
<div>
|
||||
{applicationIds.length} {applicationIds.length > 1 ? translate('Applications') : translate('Application')}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -118,6 +127,7 @@ class Tag extends Component {
|
||||
indexerIds={indexerIds}
|
||||
notificationIds={notificationIds}
|
||||
indexerProxyIds={indexerProxyIds}
|
||||
applicationIds={applicationIds}
|
||||
isOpen={isDetailsModalOpen}
|
||||
onModalClose={this.onDetailsModalClose}
|
||||
onDeleteTagPress={this.onDeleteTagPress}
|
||||
@@ -143,13 +153,15 @@ Tag.propTypes = {
|
||||
notificationIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
indexerIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
indexerProxyIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
applicationIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
onConfirmDeleteTag: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
Tag.defaultProps = {
|
||||
indexerIds: [],
|
||||
notificationIds: [],
|
||||
indexerProxyIds: []
|
||||
indexerProxyIds: [],
|
||||
applicationIds: []
|
||||
};
|
||||
|
||||
export default Tag;
|
||||
|
||||
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchIndexerProxies, fetchNotifications } from 'Store/Actions/settingsActions';
|
||||
import { fetchApplications, fetchIndexerProxies, fetchNotifications } from 'Store/Actions/settingsActions';
|
||||
import { fetchTagDetails } from 'Store/Actions/tagActions';
|
||||
import Tags from './Tags';
|
||||
|
||||
@@ -27,7 +27,8 @@ function createMapStateToProps() {
|
||||
const mapDispatchToProps = {
|
||||
dispatchFetchTagDetails: fetchTagDetails,
|
||||
dispatchFetchNotifications: fetchNotifications,
|
||||
dispatchFetchIndexerProxies: fetchIndexerProxies
|
||||
dispatchFetchIndexerProxies: fetchIndexerProxies,
|
||||
dispatchFetchApplications: fetchApplications
|
||||
};
|
||||
|
||||
class MetadatasConnector extends Component {
|
||||
@@ -39,12 +40,14 @@ class MetadatasConnector extends Component {
|
||||
const {
|
||||
dispatchFetchTagDetails,
|
||||
dispatchFetchNotifications,
|
||||
dispatchFetchIndexerProxies
|
||||
dispatchFetchIndexerProxies,
|
||||
dispatchFetchApplications
|
||||
} = this.props;
|
||||
|
||||
dispatchFetchTagDetails();
|
||||
dispatchFetchNotifications();
|
||||
dispatchFetchIndexerProxies();
|
||||
dispatchFetchApplications();
|
||||
}
|
||||
|
||||
//
|
||||
@@ -62,7 +65,8 @@ class MetadatasConnector extends Component {
|
||||
MetadatasConnector.propTypes = {
|
||||
dispatchFetchTagDetails: PropTypes.func.isRequired,
|
||||
dispatchFetchNotifications: PropTypes.func.isRequired,
|
||||
dispatchFetchIndexerProxies: PropTypes.func.isRequired
|
||||
dispatchFetchIndexerProxies: PropTypes.func.isRequired,
|
||||
dispatchFetchApplications: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(MetadatasConnector);
|
||||
|
||||
@@ -144,7 +144,7 @@ export const defaultState = {
|
||||
},
|
||||
|
||||
category: function(item) {
|
||||
if (item.categories.length > 0) {
|
||||
if (item.categories !== undefined && item.categories.length > 0) {
|
||||
const sortedCats = item.categories.filter((cat) => cat.name !== undefined).sort((c) => c.id);
|
||||
const firstCat = sortedCats[0];
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
@define-mixin scrollbar {
|
||||
scrollbar-color: var(--scrollbarBackgroundColor) transparent;
|
||||
scrollbar-width: thin;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
|
||||
@@ -74,29 +74,29 @@ module.exports = {
|
||||
|
||||
defaultButtonTextColor: '#eee',
|
||||
defaultButtonBackgroundColor: '#333',
|
||||
defaultBorderColor: '#eaeaea',
|
||||
defaultBorderColor: '#393f45',
|
||||
defaultHoverBackgroundColor: '#444',
|
||||
defaultHoverBorderColor: '#d6d6d6;',
|
||||
defaultHoverBorderColor: '#5a6265',
|
||||
|
||||
primaryBackgroundColor: '#5d9cec',
|
||||
primaryBorderColor: '#5899eb',
|
||||
primaryHoverBackgroundColor: '#4b91ea',
|
||||
primaryHoverBorderColor: '#3483e7;',
|
||||
primaryHoverBorderColor: '#3483e7',
|
||||
|
||||
successBackgroundColor: '#27c24c',
|
||||
successBorderColor: '#26be4a',
|
||||
successHoverBackgroundColor: '#24b145',
|
||||
successHoverBorderColor: '#1f9c3d;',
|
||||
successHoverBorderColor: '#1f9c3d',
|
||||
|
||||
warningBackgroundColor: '#ff902b',
|
||||
warningBorderColor: '#ff8d26',
|
||||
warningHoverBackgroundColor: '#ff8517',
|
||||
warningHoverBorderColor: '#fc7800;',
|
||||
warningHoverBorderColor: '#fc7800',
|
||||
|
||||
dangerBackgroundColor: '#f05050',
|
||||
dangerBorderColor: '#f04b4b',
|
||||
dangerHoverBackgroundColor: '#ee3d3d',
|
||||
dangerHoverBorderColor: '#ec2626;',
|
||||
dangerHoverBorderColor: '#ec2626',
|
||||
|
||||
iconButtonDisabledColor: '#7a7a7a',
|
||||
iconButtonHoverColor: '#666',
|
||||
|
||||
@@ -76,27 +76,27 @@ module.exports = {
|
||||
defaultButtonBackgroundColor: '#fff',
|
||||
defaultBorderColor: '#eaeaea',
|
||||
defaultHoverBackgroundColor: '#f5f5f5',
|
||||
defaultHoverBorderColor: '#d6d6d6;',
|
||||
defaultHoverBorderColor: '#d6d6d6',
|
||||
|
||||
primaryBackgroundColor: '#5d9cec',
|
||||
primaryBorderColor: '#5899eb',
|
||||
primaryHoverBackgroundColor: '#4b91ea',
|
||||
primaryHoverBorderColor: '#3483e7;',
|
||||
primaryHoverBorderColor: '#3483e7',
|
||||
|
||||
successBackgroundColor: '#27c24c',
|
||||
successBorderColor: '#26be4a',
|
||||
successHoverBackgroundColor: '#24b145',
|
||||
successHoverBorderColor: '#1f9c3d;',
|
||||
successHoverBorderColor: '#1f9c3d',
|
||||
|
||||
warningBackgroundColor: '#ff902b',
|
||||
warningBorderColor: '#ff8d26',
|
||||
warningHoverBackgroundColor: '#ff8517',
|
||||
warningHoverBorderColor: '#fc7800;',
|
||||
warningHoverBorderColor: '#fc7800',
|
||||
|
||||
dangerBackgroundColor: '#f05050',
|
||||
dangerBorderColor: '#f04b4b',
|
||||
dangerHoverBackgroundColor: '#ee3d3d',
|
||||
dangerHoverBorderColor: '#ec2626;',
|
||||
dangerHoverBorderColor: '#ec2626',
|
||||
|
||||
iconButtonDisabledColor: '#7a7a7a',
|
||||
iconButtonHoverColor: '#666',
|
||||
|
||||
109
package.json
109
package.json
@@ -5,7 +5,7 @@
|
||||
"scripts": {
|
||||
"build": "webpack --config ./frontend/build/webpack.config.js",
|
||||
"prebuild": "yarn clean",
|
||||
"clean": "rimraf ./_output/UI && rimraf \"**/*.js.map\"",
|
||||
"clean": "rimraf ./_output/UI && rimraf --glob \"**/*.js.map\"",
|
||||
"start": "webpack --watch --config ./frontend/build/webpack.config.js",
|
||||
"watch": "webpack --watch --config ./frontend/build/webpack.config.js",
|
||||
"lint": "eslint --config frontend/.eslintrc.js --ignore-path frontend/.eslintignore frontend/",
|
||||
@@ -20,42 +20,38 @@
|
||||
"readmeFilename": "readme.md",
|
||||
"main": "index.js",
|
||||
"browserslist": [
|
||||
">0.25%",
|
||||
"not ie 11",
|
||||
"not op_mini all",
|
||||
"not chrome < 60"
|
||||
"defaults"
|
||||
],
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "6.2.1",
|
||||
"@fortawesome/fontawesome-svg-core": "6.2.1",
|
||||
"@fortawesome/free-regular-svg-icons": "6.2.1",
|
||||
"@fortawesome/free-solid-svg-icons": "6.2.1",
|
||||
"@fortawesome/fontawesome-free": "6.4.0",
|
||||
"@fortawesome/fontawesome-svg-core": "6.4.0",
|
||||
"@fortawesome/free-regular-svg-icons": "6.4.0",
|
||||
"@fortawesome/free-solid-svg-icons": "6.4.0",
|
||||
"@fortawesome/react-fontawesome": "0.2.0",
|
||||
"@juggle/resize-observer": "3.4.0",
|
||||
"@microsoft/signalr": "6.0.13",
|
||||
"@sentry/browser": "7.28.0",
|
||||
"@sentry/integrations": "7.28.0",
|
||||
"@types/jest": "29.2.5",
|
||||
"@types/node": "18.11.18",
|
||||
"@types/react": "18.0.26",
|
||||
"@types/react-dom": "18.0.10",
|
||||
"chart.js": "4.1.1",
|
||||
"@microsoft/signalr": "6.0.16",
|
||||
"@sentry/browser": "7.51.2",
|
||||
"@sentry/integrations": "7.51.2",
|
||||
"@types/node": "18.15.11",
|
||||
"@types/react": "18.2.6",
|
||||
"@types/react-dom": "18.2.4",
|
||||
"chart.js": "4.3.0",
|
||||
"classnames": "2.3.2",
|
||||
"clipboard": "2.0.11",
|
||||
"connected-react-router": "6.9.3",
|
||||
"element-class": "0.2.2",
|
||||
"filesize": "10.0.6",
|
||||
"filesize": "10.0.7",
|
||||
"history": "4.10.1",
|
||||
"https-browserify": "1.0.0",
|
||||
"jdu": "1.0.0",
|
||||
"jquery": "3.6.2",
|
||||
"jquery": "3.7.0",
|
||||
"lodash": "4.17.21",
|
||||
"mobile-detect": "1.4.5",
|
||||
"moment": "2.29.4",
|
||||
"mousetrap": "1.6.5",
|
||||
"normalize.css": "8.0.1",
|
||||
"prop-types": "15.8.1",
|
||||
"qs": "6.11.0",
|
||||
"qs": "6.11.1",
|
||||
"react": "17.0.2",
|
||||
"react-addons-shallow-compare": "15.6.3",
|
||||
"react-async-script": "1.2.0",
|
||||
@@ -67,7 +63,7 @@
|
||||
"react-dnd-touch-backend": "14.1.1",
|
||||
"react-document-title": "2.0.3",
|
||||
"react-dom": "17.0.2",
|
||||
"react-focus-lock": "2.9.2",
|
||||
"react-focus-lock": "2.9.4",
|
||||
"react-google-recaptcha": "2.1.0",
|
||||
"react-lazyload": "3.2.0",
|
||||
"react-measure": "1.4.7",
|
||||
@@ -77,79 +73,76 @@
|
||||
"react-router-dom": "5.2.0",
|
||||
"react-text-truncate": "0.19.0",
|
||||
"react-use-measure": "2.1.1",
|
||||
"react-virtualized": "9.21.1",
|
||||
"react-virtualized": "9.22.3",
|
||||
"react-window": "1.8.8",
|
||||
"redux": "4.2.0",
|
||||
"redux": "4.2.1",
|
||||
"redux-actions": "2.6.5",
|
||||
"redux-batched-actions": "0.5.0",
|
||||
"redux-localstorage": "0.4.1",
|
||||
"redux-thunk": "2.4.2",
|
||||
"reselect": "4.1.7",
|
||||
"stacktrace-js": "2.0.2",
|
||||
"typescript": "4.9.4"
|
||||
"typescript": "5.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.20.5",
|
||||
"@babel/eslint-parser": "7.19.1",
|
||||
"@babel/core": "7.21.8",
|
||||
"@babel/eslint-parser": "7.21.8",
|
||||
"@babel/plugin-proposal-class-properties": "7.18.6",
|
||||
"@babel/plugin-proposal-decorators": "7.20.5",
|
||||
"@babel/plugin-proposal-export-default-from": "7.18.10",
|
||||
"@babel/plugin-proposal-export-namespace-from": "7.18.9",
|
||||
"@babel/plugin-proposal-function-sent": "7.18.6",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "7.18.6",
|
||||
"@babel/plugin-proposal-numeric-separator": "7.18.6",
|
||||
"@babel/plugin-proposal-optional-chaining": "7.18.9",
|
||||
"@babel/plugin-proposal-throw-expressions": "7.18.6",
|
||||
"@babel/plugin-proposal-optional-chaining": "7.21.0",
|
||||
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
||||
"@babel/preset-env": "7.20.2",
|
||||
"@babel/preset-env": "7.21.5",
|
||||
"@babel/preset-react": "7.18.6",
|
||||
"@babel/preset-typescript": "7.18.6",
|
||||
"@babel/preset-typescript": "7.21.5",
|
||||
"@types/react-window": "1.8.5",
|
||||
"@typescript-eslint/eslint-plugin": "5.48.1",
|
||||
"@typescript-eslint/parser": "5.48.0",
|
||||
"@types/webpack-livereload-plugin": "2.3.3",
|
||||
"@typescript-eslint/eslint-plugin": "5.59.5",
|
||||
"@typescript-eslint/parser": "5.59.5",
|
||||
"are-you-es5": "2.1.2",
|
||||
"autoprefixer": "10.4.13",
|
||||
"babel-eslint": "10.1.0",
|
||||
"babel-loader": "9.1.0",
|
||||
"autoprefixer": "10.4.14",
|
||||
"babel-loader": "9.1.2",
|
||||
"babel-plugin-inline-classnames": "2.0.1",
|
||||
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
|
||||
"core-js": "3.26.1",
|
||||
"core-js": "3.30.2",
|
||||
"css-loader": "6.7.3",
|
||||
"css-modules-typescript-loader": "4.0.1",
|
||||
"eslint": "8.30.0",
|
||||
"eslint-config-prettier": "8.6.0",
|
||||
"eslint": "8.40.0",
|
||||
"eslint-config-prettier": "8.8.0",
|
||||
"eslint-plugin-filenames": "1.3.2",
|
||||
"eslint-plugin-import": "2.26.0",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"eslint-plugin-prettier": "4.2.1",
|
||||
"eslint-plugin-react": "7.31.11",
|
||||
"eslint-plugin-react": "7.32.2",
|
||||
"eslint-plugin-react-hooks": "4.6.0",
|
||||
"eslint-plugin-simple-import-sort": "8.0.0",
|
||||
"eslint-plugin-simple-import-sort": "10.0.0",
|
||||
"file-loader": "6.2.0",
|
||||
"filemanager-webpack-plugin": "8.0.0",
|
||||
"fork-ts-checker-webpack-plugin": "7.2.14",
|
||||
"html-webpack-plugin": "5.5.0",
|
||||
"fork-ts-checker-webpack-plugin": "8.0.0",
|
||||
"html-webpack-plugin": "5.5.1",
|
||||
"loader-utils": "^3.2.1",
|
||||
"mini-css-extract-plugin": "2.7.2",
|
||||
"postcss": "8.4.20",
|
||||
"mini-css-extract-plugin": "2.7.5",
|
||||
"postcss": "8.4.23",
|
||||
"postcss-color-function": "4.1.0",
|
||||
"postcss-loader": "7.0.2",
|
||||
"postcss-loader": "7.3.0",
|
||||
"postcss-mixins": "9.0.4",
|
||||
"postcss-nested": "6.0.0",
|
||||
"postcss-nested": "6.0.1",
|
||||
"postcss-simple-vars": "7.0.1",
|
||||
"postcss-url": "10.1.3",
|
||||
"prettier": "2.8.2",
|
||||
"prettier": "2.8.8",
|
||||
"require-nocache": "1.0.0",
|
||||
"rimraf": "3.0.2",
|
||||
"rimraf": "4.4.1",
|
||||
"run-sequence": "2.2.1",
|
||||
"streamqueue": "1.1.2",
|
||||
"style-loader": "3.3.1",
|
||||
"stylelint": "14.16.0",
|
||||
"stylelint-order": "5.0.0",
|
||||
"style-loader": "3.3.2",
|
||||
"stylelint": "15.6.1",
|
||||
"stylelint-order": "6.0.3",
|
||||
"terser-webpack-plugin": "5.3.8",
|
||||
"ts-loader": "9.4.2",
|
||||
"typescript-plugin-css-modules": "4.1.1",
|
||||
"typescript-plugin-css-modules": "5.0.1",
|
||||
"url-loader": "4.1.1",
|
||||
"webpack": "5.75.0",
|
||||
"webpack-cli": "5.0.1",
|
||||
"webpack": "5.82.1",
|
||||
"webpack-cli": "5.1.1",
|
||||
"webpack-livereload-plugin": "3.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
<PropertyGroup>
|
||||
<AnalysisLevel>6.0-all</AnalysisLevel>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles>
|
||||
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
||||
<ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles>
|
||||
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
|
||||
<RuntimeIdentifiers>win-x64;win-x86;osx-x64;osx-arm64;linux-x64;linux-musl-x64;linux-musl-arm;linux-arm;linux-arm64;linux-musl-arm64</RuntimeIdentifiers>
|
||||
@@ -24,7 +26,7 @@
|
||||
<ProwlarrProject Condition="$(MSBuildProjectName.StartsWith('Prowlarr'))">true</ProwlarrProject>
|
||||
<ProwlarrProject Condition="$(MSBuildProjectName.StartsWith('ServiceInstall'))">true</ProwlarrProject>
|
||||
<ProwlarrProject Condition="$(MSBuildProjectName.StartsWith('ServiceUninstall'))">true</ProwlarrProject>
|
||||
|
||||
|
||||
<!-- A test project gets the test sdk packages automatically added -->
|
||||
<TestProject>false</TestProject>
|
||||
<TestProject Condition="$(MSBuildProjectName.EndsWith('.Test'))">true</TestProject>
|
||||
@@ -50,7 +52,7 @@
|
||||
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
<!-- Test projects need bindingRedirects -->
|
||||
<PropertyGroup Condition="'$(ProwlarrOutputType)'=='Test'">
|
||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||
@@ -63,7 +65,7 @@
|
||||
<Product>Prowlarr</Product>
|
||||
<Company>prowlarr.com</Company>
|
||||
<Copyright>Copyright 2014-$([System.DateTime]::Now.ToString('yyyy')) prowlarr.com (GNU General Public v3)</Copyright>
|
||||
|
||||
|
||||
<!-- Should be replaced by CI -->
|
||||
<AssemblyVersion>10.0.0.*</AssemblyVersion>
|
||||
<AssemblyConfiguration>$(Configuration)-dev</AssemblyConfiguration>
|
||||
@@ -72,8 +74,10 @@
|
||||
<GenerateAssemblyFileVersionAttribute>false</GenerateAssemblyFileVersionAttribute>
|
||||
<GenerateAssemblyInformationalVersionAttribute>false</GenerateAssemblyInformationalVersionAttribute>
|
||||
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
|
||||
|
||||
|
||||
<Deterministic Condition="$(AssemblyVersion.EndsWith('*'))">False</Deterministic>
|
||||
|
||||
<PathMap>$(MSBuildProjectDirectory)=./$(MSBuildProjectName)/</PathMap>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Set the AssemblyConfiguration attribute for projects -->
|
||||
@@ -92,7 +96,7 @@
|
||||
<!-- FXCop Built into Net5 SDK now as NETAnalyzers, Enabled by default on net5 projects -->
|
||||
<EnableNETAnalyzers>false</EnableNETAnalyzers>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
<!-- Standard testing packages -->
|
||||
<ItemGroup Condition="'$(TestProject)'=='true'">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
|
||||
|
||||
@@ -11,7 +11,6 @@ using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Test.Common;
|
||||
using OpenQA.Selenium;
|
||||
using OpenQA.Selenium.Chrome;
|
||||
using OpenQA.Selenium.Remote;
|
||||
|
||||
namespace NzbDrone.Automation.Test
|
||||
{
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@@ -352,6 +351,26 @@ namespace NzbDrone.Common.Test.DiskTests
|
||||
.Verify(v => v.DeleteFile(_targetPath), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_rollback_move_on_partial_if_destination_already_exists()
|
||||
{
|
||||
Mocker.GetMock<IDiskProvider>()
|
||||
.Setup(v => v.MoveFile(_sourcePath, _targetPath, false))
|
||||
.Callback(() =>
|
||||
{
|
||||
WithExistingFile(_targetPath, true, 900);
|
||||
});
|
||||
|
||||
Mocker.GetMock<IDiskProvider>()
|
||||
.Setup(v => v.MoveFile(_sourcePath, _targetPath, false))
|
||||
.Throws(new FileAlreadyExistsException("File already exists", _targetPath));
|
||||
|
||||
Assert.Throws<FileAlreadyExistsException>(() => Subject.TransferFile(_sourcePath, _targetPath, TransferMode.Move));
|
||||
|
||||
Mocker.GetMock<IDiskProvider>()
|
||||
.Verify(v => v.DeleteFile(_targetPath), Times.Never());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_log_error_if_rollback_partialmove_fails()
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.EnsureThat;
|
||||
using NzbDrone.Test.Common;
|
||||
|
||||
@@ -12,14 +13,14 @@ namespace NzbDrone.Common.Test.EnsureTest
|
||||
public void EnsureWindowsPath(string path)
|
||||
{
|
||||
WindowsOnly();
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||
}
|
||||
|
||||
[TestCase(@"/var/user/file with, comma.mkv")]
|
||||
public void EnsureLinuxPath(string path)
|
||||
{
|
||||
PosixOnly();
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System.Globalization;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Extensions;
|
||||
|
||||
101
src/NzbDrone.Common.Test/Http/CookieUtilFixture.cs
Normal file
101
src/NzbDrone.Common.Test/Http/CookieUtilFixture.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Http;
|
||||
|
||||
namespace NzbDrone.Common.Test.Http
|
||||
{
|
||||
[TestFixture]
|
||||
public class CookieUtilFixture
|
||||
{
|
||||
[Test]
|
||||
public void CookieHeaderToDictionaryGood()
|
||||
{
|
||||
// valid cookies with non-alpha characters in the value
|
||||
var cookieHeader = "__cfduid=d6237f041586694295; __cf_bm=TlOng/xyqckk-TMen38z+0RFYA7YA=";
|
||||
var expectedCookieDictionary = new Dictionary<string, string>
|
||||
{
|
||||
{ "__cfduid", "d6237f041586694295" },
|
||||
{ "__cf_bm", "TlOng/xyqckk-TMen38z+0RFYA7YA=" }
|
||||
};
|
||||
CollectionAssert.AreEqual(expectedCookieDictionary, CookieUtil.CookieHeaderToDictionary(cookieHeader));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CookieHeaderToDictionaryDuplicateKeys()
|
||||
{
|
||||
// cookie with duplicate keys and whitespace separator instead of ;
|
||||
var cookieHeader = "__cfduid=d6237f041586694295; __cf_bm=TlOng/xyqckk-TMen38z+0RFYA7YA= __cf_bm=test";
|
||||
var expectedCookieDictionary = new Dictionary<string, string>
|
||||
{
|
||||
{ "__cfduid", "d6237f041586694295" },
|
||||
{ "__cf_bm", "test" } // we always assume the latest value is the most recent
|
||||
};
|
||||
CollectionAssert.AreEqual(expectedCookieDictionary, CookieUtil.CookieHeaderToDictionary(cookieHeader));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CookieHeaderToDictionaryMalformed()
|
||||
{
|
||||
// malformed cookies
|
||||
var cookieHeader = "__cfduidd6237f041586694295; __cf_;bm TlOng; good_cookie=value";
|
||||
var expectedCookieDictionary = new Dictionary<string, string> { { "good_cookie", "value" }, };
|
||||
CollectionAssert.AreEqual(expectedCookieDictionary, CookieUtil.CookieHeaderToDictionary(cookieHeader));
|
||||
}
|
||||
|
||||
[Test]
|
||||
[SuppressMessage("ReSharper", "CollectionNeverUpdated.Local")]
|
||||
public void CookieHeaderToDictionaryNull()
|
||||
{
|
||||
// null cookie header
|
||||
var expectedCookieDictionary = new Dictionary<string, string>();
|
||||
CollectionAssert.AreEqual(expectedCookieDictionary, CookieUtil.CookieHeaderToDictionary(null));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CookieDictionaryToHeaderGood()
|
||||
{
|
||||
// valid cookies with non-alpha characters in the value
|
||||
var cookieDictionary = new Dictionary<string, string>
|
||||
{
|
||||
{ "__cfduid", "d6237f041586694295" },
|
||||
{ "__cf_bm", "TlOng/xyqckk-TMen38z+0RFYA7YA=" }
|
||||
};
|
||||
var expectedCookieHeader = "__cfduid=d6237f041586694295; __cf_bm=TlOng/xyqckk-TMen38z+0RFYA7YA=";
|
||||
CollectionAssert.AreEqual(expectedCookieHeader, CookieUtil.CookieDictionaryToHeader(cookieDictionary));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CookieDictionaryToHeaderMalformed1()
|
||||
{
|
||||
// malformed key
|
||||
var cookieDictionary = new Dictionary<string, string>
|
||||
{
|
||||
{ "__cf_=bm", "34234234" }
|
||||
};
|
||||
var ex = Assert.Throws<FormatException>(() => CookieUtil.CookieDictionaryToHeader(cookieDictionary));
|
||||
Assert.AreEqual("The cookie '__cf_=bm=34234234' is malformed.", ex.Message);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CookieDictionaryToHeaderMalformed2()
|
||||
{
|
||||
// malformed value
|
||||
var cookieDictionary = new Dictionary<string, string>
|
||||
{
|
||||
{ "__cf_bm", "34234 234" }
|
||||
};
|
||||
var ex = Assert.Throws<FormatException>(() => CookieUtil.CookieDictionaryToHeader(cookieDictionary));
|
||||
Assert.AreEqual("The cookie '__cf_bm=34234 234' is malformed.", ex.Message);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CookieDictionaryToHeaderNull()
|
||||
{
|
||||
// null cookie dictionary
|
||||
var expectedCookieHeader = "";
|
||||
CollectionAssert.AreEqual(expectedCookieHeader, CookieUtil.CookieDictionaryToHeader(null));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ namespace NzbDrone.Common.Test
|
||||
[TestCase(@"\\Testserver\Test\file.ext", @"\\Testserver\Test\file.ext")]
|
||||
[TestCase(@"\\Testserver\Test\file.ext\\", @"\\Testserver\Test\file.ext")]
|
||||
[TestCase(@"\\Testserver\Test\file.ext \\", @"\\Testserver\Test\file.ext")]
|
||||
[TestCase(@"//CAPITAL//lower// ", @"\\CAPITAL\lower")]
|
||||
public void Clean_Path_Windows(string dirty, string clean)
|
||||
{
|
||||
WindowsOnly();
|
||||
|
||||
@@ -3,7 +3,6 @@ using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Common.EnsureThat;
|
||||
using NzbDrone.Common.Extensions;
|
||||
|
||||
namespace NzbDrone.Common.Cache
|
||||
{
|
||||
|
||||
@@ -5,8 +5,6 @@ using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Loader;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
|
||||
namespace NzbDrone.Common.Composition
|
||||
@@ -19,16 +17,17 @@ namespace NzbDrone.Common.Composition
|
||||
RegisterSQLiteResolver();
|
||||
}
|
||||
|
||||
public static IEnumerable<Assembly> Load(IEnumerable<string> assemblies)
|
||||
public static IList<Assembly> Load(IList<string> assemblyNames)
|
||||
{
|
||||
var toLoad = assemblies.ToList();
|
||||
var toLoad = assemblyNames.ToList();
|
||||
toLoad.Add("Prowlarr.Common");
|
||||
toLoad.Add(OsInfo.IsWindows ? "Prowlarr.Windows" : "Prowlarr.Mono");
|
||||
|
||||
var startupPath = AppDomain.CurrentDomain.BaseDirectory;
|
||||
|
||||
return toLoad.Select(x =>
|
||||
AssemblyLoadContext.Default.LoadFromAssemblyPath(Path.Combine(startupPath, $"{x}.dll")));
|
||||
return toLoad
|
||||
.Select(x => AssemblyLoadContext.Default.LoadFromAssemblyPath(Path.Combine(startupPath, $"{x}.dll")))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static Assembly ContainerResolveEventHandler(object sender, ResolveEventArgs args)
|
||||
|
||||
@@ -65,7 +65,7 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
private void CheckFolderExists(string path)
|
||||
{
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
if (!FolderExists(path))
|
||||
{
|
||||
@@ -75,7 +75,7 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
private void CheckFileExists(string path)
|
||||
{
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
if (!FileExists(path))
|
||||
{
|
||||
@@ -93,19 +93,19 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
public bool FolderExists(string path)
|
||||
{
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||
return Directory.Exists(path);
|
||||
}
|
||||
|
||||
public bool FileExists(string path)
|
||||
{
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||
return FileExists(path, PathStringComparison);
|
||||
}
|
||||
|
||||
public bool FileExists(string path, StringComparison stringComparison)
|
||||
{
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
switch (stringComparison)
|
||||
{
|
||||
@@ -125,7 +125,7 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
public bool FolderWritable(string path)
|
||||
{
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -144,35 +144,35 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
public bool FolderEmpty(string path)
|
||||
{
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
return Directory.EnumerateFileSystemEntries(path).Empty();
|
||||
}
|
||||
|
||||
public string[] GetDirectories(string path)
|
||||
{
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
return Directory.GetDirectories(path);
|
||||
}
|
||||
|
||||
public string[] GetFiles(string path, SearchOption searchOption)
|
||||
{
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
return Directory.GetFiles(path, "*.*", searchOption);
|
||||
}
|
||||
|
||||
public long GetFolderSize(string path)
|
||||
{
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
return GetFiles(path, SearchOption.AllDirectories).Sum(e => new FileInfo(e).Length);
|
||||
}
|
||||
|
||||
public long GetFileSize(string path)
|
||||
{
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
if (!FileExists(path))
|
||||
{
|
||||
@@ -185,13 +185,13 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
public void CreateFolder(string path)
|
||||
{
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||
Directory.CreateDirectory(path);
|
||||
}
|
||||
|
||||
public void DeleteFile(string path)
|
||||
{
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||
Logger.Trace("Deleting file: {0}", path);
|
||||
|
||||
RemoveReadOnly(path);
|
||||
@@ -201,8 +201,8 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
public void CloneFile(string source, string destination, bool overwrite = false)
|
||||
{
|
||||
Ensure.That(source, () => source).IsValidPath();
|
||||
Ensure.That(destination, () => destination).IsValidPath();
|
||||
Ensure.That(source, () => source).IsValidPath(PathValidationType.CurrentOs);
|
||||
Ensure.That(destination, () => destination).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
if (source.PathEquals(destination))
|
||||
{
|
||||
@@ -219,8 +219,8 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
public void CopyFile(string source, string destination, bool overwrite = false)
|
||||
{
|
||||
Ensure.That(source, () => source).IsValidPath();
|
||||
Ensure.That(destination, () => destination).IsValidPath();
|
||||
Ensure.That(source, () => source).IsValidPath(PathValidationType.CurrentOs);
|
||||
Ensure.That(destination, () => destination).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
if (source.PathEquals(destination))
|
||||
{
|
||||
@@ -237,8 +237,8 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
public void MoveFile(string source, string destination, bool overwrite = false)
|
||||
{
|
||||
Ensure.That(source, () => source).IsValidPath();
|
||||
Ensure.That(destination, () => destination).IsValidPath();
|
||||
Ensure.That(source, () => source).IsValidPath(PathValidationType.CurrentOs);
|
||||
Ensure.That(destination, () => destination).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
if (source.PathEquals(destination))
|
||||
{
|
||||
@@ -256,14 +256,19 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
public void MoveFolder(string source, string destination, bool overwrite = false)
|
||||
{
|
||||
Ensure.That(source, () => source).IsValidPath();
|
||||
Ensure.That(destination, () => destination).IsValidPath();
|
||||
Ensure.That(source, () => source).IsValidPath(PathValidationType.CurrentOs);
|
||||
Ensure.That(destination, () => destination).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
Directory.Move(source, destination);
|
||||
}
|
||||
|
||||
protected virtual void MoveFileInternal(string source, string destination)
|
||||
{
|
||||
if (File.Exists(destination))
|
||||
{
|
||||
throw new FileAlreadyExistsException("File already exists", destination);
|
||||
}
|
||||
|
||||
File.Move(source, destination);
|
||||
}
|
||||
|
||||
@@ -281,7 +286,7 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
public void DeleteFolder(string path, bool recursive)
|
||||
{
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
var files = Directory.GetFiles(path, "*.*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly);
|
||||
Array.ForEach(files, RemoveReadOnly);
|
||||
@@ -291,14 +296,14 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
public string ReadAllText(string filePath)
|
||||
{
|
||||
Ensure.That(filePath, () => filePath).IsValidPath();
|
||||
Ensure.That(filePath, () => filePath).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
return File.ReadAllText(filePath);
|
||||
}
|
||||
|
||||
public void WriteAllText(string filename, string contents)
|
||||
{
|
||||
Ensure.That(filename, () => filename).IsValidPath();
|
||||
Ensure.That(filename, () => filename).IsValidPath(PathValidationType.CurrentOs);
|
||||
RemoveReadOnly(filename);
|
||||
|
||||
// File.WriteAllText is broken on net core when writing to some CIFS mounts
|
||||
@@ -314,7 +319,7 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
public void FolderSetLastWriteTime(string path, DateTime dateTime)
|
||||
{
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
if (dateTime.Before(DateTimeExtensions.Epoch))
|
||||
{
|
||||
@@ -326,7 +331,7 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
public void FileSetLastWriteTime(string path, DateTime dateTime)
|
||||
{
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
if (dateTime.Before(DateTimeExtensions.Epoch))
|
||||
{
|
||||
@@ -351,16 +356,16 @@ namespace NzbDrone.Common.Disk
|
||||
}
|
||||
}
|
||||
|
||||
public string GetPathRoot(string path)
|
||||
public virtual string GetPathRoot(string path)
|
||||
{
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
return Path.GetPathRoot(path);
|
||||
}
|
||||
|
||||
public string GetParentFolder(string path)
|
||||
{
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
var parent = Directory.GetParent(path.TrimEnd(Path.DirectorySeparatorChar));
|
||||
|
||||
@@ -407,7 +412,7 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
public void EmptyFolder(string path)
|
||||
{
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
foreach (var file in GetFiles(path, SearchOption.TopDirectoryOnly))
|
||||
{
|
||||
@@ -478,8 +483,7 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
return mounts.Where(drive => drive.RootDirectory.PathEquals(path) ||
|
||||
drive.RootDirectory.IsParentPath(path))
|
||||
.OrderByDescending(drive => drive.RootDirectory.Length)
|
||||
.FirstOrDefault();
|
||||
.MaxBy(drive => drive.RootDirectory.Length);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -497,7 +501,7 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
public List<DirectoryInfo> GetDirectoryInfos(string path)
|
||||
{
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
var di = new DirectoryInfo(path);
|
||||
|
||||
@@ -506,14 +510,14 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
public FileInfo GetFileInfo(string path)
|
||||
{
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
return new FileInfo(path);
|
||||
}
|
||||
|
||||
public List<FileInfo> GetFileInfos(string path, SearchOption searchOption = SearchOption.TopDirectoryOnly)
|
||||
{
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
var di = new DirectoryInfo(path);
|
||||
|
||||
|
||||
@@ -43,8 +43,8 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
public TransferMode TransferFolder(string sourcePath, string targetPath, TransferMode mode)
|
||||
{
|
||||
Ensure.That(sourcePath, () => sourcePath).IsValidPath();
|
||||
Ensure.That(targetPath, () => targetPath).IsValidPath();
|
||||
Ensure.That(sourcePath, () => sourcePath).IsValidPath(PathValidationType.CurrentOs);
|
||||
Ensure.That(targetPath, () => targetPath).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
sourcePath = ResolveRealParentPath(sourcePath);
|
||||
targetPath = ResolveRealParentPath(targetPath);
|
||||
@@ -140,8 +140,8 @@ namespace NzbDrone.Common.Disk
|
||||
{
|
||||
var filesCopied = 0;
|
||||
|
||||
Ensure.That(sourcePath, () => sourcePath).IsValidPath();
|
||||
Ensure.That(targetPath, () => targetPath).IsValidPath();
|
||||
Ensure.That(sourcePath, () => sourcePath).IsValidPath(PathValidationType.CurrentOs);
|
||||
Ensure.That(targetPath, () => targetPath).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
sourcePath = ResolveRealParentPath(sourcePath);
|
||||
targetPath = ResolveRealParentPath(targetPath);
|
||||
@@ -255,8 +255,8 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
public TransferMode TransferFile(string sourcePath, string targetPath, TransferMode mode, bool overwrite = false)
|
||||
{
|
||||
Ensure.That(sourcePath, () => sourcePath).IsValidPath();
|
||||
Ensure.That(targetPath, () => targetPath).IsValidPath();
|
||||
Ensure.That(sourcePath, () => sourcePath).IsValidPath(PathValidationType.CurrentOs);
|
||||
Ensure.That(targetPath, () => targetPath).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
sourcePath = ResolveRealParentPath(sourcePath);
|
||||
targetPath = ResolveRealParentPath(targetPath);
|
||||
@@ -500,9 +500,13 @@ namespace NzbDrone.Common.Disk
|
||||
throw new IOException(string.Format("File move incomplete, data loss may have occurred. [{0}] was {1} bytes long instead of the expected {2}.", targetPath, targetSize, originalSize));
|
||||
}
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
RollbackPartialMove(sourcePath, targetPath);
|
||||
if (ex is not FileAlreadyExistsException)
|
||||
{
|
||||
RollbackPartialMove(sourcePath, targetPath);
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
15
src/NzbDrone.Common/Disk/FileAlreadyExistsException.cs
Normal file
15
src/NzbDrone.Common/Disk/FileAlreadyExistsException.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
|
||||
namespace NzbDrone.Common.Disk
|
||||
{
|
||||
public class FileAlreadyExistsException : Exception
|
||||
{
|
||||
public string Filename { get; set; }
|
||||
|
||||
public FileAlreadyExistsException(string message, string filename)
|
||||
: base(message)
|
||||
{
|
||||
Filename = filename;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,7 +71,7 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
if (
|
||||
allowFoldersWithoutTrailingSlashes &&
|
||||
query.IsPathValid() &&
|
||||
query.IsPathValid(PathValidationType.CurrentOs) &&
|
||||
_diskProvider.FolderExists(query))
|
||||
{
|
||||
return GetResult(query, includeFiles);
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Security.AccessControl;
|
||||
using System.Security.Principal;
|
||||
|
||||
namespace NzbDrone.Common.Disk
|
||||
{
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Common.Extensions;
|
||||
|
||||
namespace NzbDrone.Common.Disk
|
||||
@@ -162,7 +161,7 @@ namespace NzbDrone.Common.Disk
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsValid => _path.IsPathValid();
|
||||
public bool IsValid => _path.IsPathValid(PathValidationType.CurrentOs);
|
||||
|
||||
private int GetFileNameIndex()
|
||||
{
|
||||
|
||||
8
src/NzbDrone.Common/Disk/PathValidationType.cs
Normal file
8
src/NzbDrone.Common/Disk/PathValidationType.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace NzbDrone.Common.Disk
|
||||
{
|
||||
public enum PathValidationType
|
||||
{
|
||||
CurrentOs,
|
||||
AnyOs
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text.RegularExpressions;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.EnsureThat.Resources;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
@@ -111,14 +112,14 @@ namespace NzbDrone.Common.EnsureThat
|
||||
}
|
||||
|
||||
[DebuggerStepThrough]
|
||||
public static Param<string> IsValidPath(this Param<string> param)
|
||||
public static Param<string> IsValidPath(this Param<string> param, PathValidationType validationType)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(param.Value))
|
||||
{
|
||||
throw ExceptionFactory.CreateForParamValidation(param.Name, ExceptionMessages.EnsureExtensions_IsNotNullOrWhiteSpace);
|
||||
}
|
||||
|
||||
if (param.Value.IsPathValid())
|
||||
if (param.Value.IsPathValid(validationType))
|
||||
{
|
||||
return param;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.AccessControl;
|
||||
using System.Security.Principal;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.Exceptions;
|
||||
|
||||
@@ -29,12 +29,12 @@ namespace NzbDrone.Common.Extensions
|
||||
public static string CleanFilePath(this string path)
|
||||
{
|
||||
Ensure.That(path, () => path).IsNotNullOrWhiteSpace();
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.AnyOs);
|
||||
|
||||
var info = new FileInfo(path.Trim());
|
||||
|
||||
// UNC
|
||||
if (OsInfo.IsWindows && info.FullName.StartsWith(@"\\"))
|
||||
if (!info.FullName.Contains('/') && info.FullName.StartsWith(@"\\"))
|
||||
{
|
||||
return info.FullName.TrimEnd('/', '\\', ' ');
|
||||
}
|
||||
@@ -136,24 +136,24 @@ namespace NzbDrone.Common.Extensions
|
||||
|
||||
private static readonly Regex WindowsPathWithDriveRegex = new Regex(@"^[a-zA-Z]:\\", RegexOptions.Compiled);
|
||||
|
||||
public static bool IsPathValid(this string path)
|
||||
public static bool IsPathValid(this string path, PathValidationType validationType)
|
||||
{
|
||||
if (path.ContainsInvalidPathChars() || string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (validationType == PathValidationType.AnyOs)
|
||||
{
|
||||
return IsPathValidForWindows(path) || IsPathValidForNonWindows(path);
|
||||
}
|
||||
|
||||
if (OsInfo.IsNotWindows)
|
||||
{
|
||||
return path.StartsWith(Path.DirectorySeparatorChar.ToString());
|
||||
return IsPathValidForNonWindows(path);
|
||||
}
|
||||
|
||||
if (path.StartsWith("\\") || WindowsPathWithDriveRegex.IsMatch(path))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return IsPathValidForWindows(path);
|
||||
}
|
||||
|
||||
public static bool ContainsInvalidPathChars(this string text)
|
||||
@@ -337,5 +337,15 @@ namespace NzbDrone.Common.Extensions
|
||||
{
|
||||
return Path.Combine(appFolderInfo.StartUpFolder, NLOG_CONFIG_FILE);
|
||||
}
|
||||
|
||||
private static bool IsPathValidForWindows(string path)
|
||||
{
|
||||
return path.StartsWith("\\") || WindowsPathWithDriveRegex.IsMatch(path);
|
||||
}
|
||||
|
||||
private static bool IsPathValidForNonWindows(string path)
|
||||
{
|
||||
return path.StartsWith("/");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,18 +16,7 @@ namespace NzbDrone.Common.Extensions
|
||||
return false;
|
||||
}
|
||||
|
||||
Uri uri;
|
||||
if (!Uri.TryCreate(path, UriKind.Absolute, out uri))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!uri.IsWellFormedOriginalString())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return Uri.TryCreate(path, UriKind.Absolute, out var uri) && uri.IsWellFormedOriginalString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,14 @@ namespace NzbDrone.Common
|
||||
return $"{mCrc:x8}";
|
||||
}
|
||||
|
||||
public static string ComputeSha256Hash(string rawData)
|
||||
{
|
||||
using var sha256Hash = SHA256.Create();
|
||||
var hashBytes = sha256Hash.ComputeHash(Encoding.UTF8.GetBytes(rawData));
|
||||
|
||||
return Convert.ToHexString(hashBytes);
|
||||
}
|
||||
|
||||
public static string CalculateMd5(string s)
|
||||
{
|
||||
// Use input string to calculate MD5 hash
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace NzbDrone.Common.Http
|
||||
{
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
|
||||
// NOTE: we are not checking non-ascii characters and we should
|
||||
private static readonly Regex _CookieRegex = new Regex(@"([^\(\)<>@,;:\\""/\[\]\?=\{\}\s]+)=([^,;\\""\s]+)");
|
||||
private static readonly Regex CookieRegex = new (@"([^\(\)<>@,;:\\""/\[\]\?=\{\}\s]+)=([^,;\\""\s]+)");
|
||||
private static readonly string[] FilterProps = { "COMMENT", "COMMENTURL", "DISCORD", "DOMAIN", "EXPIRES", "MAX-AGE", "PATH", "PORT", "SECURE", "VERSION", "HTTPONLY", "SAMESITE" };
|
||||
private static readonly char[] InvalidKeyChars = { '(', ')', '<', '>', '@', ',', ';', ':', '\\', '"', '/', '[', ']', '?', '=', '{', '}', ' ', '\t', '\n' };
|
||||
private static readonly char[] InvalidValueChars = { '"', ',', ';', '\\', ' ', '\t', '\n' };
|
||||
@@ -22,7 +22,7 @@ namespace NzbDrone.Common.Http
|
||||
return cookieDictionary;
|
||||
}
|
||||
|
||||
var matches = _CookieRegex.Match(cookieHeader);
|
||||
var matches = CookieRegex.Match(cookieHeader);
|
||||
while (matches.Success)
|
||||
{
|
||||
if (matches.Groups.Count > 2 && !FilterProps.Contains(matches.Groups[1].Value.ToUpperInvariant()))
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System.Net.Http;
|
||||
using System.Net.Security;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Security;
|
||||
@@ -9,7 +8,6 @@ using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http.Proxy;
|
||||
@@ -247,7 +245,7 @@ namespace NzbDrone.Common.Http.Dispatchers
|
||||
}
|
||||
}
|
||||
|
||||
private void AddContentHeader(HttpRequestMessage request, string header, string value)
|
||||
private static void AddContentHeader(HttpRequestMessage request, string header, string value)
|
||||
{
|
||||
var headers = request.Content?.Headers;
|
||||
if (headers == null)
|
||||
|
||||
@@ -322,11 +322,12 @@ namespace NzbDrone.Common.Http
|
||||
_logger.Debug("Downloading [{0}] to [{1}]", url, fileName);
|
||||
|
||||
var stopWatch = Stopwatch.StartNew();
|
||||
using (var fileStream = new FileStream(fileNamePart, FileMode.Create, FileAccess.ReadWrite))
|
||||
await using (var fileStream = new FileStream(fileNamePart, FileMode.Create, FileAccess.ReadWrite))
|
||||
{
|
||||
var request = new HttpRequest(url);
|
||||
request.AllowAutoRedirect = true;
|
||||
request.ResponseStream = fileStream;
|
||||
request.RequestTimeout = TimeSpan.FromSeconds(300);
|
||||
var response = await GetAsync(request);
|
||||
|
||||
if (response.Headers.ContentType != null && response.Headers.ContentType.Contains("text/html"))
|
||||
|
||||
@@ -4,7 +4,6 @@ using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using NzbDrone.Common.Extensions;
|
||||
|
||||
@@ -21,6 +21,7 @@ namespace NzbDrone.Common.Http
|
||||
public Dictionary<string, string> Segments { get; private set; }
|
||||
public HttpHeader Headers { get; private set; }
|
||||
public bool SuppressHttpError { get; set; }
|
||||
public IEnumerable<HttpStatusCode> SuppressHttpErrorStatusCodes { get; set; }
|
||||
public bool LogHttpError { get; set; }
|
||||
public bool UseSimplifiedUserAgent { get; set; }
|
||||
public bool AllowAutoRedirect { get; set; }
|
||||
@@ -108,6 +109,7 @@ namespace NzbDrone.Common.Http
|
||||
request.Method = Method;
|
||||
request.Encoding = Encoding;
|
||||
request.SuppressHttpError = SuppressHttpError;
|
||||
request.SuppressHttpErrorStatusCodes = SuppressHttpErrorStatusCodes;
|
||||
request.LogHttpError = LogHttpError;
|
||||
request.UseSimplifiedUserAgent = UseSimplifiedUserAgent;
|
||||
request.AllowAutoRedirect = AllowAutoRedirect;
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NLog.Fluent;
|
||||
|
||||
namespace NzbDrone.Common.Instrumentation.Extensions
|
||||
{
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using NLog;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
|
||||
namespace NzbDrone.Common.Instrumentation
|
||||
{
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using NLog;
|
||||
using NLog.Targets;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Sentry;
|
||||
using Sentry.Protocol;
|
||||
|
||||
namespace NzbDrone.Common.Instrumentation.Sentry
|
||||
{
|
||||
|
||||
@@ -12,7 +12,6 @@ using Npgsql;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using Sentry;
|
||||
using Sentry.Protocol;
|
||||
|
||||
namespace NzbDrone.Common.Instrumentation.Sentry
|
||||
{
|
||||
|
||||
@@ -14,14 +14,14 @@ namespace NzbDrone.Common.OAuth
|
||||
{
|
||||
get
|
||||
{
|
||||
var parameters = this.Where(p => p.Name.Equals(name));
|
||||
var parameters = this.Where(p => p.Name.Equals(name)).ToArray();
|
||||
|
||||
if (!parameters.Any())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (parameters.Count() == 1)
|
||||
if (parameters.Length == 1)
|
||||
{
|
||||
return parameters.Single();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
|
||||
|
||||
@@ -131,14 +131,7 @@ namespace NzbDrone.Common.Processes
|
||||
var key = environmentVariable.Key.ToString();
|
||||
var value = environmentVariable.Value?.ToString();
|
||||
|
||||
if (startInfo.EnvironmentVariables.ContainsKey(key))
|
||||
{
|
||||
startInfo.EnvironmentVariables[key] = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
startInfo.EnvironmentVariables.Add(key, value);
|
||||
}
|
||||
startInfo.EnvironmentVariables[key] = value;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -320,7 +313,7 @@ namespace NzbDrone.Common.Processes
|
||||
processInfo = new ProcessInfo();
|
||||
processInfo.Id = process.Id;
|
||||
processInfo.Name = process.ProcessName;
|
||||
processInfo.StartPath = GetExeFileName(process);
|
||||
processInfo.StartPath = process.MainModule.FileName;
|
||||
|
||||
if (process.Id != GetCurrentProcessId() && process.HasExited)
|
||||
{
|
||||
@@ -335,16 +328,6 @@ namespace NzbDrone.Common.Processes
|
||||
return processInfo;
|
||||
}
|
||||
|
||||
private static string GetExeFileName(Process process)
|
||||
{
|
||||
if (process.MainModule.FileName != "mono.exe")
|
||||
{
|
||||
return process.MainModule.FileName;
|
||||
}
|
||||
|
||||
return process.Modules.Cast<ProcessModule>().FirstOrDefault(module => module.ModuleName.ToLower().EndsWith(".exe")).FileName;
|
||||
}
|
||||
|
||||
private List<Process> GetProcessesByName(string name)
|
||||
{
|
||||
//TODO: move this to an OS specific class
|
||||
|
||||
@@ -4,16 +4,16 @@
|
||||
<DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DryIoc.dll" Version="5.3.3" />
|
||||
<PackageReference Include="DryIoc.dll" Version="5.3.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.1" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="NLog" Version="5.1.0" />
|
||||
<PackageReference Include="NLog.Extensions.Logging" Version="5.2.0" />
|
||||
<PackageReference Include="Npgsql" Version="5.0.11" />
|
||||
<PackageReference Include="Sentry" Version="3.24.1" />
|
||||
<PackageReference Include="Sentry" Version="3.29.1" />
|
||||
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
|
||||
<PackageReference Include="SharpZipLib" Version="1.3.3" />
|
||||
<PackageReference Include="SharpZipLib" Version="1.4.2" />
|
||||
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
||||
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.1" />
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.ServiceProcess;
|
||||
using NLog;
|
||||
|
||||
826
src/NzbDrone.Core.Test/Files/Indexers/AnimeBytes/recentfeed.json
Normal file
826
src/NzbDrone.Core.Test/Files/Indexers/AnimeBytes/recentfeed.json
Normal file
@@ -0,0 +1,826 @@
|
||||
{
|
||||
"Results": 9999,
|
||||
"Pagination": {
|
||||
"Current": 1,
|
||||
"Max": 99,
|
||||
"Limit": {
|
||||
"Min": 15,
|
||||
"Coerced": 15,
|
||||
"Max": 50
|
||||
}
|
||||
},
|
||||
"Matches": 2,
|
||||
"Groups": [
|
||||
{
|
||||
"ID": 575,
|
||||
"CategoryName": "Anime",
|
||||
"FullName": "Cowboy Bebop: Tengoku no Tobira - Movie [2001]",
|
||||
"GroupName": "Movie",
|
||||
"SeriesID": "141",
|
||||
"SeriesName": "Cowboy Bebop: Tengoku no Tobira",
|
||||
"Artists": null,
|
||||
"Year": "2001",
|
||||
"Image": "https://mei.animebytes.tv/2bac1a04148be77ce41251d3cb44bbd5.jpg",
|
||||
"Synonymns": [
|
||||
"カウボーイビバップ天国の扉",
|
||||
"Cowboy Bebop: Knockin' on Heaven's Door"
|
||||
],
|
||||
"SynonymnsV2": {
|
||||
"Japanese": "カウボーイビバップ天国の扉",
|
||||
"Romaji": "",
|
||||
"Alternative": "Cowboy Bebop: Knockin' on Heaven's Door"
|
||||
},
|
||||
"Snatched": 4900,
|
||||
"Comments": 11,
|
||||
"Links": {
|
||||
"AniDB": "https://anidb.net/anime/219",
|
||||
"ANN": "https://www.animenewsnetwork.com/encyclopedia/anime.php?id=353",
|
||||
"Wikipedia": "https://en.wikipedia.org/wiki/Cowboy_Bebop:_The_Movie",
|
||||
"MAL": "https://myanimelist.net/anime/5"
|
||||
},
|
||||
"Votes": 572,
|
||||
"AvgVote": 8.3,
|
||||
"Associations": null,
|
||||
"Description": "Mars is under siege! Just before Halloween 2071, a terrorist bomb destroys a tanker truck on Highway One, close to the densely-populated crater city. There are casualties up to half a mile from the blast — 500 killed or injured by what appears to be a biochemical weapon. The reward for the bomber's capture is a massive 300,000,000 woolongs... and there are four humans and a dog who really need the money. Down on their luck as usual, the crew of the Bebop get on the case.\r\n\r\n[i]Note: The movie takes place between (in the time period of) the Cowboy Bebop episodes 22 and 23.[/i]",
|
||||
"DescriptionHTML": "Mars is under siege! Just before Halloween 2071, a terrorist bomb destroys a tanker truck on Highway One, close to the densely-populated crater city. There are casualties up to half a mile from the blast — 500 killed or injured by what appears to be a biochemical weapon. The reward for the bomber's capture is a massive 300,000,000 woolongs... and there are four humans and a dog who really need the money. Down on their luck as usual, the crew of the Bebop get on the case.<br />\r\n<br />\r\n<em>Note: The movie takes place between (in the time period of) the Cowboy Bebop episodes 22 and 23.</em>",
|
||||
"EpCount": 0,
|
||||
"StudioList": "Sunrise///28|BONES///35",
|
||||
"PastWeek": 2,
|
||||
"Incomplete": false,
|
||||
"Ongoing": false,
|
||||
"Tags": [
|
||||
"adventure",
|
||||
"comedy",
|
||||
"drama",
|
||||
"scifi",
|
||||
"seinen",
|
||||
"action"
|
||||
],
|
||||
"Torrents": [
|
||||
{
|
||||
"ID": 959397,
|
||||
"EditionData": {
|
||||
"EditionTitle": ""
|
||||
},
|
||||
"RawDownMultiplier": 0,
|
||||
"RawUpMultiplier": 1,
|
||||
"Link": "https://animebytes.tv/torrent/959397/download/somepass",
|
||||
"Property": "Blu-ray | MKV | h265 10-bit | 1080p | Opus 5.1 | Softsubs (Polarwindz) | Freeleech",
|
||||
"Snatched": 16,
|
||||
"Seeders": 5,
|
||||
"Leechers": 1,
|
||||
"Status": 0,
|
||||
"Size": 13090646841,
|
||||
"FileCount": 1,
|
||||
"FileList": [
|
||||
{
|
||||
"filename": "[Polarwindz] Cowboy Bebop The Movie - Knockin' on Heaven's Door [BD 1080p x265 10bit Opus 5.1].mkv",
|
||||
"size": 13090646841
|
||||
}
|
||||
],
|
||||
"UploadTime": "2023-04-02 05:00:43"
|
||||
},
|
||||
{
|
||||
"ID": 909565,
|
||||
"EditionData": {
|
||||
"EditionTitle": ""
|
||||
},
|
||||
"RawDownMultiplier": 0,
|
||||
"RawUpMultiplier": 1,
|
||||
"Link": "https://animebytes.tv/torrent/909565/download/somepass",
|
||||
"Property": "Blu-ray | MKV | h265 10-bit | 1080p | Opus 5.1 | Dual Audio | Softsubs (Yūrei) | Freeleech",
|
||||
"Snatched": 29,
|
||||
"Seeders": 6,
|
||||
"Leechers": 0,
|
||||
"Status": 0,
|
||||
"Size": 15717521349,
|
||||
"FileCount": 1,
|
||||
"FileList": [
|
||||
{
|
||||
"filename": "Cowboy Bebop - Tengoku no Tobira.mkv",
|
||||
"size": 15717521349
|
||||
}
|
||||
],
|
||||
"UploadTime": "2020-09-03 18:04:38"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"ID": 2709,
|
||||
"CategoryName": "Anime",
|
||||
"FullName": "BLEACH - TV Series [2004]",
|
||||
"GroupName": "TV Series",
|
||||
"SeriesID": "191",
|
||||
"SeriesName": "BLEACH",
|
||||
"Artists": null,
|
||||
"Year": "2004",
|
||||
"Image": "https://mei.animebytes.tv/997c8ec3ca0e70254b182b0a176f0161.jpg",
|
||||
"Synonymns": [
|
||||
"ブリーチ"
|
||||
],
|
||||
"SynonymnsV2": {
|
||||
"Japanese": "ブリーチ",
|
||||
"Romaji": "",
|
||||
"Alternative": ""
|
||||
},
|
||||
"Snatched": 22653,
|
||||
"Comments": 51,
|
||||
"Links": {
|
||||
"AniDB": "https://anidb.net/anime/2369",
|
||||
"ANN": "https://www.animenewsnetwork.com/encyclopedia/anime.php?id=4240",
|
||||
"Wikipedia": "https://en.wikipedia.org/wiki/Bleach_(anime)",
|
||||
"MAL": "https://myanimelist.net/anime/269"
|
||||
},
|
||||
"Votes": 530,
|
||||
"AvgVote": 7.3,
|
||||
"Associations": null,
|
||||
"Description": "Kurosaki Ichigo is a teenager gifted with the ability to see spirits. His life is drastically changed by the sudden appearance of a Shinigami (literally, Death god) - one who governs the flow of souls between the human world and the afterlife - named Kuchiki Rukia, who arrives in search of a Hollow, a dangerous lost soul. When Rukia is severely wounded while trying to defeat the Hollow, she attempts to transfer half of her Reiatsu (literally, Spiritual pressure) energy to Ichigo so that he can defeat the Hollow. However, Ichigo takes almost all of her energy, transforming into a Shinigami and allowing him to defeat the Hollow with ease. With her powers diminished, Rukia is left stranded in the human world until she can recover her strength. In the meantime, Ichigo must take over Rukia's role as a Shinigami, battling Hollows and guiding souls to the afterlife realm known as the Soul Society.",
|
||||
"DescriptionHTML": "Kurosaki Ichigo is a teenager gifted with the ability to see spirits. His life is drastically changed by the sudden appearance of a Shinigami (literally, Death god) - one who governs the flow of souls between the human world and the afterlife - named Kuchiki Rukia, who arrives in search of a Hollow, a dangerous lost soul. When Rukia is severely wounded while trying to defeat the Hollow, she attempts to transfer half of her Reiatsu (literally, Spiritual pressure) energy to Ichigo so that he can defeat the Hollow. However, Ichigo takes almost all of her energy, transforming into a Shinigami and allowing him to defeat the Hollow with ease. With her powers diminished, Rukia is left stranded in the human world until she can recover her strength. In the meantime, Ichigo must take over Rukia's role as a Shinigami, battling Hollows and guiding souls to the afterlife realm known as the Soul Society.",
|
||||
"EpCount": 366,
|
||||
"StudioList": "Studio Pierrot///45",
|
||||
"PastWeek": 26,
|
||||
"Incomplete": false,
|
||||
"Ongoing": false,
|
||||
"Tags": [
|
||||
"adventure",
|
||||
"comedy",
|
||||
"fantasy",
|
||||
"martial.arts",
|
||||
"school.life",
|
||||
"shounen",
|
||||
"super.power",
|
||||
"contemporary.fantasy",
|
||||
"swordplay",
|
||||
"action",
|
||||
"supernatural"
|
||||
],
|
||||
"Torrents": [
|
||||
{
|
||||
"ID": 1031199,
|
||||
"EditionData": {
|
||||
"EditionTitle": "Season 02: The Entry (021-041)"
|
||||
},
|
||||
"RawDownMultiplier": 0,
|
||||
"RawUpMultiplier": 1,
|
||||
"Link": "https://animebytes.tv/torrent/1031199/download/somepass",
|
||||
"Property": "Blu-ray | MKV | h265 10-bit | 1080p | AAC 2.0 | Dual Audio | Softsubs (GHOST) | Freeleech",
|
||||
"Snatched": 20,
|
||||
"Seeders": 24,
|
||||
"Leechers": 0,
|
||||
"Status": 0,
|
||||
"Size": 19584943785,
|
||||
"FileCount": 21,
|
||||
"FileList": [
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 021 [BD HEVC 10bit Dual Audio AC3][035452C3].mkv",
|
||||
"size": 880693454
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 022 [BD HEVC 10bit Dual Audio AC3][0E923AAD].mkv",
|
||||
"size": 851531918
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 023 [BD HEVC 10bit Dual Audio AC3][604A0EC6].mkv",
|
||||
"size": 882518038
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 024 [BD HEVC 10bit Dual Audio AC3][ABABE9B3].mkv",
|
||||
"size": 837335522
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 025 [BD HEVC 10bit Dual Audio AC3][17AEB0C1].mkv",
|
||||
"size": 933034706
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 026 [BD HEVC 10bit Dual Audio AC3][E6F0017C].mkv",
|
||||
"size": 891916996
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 027 [BD HEVC 10bit Dual Audio AC3][317791C7].mkv",
|
||||
"size": 896856044
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 028 [BD HEVC 10bit Dual Audio AC3][5CA4DF06].mkv",
|
||||
"size": 1010510386
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 029 [BD HEVC 10bit Dual Audio AC3][549CF25A].mkv",
|
||||
"size": 995648398
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 030 [BD HEVC 10bit Dual Audio AC3][CED479F9].mkv",
|
||||
"size": 991633973
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 031 [BD HEVC 10bit Dual Audio AC3][1EEFAB0E].mkv",
|
||||
"size": 950195341
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 032 [BD HEVC 10bit Dual Audio AC3][C6F3C39A].mkv",
|
||||
"size": 859218784
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 033 [BD HEVC 10bit Dual Audio AC3][A8424897].mkv",
|
||||
"size": 933230823
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 034 [BD HEVC 10bit Dual Audio AC3][96C94DB8].mkv",
|
||||
"size": 818179634
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 035 [BD HEVC 10bit Dual Audio AC3][A07831AE].mkv",
|
||||
"size": 882457138
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 036 [BD HEVC 10bit Dual Audio AC3][D984C169].mkv",
|
||||
"size": 895683290
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 037 [BD HEVC 10bit Dual Audio AC3][32C05A7E].mkv",
|
||||
"size": 970033716
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 038 [BD HEVC 10bit Dual Audio AC3][824B3EEC].mkv",
|
||||
"size": 956573899
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 039 [BD HEVC 10bit Dual Audio AC3][A45233DF].mkv",
|
||||
"size": 1238240707
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 040 [BD HEVC 10bit Dual Audio AC3][5334A7F1].mkv",
|
||||
"size": 894491562
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 041 [BD HEVC 10bit Dual Audio AC3][A4F17363].mkv",
|
||||
"size": 1014959456
|
||||
}
|
||||
],
|
||||
"UploadTime": "2023-03-03 03:03:17"
|
||||
},
|
||||
{
|
||||
"ID": 1031203,
|
||||
"EditionData": {
|
||||
"EditionTitle": "Season 03: The Rescue (042-063)"
|
||||
},
|
||||
"RawDownMultiplier": 0,
|
||||
"RawUpMultiplier": 1,
|
||||
"Link": "https://animebytes.tv/torrent/1031203/download/somepass",
|
||||
"Property": "Blu-ray | MKV | h265 10-bit | 1080p | AC3 2.0 | Dual Audio | Softsubs (GHOST) | Freeleech",
|
||||
"Snatched": 6,
|
||||
"Seeders": 12,
|
||||
"Leechers": 2,
|
||||
"Status": 0,
|
||||
"Size": 24498538059,
|
||||
"FileCount": 22,
|
||||
"FileList": [
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 042 [BD HEVC 10bit Dual Audio AC3][55763BF6].mkv",
|
||||
"size": 899825828
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 043 [BD HEVC 10bit Dual Audio AC3][70B71ECC].mkv",
|
||||
"size": 902256094
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 044 [BD HEVC 10bit Dual Audio AC3][35F5526B].mkv",
|
||||
"size": 885323344
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 045 [BD HEVC 10bit Dual Audio AC3][9C1DAE4E].mkv",
|
||||
"size": 1082465042
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 046 [BD HEVC 10bit Dual Audio AC3][869EF5B6].mkv",
|
||||
"size": 957433930
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 047 [BD HEVC 10bit Dual Audio AC3][890DA7CE].mkv",
|
||||
"size": 997124540
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 048 [BD HEVC 10bit Dual Audio AC3][39064E08].mkv",
|
||||
"size": 1026751383
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 049 [BD HEVC 10bit Dual Audio AC3][C536D3DB].mkv",
|
||||
"size": 994918391
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 050 [BD HEVC 10bit Dual Audio AC3][A7A0CB24].mkv",
|
||||
"size": 1141146920
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 051 [BD HEVC 10bit Dual Audio AC3][25C06D9D].mkv",
|
||||
"size": 951751791
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 052 [BD HEVC 10bit Dual Audio AC3][FB506194].mkv",
|
||||
"size": 1131756065
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 053 [BD HEVC 10bit Dual Audio AC3][4A76C66D].mkv",
|
||||
"size": 1076952986
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 054 [BD HEVC 10bit Dual Audio AC3][51D8E5F8].mkv",
|
||||
"size": 1369454462
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 055 [BD HEVC 10bit Dual Audio AC3][DCF20007].mkv",
|
||||
"size": 1428073116
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 056 [BD HEVC 10bit Dual Audio AC3][34A28687].mkv",
|
||||
"size": 1304804717
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 057 [BD HEVC 10bit Dual Audio AC3][D1D5FE29].mkv",
|
||||
"size": 1056220730
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 058 [BD HEVC 10bit Dual Audio AC3][C6EAC278].mkv",
|
||||
"size": 1551455953
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 059 [BD HEVC 10bit Dual Audio AC3][E7B25869].mkv",
|
||||
"size": 1026910923
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 060 [BD HEVC 10bit Dual Audio AC3][C9D257D4].mkv",
|
||||
"size": 1059631177
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 061 [BD HEVC 10bit Dual Audio AC3][0521C8D3].mkv",
|
||||
"size": 1457893287
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 062 [BD HEVC 10bit Dual Audio AC3][65CBA616].mkv",
|
||||
"size": 1262863946
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 063 [BD HEVC 10bit Dual Audio AC3][CF63E244].mkv",
|
||||
"size": 933523434
|
||||
}
|
||||
],
|
||||
"UploadTime": "2023-04-03 03:14:35"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"ID": 81926,
|
||||
"CategoryName": "Anime",
|
||||
"FullName": "Dr. STONE: NEW WORLD - TV Series [2023]",
|
||||
"GroupName": "TV Series",
|
||||
"SeriesID": "79217",
|
||||
"SeriesName": "Dr. STONE: NEW WORLD",
|
||||
"Artists": null,
|
||||
"Year": "2023",
|
||||
"Image": "https://mei.animebytes.tv/Tu0p0k56514.jpg",
|
||||
"Synonymns": {
|
||||
"0": "ドクターストーン NEW WORLD",
|
||||
"2": "Dr. STONE S3, Dr. STONE Season 3, Dr.STONE 3rd Season"
|
||||
},
|
||||
"SynonymnsV2": {
|
||||
"Japanese": "ドクターストーン NEW WORLD",
|
||||
"Romaji": "",
|
||||
"Alternative": "Dr. STONE S3, Dr. STONE Season 3, Dr.STONE 3rd Season"
|
||||
},
|
||||
"Snatched": 1870,
|
||||
"Comments": 0,
|
||||
"Links": {
|
||||
"AniDB": "https://anidb.net/anime/17053",
|
||||
"ANN": "https://www.animenewsnetwork.com/encyclopedia/anime.php?id=25068",
|
||||
"Wikipedia": "https://en.wikipedia.org/wiki/Dr._Stone",
|
||||
"MAL": "https://myanimelist.net/anime/48549"
|
||||
},
|
||||
"Votes": 0,
|
||||
"AvgVote": 0,
|
||||
"Associations": null,
|
||||
"Description": "Third season of [i]Dr. STONE[/i].\r\n\r\nWith the Stone Wars over, the former members of Tsukasa's Empire of Might join forces with the Kingdom of Science to build a ship capable of sailing across the open ocean to seek answers to the mystery of global petrification. However, before they can begin their voyage Senku and his friends need to find some key resources and push some new scientific advancements to build the type of vessel they need.\r\n\r\n* Based on an adventure sci-fi shounen manga series written by Inagaki Riichirou and illustrated by Boichi.\r\n\r\n[i]Note: The first episode received an early screening at a special event on March 12, 2023 at Iino Hall in Tokyo. The regular TV broadcast started on April 6, 2023.[/i]",
|
||||
"DescriptionHTML": "Third season of <em>Dr. STONE</em>.<br />\r\n<br />\r\nWith the Stone Wars over, the former members of Tsukasa's Empire of Might join forces with the Kingdom of Science to build a ship capable of sailing across the open ocean to seek answers to the mystery of global petrification. However, before they can begin their voyage Senku and his friends need to find some key resources and push some new scientific advancements to build the type of vessel they need.<br />\r\n<br />\r\n* Based on an adventure sci-fi shounen manga series written by Inagaki Riichirou and illustrated by Boichi.<br />\r\n<br />\r\n<em>Note: The first episode received an early screening at a special event on March 12, 2023 at Iino Hall in Tokyo. The regular TV broadcast started on April 6, 2023.</em>",
|
||||
"EpCount": 0,
|
||||
"StudioList": "TMS Entertainment///11",
|
||||
"PastWeek": 6,
|
||||
"Incomplete": false,
|
||||
"Ongoing": false,
|
||||
"Tags": [
|
||||
"adventure",
|
||||
"scifi",
|
||||
"shounen",
|
||||
"post.apocalyptic"
|
||||
],
|
||||
"Torrents": [
|
||||
{
|
||||
"ID": 1041495,
|
||||
"EditionData": {
|
||||
"EditionTitle": "Episode 3"
|
||||
},
|
||||
"RawDownMultiplier": 0,
|
||||
"RawUpMultiplier": 1,
|
||||
"Link": "https://animebytes.tv/torrent/1041495/download/somepass",
|
||||
"Property": "Web | MKV | h264 | 720p | AAC 2.0 | Softsubs (SubsPlease) | Episode 3 | Freeleech",
|
||||
"Snatched": 165,
|
||||
"Seeders": 137,
|
||||
"Leechers": 3,
|
||||
"Status": 0,
|
||||
"Size": 748209543,
|
||||
"FileCount": 1,
|
||||
"FileList": [
|
||||
{
|
||||
"filename": "[SubsPlease] Dr. Stone S3 - 03 (720p) [DAC92E18].mkv",
|
||||
"size": 748209543
|
||||
}
|
||||
],
|
||||
"UploadTime": "2023-04-20 14:32:29"
|
||||
},
|
||||
{
|
||||
"ID": 1037731,
|
||||
"EditionData": {
|
||||
"EditionTitle": "Episode 2"
|
||||
},
|
||||
"RawDownMultiplier": 0,
|
||||
"RawUpMultiplier": 1,
|
||||
"Link": "https://animebytes.tv/torrent/1037731/download/somepass",
|
||||
"Property": "Web | MKV | h264 | 720p | AAC 2.0 | Softsubs (SubsPlease) | Episode 2 | Freeleech",
|
||||
"Snatched": 174,
|
||||
"Seeders": 122,
|
||||
"Leechers": 1,
|
||||
"Status": 0,
|
||||
"Size": 748808730,
|
||||
"FileCount": 1,
|
||||
"FileList": [
|
||||
{
|
||||
"filename": "[SubsPlease] Dr. Stone S3 - 02 (720p) [AE2DA9AB].mkv",
|
||||
"size": 748808730
|
||||
}
|
||||
],
|
||||
"UploadTime": "2023-04-13 14:34:16"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"ID": 69267,
|
||||
"CategoryName": "Anime",
|
||||
"FullName": "Dr. STONE: STONE WARS - TV Series [2021]",
|
||||
"GroupName": "TV Series",
|
||||
"SeriesID": "67161",
|
||||
"SeriesName": "Dr. STONE: STONE WARS",
|
||||
"Artists": null,
|
||||
"Year": "2021",
|
||||
"Image": "https://mei.animebytes.tv/6pqXEK82OfD.jpg",
|
||||
"Synonymns": {
|
||||
"0": "ドクターストーン STONE WARS",
|
||||
"2": "Dr. STONE S2, Dr. STONE Season 2, Dr.STONE 2nd Season"
|
||||
},
|
||||
"SynonymnsV2": {
|
||||
"Japanese": "ドクターストーン STONE WARS",
|
||||
"Romaji": "",
|
||||
"Alternative": "Dr. STONE S2, Dr. STONE Season 2, Dr.STONE 2nd Season"
|
||||
},
|
||||
"Snatched": 1181,
|
||||
"Comments": 4,
|
||||
"Links": {
|
||||
"AniDB": "https://anidb.net/anime/15305",
|
||||
"ANN": "https://www.animenewsnetwork.com/encyclopedia/anime.php?id=22942",
|
||||
"Wikipedia": "https://en.wikipedia.org/wiki/Dr._Stone",
|
||||
"MAL": "https://myanimelist.net/anime/40852/Dr_Stone__Stone_Wars"
|
||||
},
|
||||
"Votes": 23,
|
||||
"AvgVote": 7.8,
|
||||
"Associations": null,
|
||||
"Description": "* Based on an adventure sci-fi shounen manga series written by Inagaki Riichirou and illustrated by Boichi.\r\n\r\nSenkuu, Chrome and the other villagers are in a battle of wits and brawn against the Tsukasa Empire after the revelation that Senkuu's father left behind a lasting message.\r\n\r\nWith a plan to build a phone that could be used to take down the bad guys from within, Senkuu will need to finally enlist the help of his friends who are a part of the Tsukasa Empire.",
|
||||
"DescriptionHTML": "* Based on an adventure sci-fi shounen manga series written by Inagaki Riichirou and illustrated by Boichi.<br />\r\n<br />\r\nSenkuu, Chrome and the other villagers are in a battle of wits and brawn against the Tsukasa Empire after the revelation that Senkuu's father left behind a lasting message.<br />\r\n<br />\r\nWith a plan to build a phone that could be used to take down the bad guys from within, Senkuu will need to finally enlist the help of his friends who are a part of the Tsukasa Empire.",
|
||||
"EpCount": 11,
|
||||
"StudioList": "TMS Entertainment///11",
|
||||
"PastWeek": 0,
|
||||
"Incomplete": false,
|
||||
"Ongoing": false,
|
||||
"Tags": [
|
||||
"adventure",
|
||||
"scifi",
|
||||
"shounen",
|
||||
"post.apocalyptic"
|
||||
],
|
||||
"Torrents": [
|
||||
{
|
||||
"ID": 944509,
|
||||
"EditionData": {
|
||||
"EditionTitle": ""
|
||||
},
|
||||
"RawDownMultiplier": 0,
|
||||
"RawUpMultiplier": 1,
|
||||
"Link": "https://animebytes.tv/torrent/944509/download/somepass",
|
||||
"Property": "Web | MKV | h264 | 1080p | AAC 2.0 | Dual Audio | Softsubs (-ZR-) | Freeleech",
|
||||
"Snatched": 188,
|
||||
"Seeders": 31,
|
||||
"Leechers": 1,
|
||||
"Status": 0,
|
||||
"Size": 16611719364,
|
||||
"FileCount": 11,
|
||||
"FileList": [
|
||||
{
|
||||
"filename": "Dr. Stone S02E01v2 2021 1080p WEB-DL AVC AAC 2.0 Dual Audio -ZR-.mkv",
|
||||
"size": 1512195256
|
||||
},
|
||||
{
|
||||
"filename": "Dr. Stone S02E02 2021 1080p WEB-DL AVC AAC 2.0 Dual Audio -ZR-.mkv",
|
||||
"size": 1507917714
|
||||
},
|
||||
{
|
||||
"filename": "Dr. Stone S02E03 2021 1080p WEB-DL AVC AAC 2.0 Dual Audio -ZR-.mkv",
|
||||
"size": 1510054199
|
||||
},
|
||||
{
|
||||
"filename": "Dr. Stone S02E04 2021 1080p WEB-DL AVC AAC 2.0 Dual Audio -ZR-.mkv",
|
||||
"size": 1507100461
|
||||
},
|
||||
{
|
||||
"filename": "Dr. Stone S02E05 2021 1080p WEB-DL AVC AAC 2.0 Dual Audio -ZR-.mkv",
|
||||
"size": 1507258273
|
||||
},
|
||||
{
|
||||
"filename": "Dr. Stone S02E06 2021 1080p WEB-DL AVC AAC 2.0 Dual Audio -ZR-.mkv",
|
||||
"size": 1511039711
|
||||
},
|
||||
{
|
||||
"filename": "Dr. Stone S02E07 2021 1080p WEB-DL AVC AAC 2.0 Dual Audio -ZR-.mkv",
|
||||
"size": 1507219047
|
||||
},
|
||||
{
|
||||
"filename": "Dr. Stone S02E08 2021 1080p WEB-DL AVC AAC 2.0 Dual Audio -ZR-.mkv",
|
||||
"size": 1510996213
|
||||
},
|
||||
{
|
||||
"filename": "Dr. Stone S02E09 2021 1080p WEB-DL AVC AAC 2.0 Dual Audio -ZR-.mkv",
|
||||
"size": 1512785600
|
||||
},
|
||||
{
|
||||
"filename": "Dr. Stone S02E10 2021 1080p WEB-DL AVC AAC 2.0 Dual Audio -ZR-.mkv",
|
||||
"size": 1511889715
|
||||
},
|
||||
{
|
||||
"filename": "Dr. Stone S02E11 2021 1080p WEB-DL AVC AAC 2.0 Dual Audio -ZR-.mkv",
|
||||
"size": 1513263175
|
||||
}
|
||||
],
|
||||
"UploadTime": "2021-06-03 20:30:00"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"ID": 60598,
|
||||
"CategoryName": "Anime",
|
||||
"FullName": "Dr. STONE - TV Series [2019]",
|
||||
"GroupName": "TV Series",
|
||||
"SeriesID": "56617",
|
||||
"SeriesName": "Dr. STONE",
|
||||
"Artists": null,
|
||||
"Year": "2019",
|
||||
"Image": "https://mei.animebytes.tv/SaFez5XG8T3.jpg",
|
||||
"Synonymns": [
|
||||
"Dr.STONE [ドクターストーン]"
|
||||
],
|
||||
"SynonymnsV2": {
|
||||
"Japanese": "Dr.STONE [ドクターストーン]",
|
||||
"Romaji": "",
|
||||
"Alternative": ""
|
||||
},
|
||||
"Snatched": 2174,
|
||||
"Comments": 8,
|
||||
"Links": {
|
||||
"AniDB": "https://anidb.net/anime/14491",
|
||||
"ANN": "https://www.animenewsnetwork.com/encyclopedia/anime.php?id=21703",
|
||||
"Wikipedia": "https://en.wikipedia.org/wiki/Dr._Stone",
|
||||
"MAL": "https://myanimelist.net/anime/38691"
|
||||
},
|
||||
"Votes": 68,
|
||||
"AvgVote": 7.9,
|
||||
"Associations": null,
|
||||
"Description": "Several thousand years after a mysterious phenomenon that turns all of humanity to stone, the extraordinarily intelligent, science-driven boy, Senku Ishigami, awakens. Facing a world of stone and the total collapse of civilization, Senku makes up his mind to use science to rebuild the world. Starting with his super strong childhood friend Taiju Oki, who awakened at the same time, they will begin to rebuild civilization from nothing... Depicting two million years of scientific history from the Stone Age to present day, the unprecedented crafting adventure story is about to begin!\r\n\r\n* Based on an adventure sci-fi shounen manga series written by Inagaki Riichirou and illustrated by Boichi.",
|
||||
"DescriptionHTML": "Several thousand years after a mysterious phenomenon that turns all of humanity to stone, the extraordinarily intelligent, science-driven boy, Senku Ishigami, awakens. Facing a world of stone and the total collapse of civilization, Senku makes up his mind to use science to rebuild the world. Starting with his super strong childhood friend Taiju Oki, who awakened at the same time, they will begin to rebuild civilization from nothing... Depicting two million years of scientific history from the Stone Age to present day, the unprecedented crafting adventure story is about to begin!<br />\r\n<br />\r\n* Based on an adventure sci-fi shounen manga series written by Inagaki Riichirou and illustrated by Boichi.",
|
||||
"EpCount": 24,
|
||||
"StudioList": "TMS Entertainment///11|8PAN///6344",
|
||||
"PastWeek": 0,
|
||||
"Incomplete": false,
|
||||
"Ongoing": false,
|
||||
"Tags": [
|
||||
"adventure",
|
||||
"comedy",
|
||||
"scifi",
|
||||
"shounen"
|
||||
],
|
||||
"Torrents": [
|
||||
{
|
||||
"ID": 430074,
|
||||
"EditionData": {
|
||||
"EditionTitle": ""
|
||||
},
|
||||
"RawDownMultiplier": 1,
|
||||
"RawUpMultiplier": 1,
|
||||
"Link": "https://animebytes.tv/torrent/430074/download/somepass",
|
||||
"Property": "Web | MKV | h264 | 720p | AAC 2.0 | Softsubs (HorribleSubs)",
|
||||
"Snatched": 108,
|
||||
"Seeders": 33,
|
||||
"Leechers": 1,
|
||||
"Status": 0,
|
||||
"Size": 16366224176,
|
||||
"FileCount": 24,
|
||||
"FileList": [
|
||||
{
|
||||
"filename": "[HorribleSubs] Dr. Stone - 01 [720p].mkv",
|
||||
"size": 477027555
|
||||
},
|
||||
{
|
||||
"filename": "[HorribleSubs] Dr. Stone - 02 [720p].mkv",
|
||||
"size": 489436551
|
||||
},
|
||||
{
|
||||
"filename": "[HorribleSubs] Dr. Stone - 03 [720p].mkv",
|
||||
"size": 503786828
|
||||
},
|
||||
{
|
||||
"filename": "[HorribleSubs] Dr. Stone - 04 [720p].mkv",
|
||||
"size": 442977598
|
||||
},
|
||||
{
|
||||
"filename": "[HorribleSubs] Dr. Stone - 05 [720p].mkv",
|
||||
"size": 523531555
|
||||
},
|
||||
{
|
||||
"filename": "[HorribleSubs] Dr. Stone - 06 [720p].mkv",
|
||||
"size": 506742468
|
||||
},
|
||||
{
|
||||
"filename": "[HorribleSubs] Dr. Stone - 07 [720p].mkv",
|
||||
"size": 746577276
|
||||
},
|
||||
{
|
||||
"filename": "[HorribleSubs] Dr. Stone - 08 [720p].mkv",
|
||||
"size": 745942485
|
||||
},
|
||||
{
|
||||
"filename": "[HorribleSubs] Dr. Stone - 09 [720p].mkv",
|
||||
"size": 746035250
|
||||
},
|
||||
{
|
||||
"filename": "[HorribleSubs] Dr. Stone - 10 [720p].mkv",
|
||||
"size": 746001386
|
||||
},
|
||||
{
|
||||
"filename": "[HorribleSubs] Dr. Stone - 11 [720p].mkv",
|
||||
"size": 746155088
|
||||
},
|
||||
{
|
||||
"filename": "[HorribleSubs] Dr. Stone - 12 [720p].mkv",
|
||||
"size": 746560710
|
||||
},
|
||||
{
|
||||
"filename": "[HorribleSubs] Dr. Stone - 13 [720p].mkv",
|
||||
"size": 745880614
|
||||
},
|
||||
{
|
||||
"filename": "[HorribleSubs] Dr. Stone - 14 [720p].mkv",
|
||||
"size": 744563919
|
||||
},
|
||||
{
|
||||
"filename": "[HorribleSubs] Dr. Stone - 15 [720p].mkv",
|
||||
"size": 745303312
|
||||
},
|
||||
{
|
||||
"filename": "[HorribleSubs] Dr. Stone - 16 [720p].mkv",
|
||||
"size": 746850910
|
||||
},
|
||||
{
|
||||
"filename": "[HorribleSubs] Dr. Stone - 17 [720p].mkv",
|
||||
"size": 744188496
|
||||
},
|
||||
{
|
||||
"filename": "[HorribleSubs] Dr. Stone - 18 [720p].mkv",
|
||||
"size": 746212236
|
||||
},
|
||||
{
|
||||
"filename": "[HorribleSubs] Dr. Stone - 19 [720p].mkv",
|
||||
"size": 744840131
|
||||
},
|
||||
{
|
||||
"filename": "[HorribleSubs] Dr. Stone - 20 [720p].mkv",
|
||||
"size": 746380081
|
||||
},
|
||||
{
|
||||
"filename": "[HorribleSubs] Dr. Stone - 21 [720p].mkv",
|
||||
"size": 744975636
|
||||
},
|
||||
{
|
||||
"filename": "[HorribleSubs] Dr. Stone - 22 [720p].mkv",
|
||||
"size": 746214757
|
||||
},
|
||||
{
|
||||
"filename": "[HorribleSubs] Dr. Stone - 23 [720p].mkv",
|
||||
"size": 744924693
|
||||
},
|
||||
{
|
||||
"filename": "[HorribleSubs] Dr. Stone - 24 [720p].mkv",
|
||||
"size": 745114641
|
||||
}
|
||||
],
|
||||
"UploadTime": "2019-12-13 17:02:48"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"ID": 41952,
|
||||
"CategoryName": "Anime",
|
||||
"FullName": "One Piece - TV Series [2019]",
|
||||
"GroupName": "TV Series",
|
||||
"SeriesID": "114",
|
||||
"SeriesName": "One Piece",
|
||||
"Artists": null,
|
||||
"Year": "2019",
|
||||
"Image": "https://mei.animebytes.tv/cQieN6oZ6Ft.jpg",
|
||||
"Synonymns": {
|
||||
"0": "ワンピース",
|
||||
"2": "One Piece: The Great Gold Pirate"
|
||||
},
|
||||
"SynonymnsV2": {
|
||||
"Japanese": "ワンピース",
|
||||
"Romaji": "",
|
||||
"Alternative": "One Piece: The Great Gold Pirate"
|
||||
},
|
||||
"Snatched": 100700,
|
||||
"Comments": 3,
|
||||
"Links": {
|
||||
"AniDB": "https://anidb.net/anime/69",
|
||||
"ANN": "https://www.animenewsnetwork.com/encyclopedia/anime.php?id=836",
|
||||
"Wikipedia": "https://en.wikipedia.org/wiki/One_Piece",
|
||||
"MAL": "https://myanimelist.net/anime/21/One_Piece"
|
||||
},
|
||||
"Votes": 89,
|
||||
"AvgVote": 8.8,
|
||||
"Associations": null,
|
||||
"Description": "The 20th season of One Piece. This represents episode 892 to current.",
|
||||
"DescriptionHTML": "The 20th season of One Piece. This represents episode 892 to current.",
|
||||
"EpCount": 0,
|
||||
"StudioList": null,
|
||||
"PastWeek": 10,
|
||||
"Incomplete": false,
|
||||
"Ongoing": false,
|
||||
"Tags": [
|
||||
"adventure",
|
||||
"fantasy",
|
||||
"martial.arts",
|
||||
"shounen",
|
||||
"super.power",
|
||||
"action"
|
||||
],
|
||||
"Torrents": [
|
||||
{
|
||||
"ID": 1043925,
|
||||
"EditionData": {
|
||||
"EditionTitle": "Episode 1059"
|
||||
},
|
||||
"RawDownMultiplier": 0,
|
||||
"RawUpMultiplier": 1,
|
||||
"Link": "https://animebytes.tv/torrent/1043925/download/somepass",
|
||||
"Property": "Web | MKV | h264 | 720p | AAC 2.0 | Softsubs (SubsPlease) | Episode 1059 | Freeleech",
|
||||
"Snatched": 125,
|
||||
"Seeders": 114,
|
||||
"Leechers": 1,
|
||||
"Status": 0,
|
||||
"Size": 743629489,
|
||||
"FileCount": 1,
|
||||
"FileList": [
|
||||
{
|
||||
"filename": "[SubsPlease] One Piece - 1059 (720p) [B347D9DE].mkv",
|
||||
"size": 743629489
|
||||
}
|
||||
],
|
||||
"UploadTime": "2023-04-23 02:06:08"
|
||||
},
|
||||
{
|
||||
"ID": 1039046,
|
||||
"EditionData": {
|
||||
"EditionTitle": "Episode 1058"
|
||||
},
|
||||
"RawDownMultiplier": 0,
|
||||
"RawUpMultiplier": 1,
|
||||
"Link": "https://animebytes.tv/torrent/1039046/download/somepass",
|
||||
"Property": "Web | MKV | h264 | 1080p | AAC 2.0 | Softsubs (SubsPlease) | Episode 1058 | Freeleech",
|
||||
"Snatched": 290,
|
||||
"Seeders": 232,
|
||||
"Leechers": 2,
|
||||
"Status": 0,
|
||||
"Size": 1453835224,
|
||||
"FileCount": 1,
|
||||
"FileList": [
|
||||
{
|
||||
"filename": "[SubsPlease] One Piece - 1058 (1080p) [E4094B4A].mkv",
|
||||
"size": 1453835224
|
||||
}
|
||||
],
|
||||
"UploadTime": "2023-04-16 02:07:42"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
1461
src/NzbDrone.Core.Test/Files/Indexers/Redacted/recentfeed.json
Normal file
1461
src/NzbDrone.Core.Test/Files/Indexers/Redacted/recentfeed.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common.Cloud;
|
||||
@@ -10,7 +9,6 @@ using NzbDrone.Common.Http.Proxy;
|
||||
using NzbDrone.Common.TPL;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Http;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Security;
|
||||
using NzbDrone.Test.Common;
|
||||
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.IndexerSearch;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.IndexerSearchTests
|
||||
{
|
||||
public class ReleaseSearchServiceFixture : CoreTest<ReleaseSearchService>
|
||||
{
|
||||
private Mock<IIndexer> _mockIndexer;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_mockIndexer = Mocker.GetMock<IIndexer>();
|
||||
_mockIndexer.SetupGet(s => s.Definition).Returns(new IndexerDefinition { Id = 1 });
|
||||
_mockIndexer.SetupGet(s => s.SupportsSearch).Returns(true);
|
||||
|
||||
Mocker.GetMock<IIndexerFactory>()
|
||||
.Setup(s => s.Enabled(It.IsAny<bool>()))
|
||||
.Returns(new List<IIndexer> { _mockIndexer.Object });
|
||||
}
|
||||
|
||||
private List<SearchCriteriaBase> WatchForSearchCriteria()
|
||||
{
|
||||
var result = new List<SearchCriteriaBase>();
|
||||
|
||||
_mockIndexer.Setup(v => v.Fetch(It.IsAny<MovieSearchCriteria>()))
|
||||
.Callback<MovieSearchCriteria>(s => result.Add(s))
|
||||
.Returns(Task.FromResult(new IndexerPageableQueryResult()));
|
||||
|
||||
_mockIndexer.Setup(v => v.Fetch(It.IsAny<TvSearchCriteria>()))
|
||||
.Callback<TvSearchCriteria>(s => result.Add(s))
|
||||
.Returns(Task.FromResult(new IndexerPageableQueryResult()));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
[TestCase("tt0183790", "0183790")]
|
||||
[TestCase("0183790", "0183790")]
|
||||
[TestCase("183790", "0183790")]
|
||||
[TestCase("tt10001870", "10001870")]
|
||||
[TestCase("10001870", "10001870")]
|
||||
public void should_normalize_imdbid_movie_search_criteria(string input, string expected)
|
||||
{
|
||||
var allCriteria = WatchForSearchCriteria();
|
||||
|
||||
var request = new NewznabRequest
|
||||
{
|
||||
t = "movie",
|
||||
imdbid = input
|
||||
};
|
||||
|
||||
Subject.Search(request, new List<int> { 1 }, false);
|
||||
|
||||
var criteria = allCriteria.OfType<MovieSearchCriteria>().ToList();
|
||||
|
||||
criteria.Count.Should().Be(1);
|
||||
criteria[0].ImdbId.Should().Be(expected);
|
||||
}
|
||||
|
||||
[TestCase("tt0183790", "0183790")]
|
||||
[TestCase("0183790", "0183790")]
|
||||
[TestCase("183790", "0183790")]
|
||||
[TestCase("tt10001870", "10001870")]
|
||||
[TestCase("10001870", "10001870")]
|
||||
public void should_normalize_imdbid_tv_search_criteria(string input, string expected)
|
||||
{
|
||||
var allCriteria = WatchForSearchCriteria();
|
||||
|
||||
var request = new NewznabRequest
|
||||
{
|
||||
t = "tvsearch",
|
||||
imdbid = input
|
||||
};
|
||||
|
||||
Subject.Search(request, new List<int> { 1 }, false);
|
||||
|
||||
var criteria = allCriteria.OfType<TvSearchCriteria>().ToList();
|
||||
|
||||
criteria.Count.Should().Be(1);
|
||||
criteria[0].ImdbId.Should().Be(expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,3 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Indexers.Definitions;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.IndexerTests.AnimeBytesTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class AnimeBytesFixture : CoreTest<AnimeBytes>
|
||||
{
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
Subject.Definition = new IndexerDefinition
|
||||
{
|
||||
Name = "AnimeBytes",
|
||||
Settings = new AnimeBytesSettings
|
||||
{
|
||||
BaseUrl = "https://animebytes.tv/",
|
||||
Username = "someuser",
|
||||
Passkey = "somepass"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task should_parse_recent_feed_from_animebytes()
|
||||
{
|
||||
var recentFeed = ReadAllText(@"Files/Indexers/AnimeBytes/recentfeed.json");
|
||||
|
||||
Mocker.GetMock<IIndexerHttpClient>()
|
||||
.Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get), Subject.Definition))
|
||||
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed)));
|
||||
|
||||
var releases = (await Subject.Fetch(new BasicSearchCriteria { Categories = new[] { 2000, 5000 } })).Releases;
|
||||
|
||||
releases.Should().HaveCount(33);
|
||||
releases.First().Should().BeOfType<TorrentInfo>();
|
||||
|
||||
var firstTorrentInfo = releases.ElementAt(2) as TorrentInfo;
|
||||
|
||||
firstTorrentInfo.Title.Should().Be("[SubsPlease] One Piece: The Great Gold Pirate - 1059 [Web][MKV][h264][720p][AAC 2.0][Softsubs (SubsPlease)][Episode 1059]");
|
||||
firstTorrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
||||
firstTorrentInfo.DownloadUrl.Should().Be("https://animebytes.tv/torrent/1043925/download/somepass");
|
||||
firstTorrentInfo.InfoUrl.Should().Be("https://animebytes.tv/torrent/1043925/group");
|
||||
firstTorrentInfo.Guid.Should().Be("https://animebytes.tv/torrent/1043925/group?nh=0F6BB43603CC07F4C804B9A29139F852");
|
||||
firstTorrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||
firstTorrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||
firstTorrentInfo.PublishDate.Should().Be(DateTime.Parse("2023-04-23 02:06:08", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal));
|
||||
firstTorrentInfo.Size.Should().Be(743629489);
|
||||
firstTorrentInfo.InfoHash.Should().Be(null);
|
||||
firstTorrentInfo.MagnetUrl.Should().Be(null);
|
||||
firstTorrentInfo.Peers.Should().Be(1 + 114);
|
||||
firstTorrentInfo.Seeders.Should().Be(114);
|
||||
firstTorrentInfo.Files.Should().Be(1);
|
||||
firstTorrentInfo.MinimumSeedTime.Should().Be(259200);
|
||||
|
||||
var secondTorrentInfo = releases.ElementAt(16) as TorrentInfo;
|
||||
|
||||
secondTorrentInfo.Title.Should().Be("[GHOST] BLEACH S03 [Blu-ray][MKV][h265 10-bit][1080p][AC3 2.0][Dual Audio][Softsubs (GHOST)]");
|
||||
secondTorrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
||||
secondTorrentInfo.DownloadUrl.Should().Be("https://animebytes.tv/torrent/1031203/download/somepass");
|
||||
secondTorrentInfo.InfoUrl.Should().Be("https://animebytes.tv/torrent/1031203/group");
|
||||
secondTorrentInfo.Guid.Should().Be("https://animebytes.tv/torrent/1031203/group?nh=F7C73EF631FE269D3A7F10BD12EC99A1");
|
||||
secondTorrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||
secondTorrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||
secondTorrentInfo.PublishDate.Should().Be(DateTime.Parse("2023-04-03 03:14:35", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal));
|
||||
secondTorrentInfo.Size.Should().Be(24498538059);
|
||||
secondTorrentInfo.InfoHash.Should().Be(null);
|
||||
secondTorrentInfo.MagnetUrl.Should().Be(null);
|
||||
secondTorrentInfo.Peers.Should().Be(2 + 12);
|
||||
secondTorrentInfo.Seeders.Should().Be(12);
|
||||
secondTorrentInfo.Files.Should().Be(22);
|
||||
secondTorrentInfo.MinimumSeedTime.Should().Be(655200);
|
||||
|
||||
var thirdTorrentInfo = releases.ElementAt(18) as TorrentInfo;
|
||||
|
||||
thirdTorrentInfo.Title.Should().Be("[Polarwindz] Cowboy Bebop: Tengoku no Tobira 2001 [Blu-ray][MKV][h265 10-bit][1080p][Opus 5.1][Softsubs (Polarwindz)]");
|
||||
thirdTorrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
||||
thirdTorrentInfo.DownloadUrl.Should().Be("https://animebytes.tv/torrent/959397/download/somepass");
|
||||
thirdTorrentInfo.InfoUrl.Should().Be("https://animebytes.tv/torrent/959397/group");
|
||||
thirdTorrentInfo.Guid.Should().Be("https://animebytes.tv/torrent/959397/group?nh=D63895DA87A25239C11F9823F46000E1");
|
||||
thirdTorrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||
thirdTorrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||
thirdTorrentInfo.PublishDate.Should().Be(DateTime.Parse("2023-04-02 05:00:43", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal));
|
||||
thirdTorrentInfo.Size.Should().Be(13090646841);
|
||||
thirdTorrentInfo.InfoHash.Should().Be(null);
|
||||
thirdTorrentInfo.MagnetUrl.Should().Be(null);
|
||||
thirdTorrentInfo.Peers.Should().Be(1 + 5);
|
||||
thirdTorrentInfo.Seeders.Should().Be(5);
|
||||
thirdTorrentInfo.Files.Should().Be(1);
|
||||
thirdTorrentInfo.MinimumSeedTime.Should().Be(475200);
|
||||
|
||||
var fourthTorrentInfo = releases.ElementAt(3) as TorrentInfo;
|
||||
|
||||
fourthTorrentInfo.Title.Should().Be("[SubsPlease] Dr. STONE: NEW WORLD S03E03 - 03 [Web][MKV][h264][720p][AAC 2.0][Softsubs (SubsPlease)][Episode 3]");
|
||||
fourthTorrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
||||
fourthTorrentInfo.DownloadUrl.Should().Be("https://animebytes.tv/torrent/1041495/download/somepass");
|
||||
fourthTorrentInfo.InfoUrl.Should().Be("https://animebytes.tv/torrent/1041495/group");
|
||||
fourthTorrentInfo.Guid.Should().Be("https://animebytes.tv/torrent/1041495/group?nh=8B78B0DD3BCC6068BFCD927E4AC674F6");
|
||||
fourthTorrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||
fourthTorrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||
fourthTorrentInfo.PublishDate.Should().Be(DateTime.Parse("2023-04-20 14:32:29", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal));
|
||||
fourthTorrentInfo.Size.Should().Be(748209543);
|
||||
fourthTorrentInfo.InfoHash.Should().Be(null);
|
||||
fourthTorrentInfo.MagnetUrl.Should().Be(null);
|
||||
fourthTorrentInfo.Peers.Should().Be(3 + 137);
|
||||
fourthTorrentInfo.Seeders.Should().Be(137);
|
||||
fourthTorrentInfo.Files.Should().Be(1);
|
||||
fourthTorrentInfo.MinimumSeedTime.Should().Be(259200);
|
||||
|
||||
var fifthTorrentInfo = releases.ElementAt(23) as TorrentInfo;
|
||||
|
||||
fifthTorrentInfo.Title.Should().Be("[-ZR-] Dr. STONE: STONE WARS S02 [Web][MKV][h264][1080p][AAC 2.0][Dual Audio][Softsubs (-ZR-)]");
|
||||
fifthTorrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
||||
fifthTorrentInfo.DownloadUrl.Should().Be("https://animebytes.tv/torrent/944509/download/somepass");
|
||||
fifthTorrentInfo.InfoUrl.Should().Be("https://animebytes.tv/torrent/944509/group");
|
||||
fifthTorrentInfo.Guid.Should().Be("https://animebytes.tv/torrent/944509/group?nh=FDCAA1EAB36D7C802F1E4B13DAE5EED7");
|
||||
fifthTorrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||
fifthTorrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||
fifthTorrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-06-03 20:30:00", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal));
|
||||
fifthTorrentInfo.Size.Should().Be(16611719364);
|
||||
fifthTorrentInfo.InfoHash.Should().Be(null);
|
||||
fifthTorrentInfo.MagnetUrl.Should().Be(null);
|
||||
fifthTorrentInfo.Peers.Should().Be(1 + 31);
|
||||
fifthTorrentInfo.Seeders.Should().Be(31);
|
||||
fifthTorrentInfo.Files.Should().Be(11);
|
||||
fifthTorrentInfo.MinimumSeedTime.Should().Be(529200);
|
||||
|
||||
var sixthTorrentInfo = releases.ElementAt(31) as TorrentInfo;
|
||||
|
||||
sixthTorrentInfo.Title.Should().Be("[HorribleSubs] Dr. STONE S01 [Web][MKV][h264][720p][AAC 2.0][Softsubs (HorribleSubs)]");
|
||||
sixthTorrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
||||
sixthTorrentInfo.DownloadUrl.Should().Be("https://animebytes.tv/torrent/430074/download/somepass");
|
||||
sixthTorrentInfo.InfoUrl.Should().Be("https://animebytes.tv/torrent/430074/group");
|
||||
sixthTorrentInfo.Guid.Should().Be("https://animebytes.tv/torrent/430074/group?nh=32279E138015D8718B2B4B49AEF64574");
|
||||
sixthTorrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||
sixthTorrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||
sixthTorrentInfo.PublishDate.Should().Be(DateTime.Parse("2019-12-13 17:02:48", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal));
|
||||
sixthTorrentInfo.Size.Should().Be(16366224176);
|
||||
sixthTorrentInfo.InfoHash.Should().Be(null);
|
||||
sixthTorrentInfo.MagnetUrl.Should().Be(null);
|
||||
sixthTorrentInfo.Peers.Should().Be(1 + 33);
|
||||
sixthTorrentInfo.Seeders.Should().Be(33);
|
||||
sixthTorrentInfo.Files.Should().Be(24);
|
||||
sixthTorrentInfo.MinimumSeedTime.Should().Be(529200);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,10 +22,10 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
Subject.Definition = new IndexerDefinition()
|
||||
Subject.Definition = new IndexerDefinition
|
||||
{
|
||||
Name = "AvistaZ",
|
||||
Settings = new AvistazSettings() { Username = "someuser", Password = "somepass", Pid = "somepid" }
|
||||
Settings = new AvistazSettings { Username = "someuser", Password = "somepass", Pid = "somepid" }
|
||||
};
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
|
||||
.Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get), Subject.Definition))
|
||||
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed)));
|
||||
|
||||
var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new int[] { 2000 } })).Releases;
|
||||
var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new[] { 2000 } })).Releases;
|
||||
|
||||
releases.Should().HaveCount(100);
|
||||
releases.First().Should().BeOfType<TorrentInfo>();
|
||||
@@ -51,7 +51,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
|
||||
torrentInfo.InfoUrl.Should().Be("https://avistaz.to/torrent/187240-japan-sinks-people-of-hope-2021-s01e05-720p-nf-web-dl-ddp20-x264-seikel");
|
||||
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-11-14 22:26:21"));
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-11-14 21:26:21"));
|
||||
torrentInfo.Size.Should().Be(935127615);
|
||||
torrentInfo.InfoHash.Should().Be("a879261d4e6e792402f92401141a21de70d51bf2");
|
||||
torrentInfo.MagnetUrl.Should().Be(null);
|
||||
|
||||
@@ -22,10 +22,10 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
Subject.Definition = new IndexerDefinition()
|
||||
Subject.Definition = new IndexerDefinition
|
||||
{
|
||||
Name = "ExoticaZ",
|
||||
Settings = new AvistazSettings() { Username = "someuser", Password = "somepass", Pid = "somepid" }
|
||||
Settings = new AvistazSettings { Username = "someuser", Password = "somepass", Pid = "somepid" }
|
||||
};
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
|
||||
.Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get), Subject.Definition))
|
||||
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed)));
|
||||
|
||||
var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new int[] { 2000 } })).Releases;
|
||||
var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new[] { 2000 } })).Releases;
|
||||
|
||||
releases.Should().HaveCount(100);
|
||||
releases.First().Should().BeOfType<TorrentInfo>();
|
||||
@@ -51,7 +51,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
|
||||
torrentInfo.InfoUrl.Should().Be("https://exoticaz.to/torrent/64040-ssis-419-my-first-experience-is-yua-mikami-from-the-day-i-lost-my-virginity-i-was-devoted-to-sex");
|
||||
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-06-11 16:04:50"));
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-06-11 15:04:50"));
|
||||
torrentInfo.Size.Should().Be(7085405541);
|
||||
torrentInfo.InfoHash.Should().Be("asdjfiasdf54asd7f4a2sdf544asdf");
|
||||
torrentInfo.MagnetUrl.Should().Be(null);
|
||||
|
||||
@@ -22,10 +22,10 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
Subject.Definition = new IndexerDefinition()
|
||||
Subject.Definition = new IndexerDefinition
|
||||
{
|
||||
Name = "PrivateHD",
|
||||
Settings = new AvistazSettings() { Username = "someuser", Password = "somepass", Pid = "somepid" }
|
||||
Settings = new AvistazSettings { Username = "someuser", Password = "somepass", Pid = "somepid" }
|
||||
};
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
|
||||
.Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get), Subject.Definition))
|
||||
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed)));
|
||||
|
||||
var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new int[] { 2000 } })).Releases;
|
||||
var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new[] { 2000 } })).Releases;
|
||||
|
||||
releases.Should().HaveCount(100);
|
||||
releases.First().Should().BeOfType<TorrentInfo>();
|
||||
@@ -51,7 +51,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
|
||||
torrentInfo.InfoUrl.Should().Be("https://privatehd.to/torrent/78506-godzilla-2014-2160p-uhd-bluray-remux-hdr-hevc-atmos-triton");
|
||||
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-03-21 05:24:49"));
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-03-21 04:24:49"));
|
||||
torrentInfo.Size.Should().Be(69914591044);
|
||||
torrentInfo.InfoHash.Should().Be("a879261d4e6e792402f92401141a21de70d51bf2");
|
||||
torrentInfo.MagnetUrl.Should().Be(null);
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FluentAssertions;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Indexers.BroadcastheNet;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.IndexerTests.BroadcastheNetTests
|
||||
{
|
||||
public class BroadcastheNetRequestGeneratorFixture : CoreTest<BroadcastheNetRequestGenerator>
|
||||
{
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
Subject.Settings = new BroadcastheNetSettings
|
||||
{
|
||||
BaseUrl = "https://api.broadcasthe.net/",
|
||||
ApiKey = "abc"
|
||||
};
|
||||
|
||||
Subject.Capabilities = new IndexerCapabilities
|
||||
{
|
||||
LimitsDefault = 100,
|
||||
LimitsMax = 1000,
|
||||
TvSearchParams = new List<TvSearchParam>
|
||||
{
|
||||
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep, TvSearchParam.TvdbId, TvSearchParam.RId
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_have_empty_parameters_if_rss_search()
|
||||
{
|
||||
var tvSearchCriteria = new TvSearchCriteria
|
||||
{
|
||||
Categories = new[] { NewznabStandardCategory.TV.Id, NewznabStandardCategory.TVHD.Id }
|
||||
};
|
||||
|
||||
var results = Subject.GetSearchRequests(tvSearchCriteria);
|
||||
|
||||
results.GetAllTiers().Should().HaveCount(1);
|
||||
|
||||
var page = results.GetAllTiers().First().First();
|
||||
var query = ParseTorrentQueryFromRequest(page.HttpRequest);
|
||||
|
||||
query.Tvdb.Should().BeNull();
|
||||
query.Tvrage.Should().BeNull();
|
||||
query.Search.Should().BeNullOrWhiteSpace();
|
||||
query.Category.Should().BeNullOrWhiteSpace();
|
||||
query.Name.Should().BeNullOrWhiteSpace();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_search_by_tvdbid_season_if_supported()
|
||||
{
|
||||
var tvSearchCriteria = new TvSearchCriteria
|
||||
{
|
||||
Categories = new[] { NewznabStandardCategory.TV.Id, NewznabStandardCategory.TVHD.Id },
|
||||
TvdbId = 371980,
|
||||
Season = 1
|
||||
};
|
||||
|
||||
var results = Subject.GetSearchRequests(tvSearchCriteria);
|
||||
|
||||
results.Tiers.Should().Be(1);
|
||||
results.GetAllTiers().Should().HaveCount(2);
|
||||
|
||||
var firstPage = results.GetAllTiers().First().First();
|
||||
var firstQuery = ParseTorrentQueryFromRequest(firstPage.HttpRequest);
|
||||
|
||||
firstQuery.Tvdb.Should().Be("371980");
|
||||
firstQuery.Tvrage.Should().BeNull();
|
||||
firstQuery.Search.Should().BeNull();
|
||||
firstQuery.Category.Should().Be("Season");
|
||||
firstQuery.Name.Should().Be("Season 1%");
|
||||
|
||||
var secondPage = results.GetAllTiers().Skip(1).First().First();
|
||||
var secondQuery = ParseTorrentQueryFromRequest(secondPage.HttpRequest);
|
||||
|
||||
secondQuery.Tvdb.Should().Be("371980");
|
||||
secondQuery.Tvrage.Should().BeNull();
|
||||
secondQuery.Search.Should().BeNull();
|
||||
secondQuery.Category.Should().Be("Episode");
|
||||
secondQuery.Name.Should().Be("S01E%");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_search_by_tvdbid_season_episode_if_supported()
|
||||
{
|
||||
var tvSearchCriteria = new TvSearchCriteria
|
||||
{
|
||||
Categories = new[] { NewznabStandardCategory.TV.Id, NewznabStandardCategory.TVHD.Id },
|
||||
TvdbId = 371980,
|
||||
Season = 1,
|
||||
Episode = "3"
|
||||
};
|
||||
|
||||
var results = Subject.GetSearchRequests(tvSearchCriteria);
|
||||
|
||||
results.Tiers.Should().Be(1);
|
||||
results.GetAllTiers().Should().HaveCount(1);
|
||||
|
||||
var page = results.GetAllTiers().First().First();
|
||||
var query = ParseTorrentQueryFromRequest(page.HttpRequest);
|
||||
|
||||
query.Tvdb.Should().Be("371980");
|
||||
query.Tvrage.Should().BeNull();
|
||||
query.Search.Should().BeNull();
|
||||
query.Category.Should().Be("Episode");
|
||||
query.Name.Should().Be("S01E03");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_search_by_tvdbid_daily_episode_if_supported()
|
||||
{
|
||||
var tvSearchCriteria = new TvSearchCriteria
|
||||
{
|
||||
Categories = new[] { NewznabStandardCategory.TV.Id, NewznabStandardCategory.TVHD.Id },
|
||||
TvdbId = 289574,
|
||||
Season = 2023,
|
||||
Episode = "01/03"
|
||||
};
|
||||
|
||||
var results = Subject.GetSearchRequests(tvSearchCriteria);
|
||||
|
||||
results.Tiers.Should().Be(1);
|
||||
results.GetAllTiers().Should().HaveCount(1);
|
||||
|
||||
var page = results.GetAllTiers().First().First();
|
||||
var query = ParseTorrentQueryFromRequest(page.HttpRequest);
|
||||
|
||||
query.Tvdb.Should().Be("289574");
|
||||
query.Tvrage.Should().BeNull();
|
||||
query.Search.Should().BeNull();
|
||||
query.Category.Should().Be("Episode");
|
||||
query.Name.Should().Be("2023.01.03");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_prefer_search_by_tvdbid_if_rid_supported()
|
||||
{
|
||||
var tvSearchCriteria = new TvSearchCriteria
|
||||
{
|
||||
Categories = new[] { NewznabStandardCategory.TV.Id, NewznabStandardCategory.TVHD.Id },
|
||||
TvdbId = 371980,
|
||||
RId = 12345
|
||||
};
|
||||
|
||||
var results = Subject.GetSearchRequests(tvSearchCriteria);
|
||||
|
||||
results.Tiers.Should().Be(1);
|
||||
results.GetAllTiers().Should().HaveCount(1);
|
||||
|
||||
var page = results.GetAllTiers().First().First();
|
||||
var query = ParseTorrentQueryFromRequest(page.HttpRequest);
|
||||
|
||||
query.Tvdb.Should().Be("371980");
|
||||
query.Tvrage.Should().BeNull();
|
||||
query.Search.Should().BeNull();
|
||||
query.Category.Should().BeNull();
|
||||
query.Name.Should().BeNull();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_search_by_term_supported()
|
||||
{
|
||||
var tvSearchCriteria = new TvSearchCriteria
|
||||
{
|
||||
Categories = new[] { NewznabStandardCategory.TV.Id, NewznabStandardCategory.TVHD.Id },
|
||||
SearchTerm = "Malcolm in the Middle"
|
||||
};
|
||||
|
||||
var results = Subject.GetSearchRequests(tvSearchCriteria);
|
||||
|
||||
results.Tiers.Should().Be(1);
|
||||
results.GetAllTiers().Should().HaveCount(1);
|
||||
|
||||
var page = results.GetAllTiers().First().First();
|
||||
var query = ParseTorrentQueryFromRequest(page.HttpRequest);
|
||||
|
||||
query.Tvdb.Should().BeNull();
|
||||
query.Tvrage.Should().BeNull();
|
||||
query.Search.Should().Be("Malcolm%in%the%Middle");
|
||||
query.Category.Should().BeNull();
|
||||
query.Name.Should().BeNull();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_search_by_term_season_if_supported()
|
||||
{
|
||||
var tvSearchCriteria = new TvSearchCriteria
|
||||
{
|
||||
Categories = new[] { NewznabStandardCategory.TV.Id, NewznabStandardCategory.TVHD.Id },
|
||||
SearchTerm = "Malcolm in the Middle",
|
||||
Season = 2
|
||||
};
|
||||
|
||||
var results = Subject.GetSearchRequests(tvSearchCriteria);
|
||||
|
||||
results.Tiers.Should().Be(1);
|
||||
results.GetAllTiers().Should().HaveCount(2);
|
||||
|
||||
var firstPage = results.GetAllTiers().First().First();
|
||||
var firstQuery = ParseTorrentQueryFromRequest(firstPage.HttpRequest);
|
||||
|
||||
firstQuery.Tvdb.Should().BeNull();
|
||||
firstQuery.Tvrage.Should().BeNull();
|
||||
firstQuery.Search.Should().Be("Malcolm%in%the%Middle");
|
||||
firstQuery.Category.Should().Be("Season");
|
||||
firstQuery.Name.Should().Be("Season 2%");
|
||||
|
||||
var secondPage = results.GetAllTiers().Skip(1).First().First();
|
||||
var secondQuery = ParseTorrentQueryFromRequest(secondPage.HttpRequest);
|
||||
|
||||
secondQuery.Tvdb.Should().BeNull();
|
||||
secondQuery.Tvrage.Should().BeNull();
|
||||
secondQuery.Search.Should().Be("Malcolm%in%the%Middle");
|
||||
secondQuery.Category.Should().Be("Episode");
|
||||
secondQuery.Name.Should().Be("S02E%");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_search_by_term_season_episode_if_supported()
|
||||
{
|
||||
var tvSearchCriteria = new TvSearchCriteria
|
||||
{
|
||||
Categories = new[] { NewznabStandardCategory.TV.Id, NewznabStandardCategory.TVHD.Id },
|
||||
SearchTerm = "Malcolm in the Middle",
|
||||
Season = 2,
|
||||
Episode = "3"
|
||||
};
|
||||
|
||||
var results = Subject.GetSearchRequests(tvSearchCriteria);
|
||||
|
||||
results.Tiers.Should().Be(1);
|
||||
results.GetAllTiers().Should().HaveCount(1);
|
||||
|
||||
var page = results.GetAllTiers().First().First();
|
||||
var query = ParseTorrentQueryFromRequest(page.HttpRequest);
|
||||
|
||||
query.Tvdb.Should().BeNull();
|
||||
query.Tvrage.Should().BeNull();
|
||||
query.Search.Should().Be("Malcolm%in%the%Middle");
|
||||
query.Category.Should().Be("Episode");
|
||||
query.Name.Should().Be("S02E03");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_search_by_term_daily_episode_if_supported()
|
||||
{
|
||||
var tvSearchCriteria = new TvSearchCriteria
|
||||
{
|
||||
Categories = new[] { NewznabStandardCategory.TV.Id, NewznabStandardCategory.TVHD.Id },
|
||||
SearchTerm = "The Late Show with Stephen Colbert",
|
||||
Season = 2023,
|
||||
Episode = "01/03"
|
||||
};
|
||||
|
||||
var results = Subject.GetSearchRequests(tvSearchCriteria);
|
||||
|
||||
results.Tiers.Should().Be(1);
|
||||
results.GetAllTiers().Should().HaveCount(1);
|
||||
|
||||
var page = results.GetAllTiers().First().First();
|
||||
var query = ParseTorrentQueryFromRequest(page.HttpRequest);
|
||||
|
||||
query.Tvdb.Should().BeNull();
|
||||
query.Tvrage.Should().BeNull();
|
||||
query.Search.Should().Be("The%Late%Show%with%Stephen%Colbert");
|
||||
query.Category.Should().Be("Episode");
|
||||
query.Name.Should().Be("2023.01.03");
|
||||
}
|
||||
|
||||
private static BroadcastheNetTorrentQuery ParseTorrentQueryFromRequest(HttpRequest httpRequest)
|
||||
{
|
||||
var encoding = HttpHeader.GetEncodingFromContentType(httpRequest.Headers.ContentType);
|
||||
var body = encoding.GetString(httpRequest.ContentData);
|
||||
|
||||
var rpcBody = JsonConvert.DeserializeObject<Dictionary<string, object>>(body);
|
||||
|
||||
return JsonConvert.DeserializeObject<BroadcastheNetTorrentQuery>(((JArray)rpcBody["params"])[1].ToJson());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ using System.Collections.Generic;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Indexers.Cardigann;
|
||||
using NzbDrone.Core.Indexers.Definitions.Cardigann;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.IndexerTests.CardigannTests
|
||||
|
||||
@@ -55,7 +55,7 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
|
||||
torrentInfo.InfoUrl.Should().Be("https://filelist.io/details.php?id=665873");
|
||||
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2020-01-25 20:20:19"));
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2020-01-25 19:20:19"));
|
||||
torrentInfo.Size.Should().Be(8300512414);
|
||||
torrentInfo.InfoHash.Should().Be(null);
|
||||
torrentInfo.MagnetUrl.Should().Be(null);
|
||||
|
||||
@@ -12,7 +12,6 @@ using NzbDrone.Core.Indexers.Definitions;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Test.Common;
|
||||
|
||||
namespace NzbDrone.Core.Test.IndexerTests.GazelleGamesTests
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user