mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2026-04-18 21:55:12 -04:00
Compare commits
132 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5e15054329 | |||
| 3dfbfd07dd | |||
| 842df6913c | |||
| 599eeb4c61 | |||
| da371dd921 | |||
| fc25ba7ac0 | |||
| 6e1bef13e2 | |||
| 72ee413411 | |||
| e87b45b47e | |||
| cc841fe3d1 | |||
| 264ffdcc26 | |||
| 5cc044aa8f | |||
| de2fd92b6f | |||
| eff09c1f72 | |||
| 9db888c9a3 | |||
| bf78396164 | |||
| 0e7eaa9221 | |||
| 5b82decc31 | |||
| 38ab533272 | |||
| 4914fcd5df | |||
| 858415b037 | |||
| 43f4899324 | |||
| c60a94adfb | |||
| f386ddb806 | |||
| 4175c2577e | |||
| 6ce9e5ceb9 | |||
| c15643be39 | |||
| a58380031d | |||
| 73af5c9a72 | |||
| d556545e7f | |||
| affde5d7b7 | |||
| 518c85dee2 | |||
| ba3a240707 | |||
| 587a73f3d6 | |||
| ae8f017ca8 | |||
| d9098b612e | |||
| 29e7cc06a1 | |||
| 387fb0bd15 | |||
| 2d33560d89 | |||
| 94a797fc1e | |||
| 2e851b0588 | |||
| 7303cdf555 | |||
| 6636cbc4ae | |||
| a5a4f62f25 | |||
| 05a7465a07 | |||
| c35f1212fb | |||
| ad95d73e9d | |||
| 30f53c20ed | |||
| 0199a37a0c | |||
| e9764820c0 | |||
| d285cbb021 | |||
| 8afaa3386d | |||
| c94beb6814 | |||
| c7eb08a0f0 | |||
| 2a2e859420 | |||
| 31f0e8212e | |||
| 1cbb9b1724 | |||
| 45dbcc6b89 | |||
| 3b26613394 | |||
| 6bb8c09fcf | |||
| 810b3612aa | |||
| 57dcd861a9 | |||
| dfe132cda2 | |||
| a635820b48 | |||
| d959e81efb | |||
| ac89cd636f | |||
| 50616f5c9e | |||
| 3f9cb2c6ea | |||
| b5aa85a548 | |||
| 0fa5127c83 | |||
| 4d137886bc | |||
| 9dde041c99 | |||
| a8234c9ce0 | |||
| 9227efdb65 | |||
| fa923e658f | |||
| 364a5564ae | |||
| 9efd0b391e | |||
| 320161e051 | |||
| 38ba810ae8 | |||
| 4e3f460a24 | |||
| 0d918a0aa9 | |||
| a110412665 | |||
| 6c97f1b6ee | |||
| 470779ead2 | |||
| b371f2d913 | |||
| 3ff3452e2d | |||
| df13537e29 | |||
| 5d2fefde8f | |||
| ffb3f83324 | |||
| 1c125733b2 | |||
| 2af7fac15e | |||
| f172d17ecc | |||
| c69843931e | |||
| cd3e99ad87 | |||
| 1cce39b404 | |||
| 9b46ab73e4 | |||
| a352c053ab | |||
| b33e45d266 | |||
| 817d61de91 | |||
| c7e5cc6462 | |||
| 25596fc2e8 | |||
| 9ff0b90626 | |||
| 4f4c011436 | |||
| bd0115931f | |||
| a0d18c546e | |||
| d935b0df82 | |||
| 9e37f69224 | |||
| 2805c4f18b | |||
| dae21f22b9 | |||
| 7ddbe09eca | |||
| 90e3c809c3 | |||
| ec8cf5f57a | |||
| f4bbf2f8af | |||
| ea98d41472 | |||
| b8cb0fd291 | |||
| d3dfa620ac | |||
| 049668f307 | |||
| c400575aac | |||
| 6f122fb2e4 | |||
| a9c210f8e7 | |||
| 1068ba8915 | |||
| 635335d876 | |||
| 2ed51cd933 | |||
| b74c46c554 | |||
| 7029e0d6ee | |||
| 438ea380f5 | |||
| 4eec675d61 | |||
| 0a9bd8287f | |||
| b583ac3a97 | |||
| 4be41ff3fb | |||
| b911f8cc08 | |||
| 22face385f |
@@ -5,9 +5,9 @@ body:
|
|||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
attributes:
|
attributes:
|
||||||
label: Is there an existing issue for this?
|
label: Is there an existing issue for this?
|
||||||
description: Please search to see if an issue already exists for the bug you encountered.
|
description: Please search to see if an open or closed issue already exists for the bug you encountered. If a bug exists and is closed note that it may only be fixed in an unstable branch.
|
||||||
options:
|
options:
|
||||||
- label: I have searched the existing issues
|
- label: I have searched the existing open and closed issues
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ body:
|
|||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
attributes:
|
attributes:
|
||||||
label: Is there an existing issue for this?
|
label: Is there an existing issue for this?
|
||||||
description: Please search to see if an issue already exists for the feature you are requesting.
|
description: Please search to see if an open or closed issue already exists for the feature you are requesting. If a request exists and is closed note that it may only be fixed in an unstable branch.
|
||||||
options:
|
options:
|
||||||
- label: I have searched the existing issues
|
- label: I have searched the existing open and closed issues
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
|
nationality, personal appearance, race, caste, color, religion, or sexual
|
||||||
|
identity and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
|
diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to a positive environment for our
|
||||||
|
community include:
|
||||||
|
|
||||||
|
* Demonstrating empathy and kindness toward other people
|
||||||
|
* Being respectful of differing opinions, viewpoints, and experiences
|
||||||
|
* Giving and gracefully accepting constructive feedback
|
||||||
|
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
|
and learning from the experience
|
||||||
|
* Focusing on what is best not just for us as individuals, but for the overall
|
||||||
|
community
|
||||||
|
|
||||||
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery, and sexual attention or advances of
|
||||||
|
any kind
|
||||||
|
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or email address,
|
||||||
|
without their explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
|
Community leaders are responsible for clarifying and enforcing our standards of
|
||||||
|
acceptable behavior and will take appropriate and fair corrective action in
|
||||||
|
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||||
|
or harmful.
|
||||||
|
|
||||||
|
Community leaders have the right and responsibility to remove, edit, or reject
|
||||||
|
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||||
|
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||||
|
decisions when appropriate.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all community spaces, and also applies when
|
||||||
|
an individual is officially representing the community in public spaces.
|
||||||
|
Examples of representing our community include using an official e-mail address,
|
||||||
|
posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported to the community leaders responsible for enforcement at
|
||||||
|
<development@prowlarr.com>.
|
||||||
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
|
reporter of any incident.
|
||||||
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Community leaders will follow these Community Impact Guidelines in determining
|
||||||
|
the consequences for any action they deem in violation of this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||||
|
unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning from community leaders, providing
|
||||||
|
clarity around the nature of the violation and an explanation of why the
|
||||||
|
behavior was inappropriate. A public apology may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Community Impact**: A violation through a single incident or series of
|
||||||
|
actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued behavior. No
|
||||||
|
interaction with the people involved, including unsolicited interaction with
|
||||||
|
those enforcing the Code of Conduct, for a specified period of time. This
|
||||||
|
includes avoiding interactions in community spaces as well as external channels
|
||||||
|
like social media. Violating these terms may lead to a temporary or permanent
|
||||||
|
ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Community Impact**: A serious violation of community standards, including
|
||||||
|
sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or public
|
||||||
|
communication with the community for a specified period of time. No public or
|
||||||
|
private interaction with the people involved, including unsolicited interaction
|
||||||
|
with those enforcing the Code of Conduct, is allowed during this period.
|
||||||
|
Violating these terms may lead to a permanent ban.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction within the
|
||||||
|
community.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
|
version 2.1, available at
|
||||||
|
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||||
|
|
||||||
|
Community Impact Guidelines were inspired by
|
||||||
|
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
|
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
|
||||||
|
[https://www.contributor-covenant.org/translations][translations].
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||||
|
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||||
|
[FAQ]: https://www.contributor-covenant.org/faq
|
||||||
|
[translations]: https://www.contributor-covenant.org/translations
|
||||||
+4
-3
@@ -9,13 +9,13 @@ variables:
|
|||||||
testsFolder: './_tests'
|
testsFolder: './_tests'
|
||||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||||
majorVersion: '0.4.7'
|
majorVersion: '1.1.0'
|
||||||
minorVersion: $[counter('minorVersion', 1)]
|
minorVersion: $[counter('minorVersion', 1)]
|
||||||
prowlarrVersion: '$(majorVersion).$(minorVersion)'
|
prowlarrVersion: '$(majorVersion).$(minorVersion)'
|
||||||
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
|
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
|
||||||
sentryOrg: 'servarr'
|
sentryOrg: 'servarr'
|
||||||
sentryUrl: 'https://sentry.servarr.com'
|
sentryUrl: 'https://sentry.servarr.com'
|
||||||
dotnetVersion: '6.0.301'
|
dotnetVersion: '6.0.404'
|
||||||
innoVersion: '6.2.0'
|
innoVersion: '6.2.0'
|
||||||
nodeVersion: '16.x'
|
nodeVersion: '16.x'
|
||||||
windowsImage: 'windows-2022'
|
windowsImage: 'windows-2022'
|
||||||
@@ -748,7 +748,7 @@ stages:
|
|||||||
inputs:
|
inputs:
|
||||||
buildType: 'current'
|
buildType: 'current'
|
||||||
artifactName: Packages
|
artifactName: Packages
|
||||||
itemPattern: '/$(pattern)'
|
itemPattern: '**/$(pattern)'
|
||||||
targetPath: $(Build.ArtifactStagingDirectory)
|
targetPath: $(Build.ArtifactStagingDirectory)
|
||||||
- bash: |
|
- bash: |
|
||||||
mkdir -p ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin
|
mkdir -p ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin
|
||||||
@@ -1108,4 +1108,5 @@ stages:
|
|||||||
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
|
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
|
||||||
DISCORDCHANNELID: $(discordChannelId)
|
DISCORDCHANNELID: $(discordChannelId)
|
||||||
DISCORDWEBHOOKKEY: $(discordWebhookKey)
|
DISCORDWEBHOOKKEY: $(discordWebhookKey)
|
||||||
|
DISCORDTHREADID: $(discordThreadId)
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import FormInputHelpText from './FormInputHelpText';
|
|||||||
import IndexerFlagsSelectInputConnector from './IndexerFlagsSelectInputConnector';
|
import IndexerFlagsSelectInputConnector from './IndexerFlagsSelectInputConnector';
|
||||||
import InfoInput from './InfoInput';
|
import InfoInput from './InfoInput';
|
||||||
import KeyValueListInput from './KeyValueListInput';
|
import KeyValueListInput from './KeyValueListInput';
|
||||||
|
import NewznabCategorySelectInputConnector from './NewznabCategorySelectInputConnector';
|
||||||
import NumberInput from './NumberInput';
|
import NumberInput from './NumberInput';
|
||||||
import OAuthInputConnector from './OAuthInputConnector';
|
import OAuthInputConnector from './OAuthInputConnector';
|
||||||
import PasswordInput from './PasswordInput';
|
import PasswordInput from './PasswordInput';
|
||||||
@@ -68,6 +69,9 @@ function getComponent(type) {
|
|||||||
case inputTypes.PATH:
|
case inputTypes.PATH:
|
||||||
return PathInputConnector;
|
return PathInputConnector;
|
||||||
|
|
||||||
|
case inputTypes.CATEGORY_SELECT:
|
||||||
|
return NewznabCategorySelectInputConnector;
|
||||||
|
|
||||||
case inputTypes.INDEXER_FLAGS_SELECT:
|
case inputTypes.INDEXER_FLAGS_SELECT:
|
||||||
return IndexerFlagsSelectInputConnector;
|
return IndexerFlagsSelectInputConnector;
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ function createMapStateToProps() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
value,
|
value: value || [],
|
||||||
values
|
values
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ class TagInputInput extends Component {
|
|||||||
<div
|
<div
|
||||||
ref={forwardedRef}
|
ref={forwardedRef}
|
||||||
className={className}
|
className={className}
|
||||||
component="div"
|
|
||||||
onMouseDown={this.onMouseDown}
|
onMouseDown={this.onMouseDown}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -36,5 +36,5 @@
|
|||||||
/** Outline **/
|
/** Outline **/
|
||||||
|
|
||||||
.outline {
|
.outline {
|
||||||
background-color: var(--white);
|
background-color: var(--cardBackgroundColor);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,5 +108,5 @@
|
|||||||
/** Outline **/
|
/** Outline **/
|
||||||
|
|
||||||
.outline {
|
.outline {
|
||||||
background-color: var(--white);
|
background-color: var(--cardBackgroundColor);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--toobarButtonHoverColor);
|
color: #515253;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import AppUpdatedModalConnector from 'App/AppUpdatedModalConnector';
|
|||||||
import ColorImpairedContext from 'App/ColorImpairedContext';
|
import ColorImpairedContext from 'App/ColorImpairedContext';
|
||||||
import ConnectionLostModalConnector from 'App/ConnectionLostModalConnector';
|
import ConnectionLostModalConnector from 'App/ConnectionLostModalConnector';
|
||||||
import SignalRConnector from 'Components/SignalRConnector';
|
import SignalRConnector from 'Components/SignalRConnector';
|
||||||
|
import AuthenticationRequiredModal from 'FirstRun/AuthenticationRequiredModal';
|
||||||
import locationShape from 'Helpers/Props/Shapes/locationShape';
|
import locationShape from 'Helpers/Props/Shapes/locationShape';
|
||||||
import PageHeader from './Header/PageHeader';
|
import PageHeader from './Header/PageHeader';
|
||||||
import PageSidebar from './Sidebar/PageSidebar';
|
import PageSidebar from './Sidebar/PageSidebar';
|
||||||
@@ -75,6 +76,7 @@ class Page extends Component {
|
|||||||
isSmallScreen,
|
isSmallScreen,
|
||||||
isSidebarVisible,
|
isSidebarVisible,
|
||||||
enableColorImpairedMode,
|
enableColorImpairedMode,
|
||||||
|
authenticationEnabled,
|
||||||
onSidebarToggle,
|
onSidebarToggle,
|
||||||
onSidebarVisibleChange
|
onSidebarVisibleChange
|
||||||
} = this.props;
|
} = this.props;
|
||||||
@@ -109,6 +111,10 @@ class Page extends Component {
|
|||||||
isOpen={this.state.isConnectionLostModalOpen}
|
isOpen={this.state.isConnectionLostModalOpen}
|
||||||
onModalClose={this.onConnectionLostModalClose}
|
onModalClose={this.onConnectionLostModalClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<AuthenticationRequiredModal
|
||||||
|
isOpen={!authenticationEnabled}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ColorImpairedContext.Provider>
|
</ColorImpairedContext.Provider>
|
||||||
);
|
);
|
||||||
@@ -124,6 +130,7 @@ Page.propTypes = {
|
|||||||
isUpdated: PropTypes.bool.isRequired,
|
isUpdated: PropTypes.bool.isRequired,
|
||||||
isDisconnected: PropTypes.bool.isRequired,
|
isDisconnected: PropTypes.bool.isRequired,
|
||||||
enableColorImpairedMode: PropTypes.bool.isRequired,
|
enableColorImpairedMode: PropTypes.bool.isRequired,
|
||||||
|
authenticationEnabled: PropTypes.bool.isRequired,
|
||||||
onResize: PropTypes.func.isRequired,
|
onResize: PropTypes.func.isRequired,
|
||||||
onSidebarToggle: PropTypes.func.isRequired,
|
onSidebarToggle: PropTypes.func.isRequired,
|
||||||
onSidebarVisibleChange: PropTypes.func.isRequired
|
onSidebarVisibleChange: PropTypes.func.isRequired
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { fetchAppProfiles, fetchGeneralSettings, fetchIndexerCategories, fetchUI
|
|||||||
import { fetchStatus } from 'Store/Actions/systemActions';
|
import { fetchStatus } from 'Store/Actions/systemActions';
|
||||||
import { fetchTags } from 'Store/Actions/tagActions';
|
import { fetchTags } from 'Store/Actions/tagActions';
|
||||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
|
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
||||||
import ErrorPage from './ErrorPage';
|
import ErrorPage from './ErrorPage';
|
||||||
import LoadingPage from './LoadingPage';
|
import LoadingPage from './LoadingPage';
|
||||||
import Page from './Page';
|
import Page from './Page';
|
||||||
@@ -133,18 +134,21 @@ function createMapStateToProps() {
|
|||||||
selectErrors,
|
selectErrors,
|
||||||
selectAppProps,
|
selectAppProps,
|
||||||
createDimensionsSelector(),
|
createDimensionsSelector(),
|
||||||
|
createSystemStatusSelector(),
|
||||||
(
|
(
|
||||||
enableColorImpairedMode,
|
enableColorImpairedMode,
|
||||||
isPopulated,
|
isPopulated,
|
||||||
errors,
|
errors,
|
||||||
app,
|
app,
|
||||||
dimensions
|
dimensions,
|
||||||
|
systemStatus
|
||||||
) => {
|
) => {
|
||||||
return {
|
return {
|
||||||
...app,
|
...app,
|
||||||
...errors,
|
...errors,
|
||||||
isPopulated,
|
isPopulated,
|
||||||
isSmallScreen: dimensions.isSmallScreen,
|
isSmallScreen: dimensions.isSmallScreen,
|
||||||
|
authenticationEnabled: systemStatus.authentication !== 'none',
|
||||||
enableColorImpairedMode
|
enableColorImpairedMode
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import { sizes } from 'Helpers/Props';
|
||||||
|
import AuthenticationRequiredModalContentConnector from './AuthenticationRequiredModalContentConnector';
|
||||||
|
|
||||||
|
function onModalClose() {
|
||||||
|
// No-op
|
||||||
|
}
|
||||||
|
|
||||||
|
function AuthenticationRequiredModal(props) {
|
||||||
|
const {
|
||||||
|
isOpen
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
size={sizes.MEDIUM}
|
||||||
|
isOpen={isOpen}
|
||||||
|
closeOnBackgroundClick={false}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
>
|
||||||
|
<AuthenticationRequiredModalContentConnector
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthenticationRequiredModal.propTypes = {
|
||||||
|
isOpen: PropTypes.bool.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AuthenticationRequiredModal;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
.authRequiredAlert {
|
||||||
|
composes: alert from '~Components/Alert.css';
|
||||||
|
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { useEffect, useRef } from 'react';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import { inputTypes, kinds } from 'Helpers/Props';
|
||||||
|
import { authenticationMethodOptions, authenticationRequiredOptions, authenticationRequiredWarning } from 'Settings/General/SecuritySettings';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './AuthenticationRequiredModalContent.css';
|
||||||
|
|
||||||
|
function onModalClose() {
|
||||||
|
// No-op
|
||||||
|
}
|
||||||
|
|
||||||
|
function AuthenticationRequiredModalContent(props) {
|
||||||
|
const {
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
isSaving,
|
||||||
|
settings,
|
||||||
|
onInputChange,
|
||||||
|
onSavePress,
|
||||||
|
dispatchFetchStatus
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
authenticationMethod,
|
||||||
|
authenticationRequired,
|
||||||
|
username,
|
||||||
|
password
|
||||||
|
} = settings;
|
||||||
|
|
||||||
|
const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none';
|
||||||
|
|
||||||
|
const didMount = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isSaving && didMount.current) {
|
||||||
|
dispatchFetchStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
didMount.current = true;
|
||||||
|
}, [isSaving, dispatchFetchStatus]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent
|
||||||
|
showCloseButton={false}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
>
|
||||||
|
<ModalHeader>
|
||||||
|
{translate('AuthenticationRequired')}
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<Alert
|
||||||
|
className={styles.authRequiredAlert}
|
||||||
|
kind={kinds.WARNING}
|
||||||
|
>
|
||||||
|
{authenticationRequiredWarning}
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
{
|
||||||
|
isPopulated && !error ?
|
||||||
|
<div>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('Authentication')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="authenticationMethod"
|
||||||
|
values={authenticationMethodOptions}
|
||||||
|
helpText={translate('AuthenticationMethodHelpText')}
|
||||||
|
onChange={onInputChange}
|
||||||
|
{...authenticationMethod}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
{
|
||||||
|
authenticationEnabled ?
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('AuthenticationRequired')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="authenticationRequired"
|
||||||
|
values={authenticationRequiredOptions}
|
||||||
|
helpText={translate('AuthenticationRequiredHelpText')}
|
||||||
|
onChange={onInputChange}
|
||||||
|
{...authenticationRequired}
|
||||||
|
/>
|
||||||
|
</FormGroup> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
authenticationEnabled ?
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('Username')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.TEXT}
|
||||||
|
name="username"
|
||||||
|
onChange={onInputChange}
|
||||||
|
{...username}
|
||||||
|
/>
|
||||||
|
</FormGroup> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
authenticationEnabled ?
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('Password')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.PASSWORD}
|
||||||
|
name="password"
|
||||||
|
onChange={onInputChange}
|
||||||
|
{...password}
|
||||||
|
/>
|
||||||
|
</FormGroup> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
</div> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!isPopulated && !error ? <LoadingIndicator /> : null
|
||||||
|
}
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<SpinnerButton
|
||||||
|
kind={kinds.PRIMARY}
|
||||||
|
isSpinning={isSaving}
|
||||||
|
isDisabled={!authenticationEnabled}
|
||||||
|
onPress={onSavePress}
|
||||||
|
>
|
||||||
|
{translate('Save')}
|
||||||
|
</SpinnerButton>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthenticationRequiredModalContent.propTypes = {
|
||||||
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
|
error: PropTypes.object,
|
||||||
|
isSaving: PropTypes.bool.isRequired,
|
||||||
|
saveError: PropTypes.object,
|
||||||
|
settings: PropTypes.object.isRequired,
|
||||||
|
onInputChange: PropTypes.func.isRequired,
|
||||||
|
onSavePress: PropTypes.func.isRequired,
|
||||||
|
dispatchFetchStatus: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AuthenticationRequiredModalContent;
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||||
|
import { fetchGeneralSettings, saveGeneralSettings, setGeneralSettingsValue } from 'Store/Actions/settingsActions';
|
||||||
|
import { fetchStatus } from 'Store/Actions/systemActions';
|
||||||
|
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
|
||||||
|
import AuthenticationRequiredModalContent from './AuthenticationRequiredModalContent';
|
||||||
|
|
||||||
|
const SECTION = 'general';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
createSettingsSectionSelector(SECTION),
|
||||||
|
(sectionSettings) => {
|
||||||
|
return {
|
||||||
|
...sectionSettings
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
dispatchClearPendingChanges: clearPendingChanges,
|
||||||
|
dispatchSetGeneralSettingsValue: setGeneralSettingsValue,
|
||||||
|
dispatchSaveGeneralSettings: saveGeneralSettings,
|
||||||
|
dispatchFetchGeneralSettings: fetchGeneralSettings,
|
||||||
|
dispatchFetchStatus: fetchStatus
|
||||||
|
};
|
||||||
|
|
||||||
|
class AuthenticationRequiredModalContentConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.dispatchFetchGeneralSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.props.dispatchClearPendingChanges({ section: `settings.${SECTION}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onInputChange = ({ name, value }) => {
|
||||||
|
this.props.dispatchSetGeneralSettingsValue({ name, value });
|
||||||
|
};
|
||||||
|
|
||||||
|
onSavePress = () => {
|
||||||
|
this.props.dispatchSaveGeneralSettings();
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
dispatchClearPendingChanges,
|
||||||
|
dispatchFetchGeneralSettings,
|
||||||
|
dispatchSetGeneralSettingsValue,
|
||||||
|
dispatchSaveGeneralSettings,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthenticationRequiredModalContent
|
||||||
|
{...otherProps}
|
||||||
|
onInputChange={this.onInputChange}
|
||||||
|
onSavePress={this.onSavePress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthenticationRequiredModalContentConnector.propTypes = {
|
||||||
|
dispatchClearPendingChanges: PropTypes.func.isRequired,
|
||||||
|
dispatchFetchGeneralSettings: PropTypes.func.isRequired,
|
||||||
|
dispatchSetGeneralSettingsValue: PropTypes.func.isRequired,
|
||||||
|
dispatchSaveGeneralSettings: PropTypes.func.isRequired,
|
||||||
|
dispatchFetchStatus: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(AuthenticationRequiredModalContentConnector);
|
||||||
@@ -8,6 +8,7 @@ export const DEVICE = 'device';
|
|||||||
export const KEY_VALUE_LIST = 'keyValueList';
|
export const KEY_VALUE_LIST = 'keyValueList';
|
||||||
export const INFO = 'info';
|
export const INFO = 'info';
|
||||||
export const MOVIE_MONITORED_SELECT = 'movieMonitoredSelect';
|
export const MOVIE_MONITORED_SELECT = 'movieMonitoredSelect';
|
||||||
|
export const CATEGORY_SELECT = 'newznabCategorySelect';
|
||||||
export const NUMBER = 'number';
|
export const NUMBER = 'number';
|
||||||
export const OAUTH = 'oauth';
|
export const OAUTH = 'oauth';
|
||||||
export const PASSWORD = 'password';
|
export const PASSWORD = 'password';
|
||||||
@@ -32,6 +33,7 @@ export const all = [
|
|||||||
KEY_VALUE_LIST,
|
KEY_VALUE_LIST,
|
||||||
INFO,
|
INFO,
|
||||||
MOVIE_MONITORED_SELECT,
|
MOVIE_MONITORED_SELECT,
|
||||||
|
CATEGORY_SELECT,
|
||||||
NUMBER,
|
NUMBER,
|
||||||
OAUTH,
|
OAUTH,
|
||||||
PASSWORD,
|
PASSWORD,
|
||||||
|
|||||||
@@ -226,6 +226,42 @@ class HistoryRow extends Component {
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
data.label ?
|
||||||
|
<HistoryRowParameter
|
||||||
|
title='Label'
|
||||||
|
value={data.label}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
data.track ?
|
||||||
|
<HistoryRowParameter
|
||||||
|
title='Track'
|
||||||
|
value={data.track}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
data.year ?
|
||||||
|
<HistoryRowParameter
|
||||||
|
title='Year'
|
||||||
|
value={data.year}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
data.genre ?
|
||||||
|
<HistoryRowParameter
|
||||||
|
title='Genre'
|
||||||
|
value={data.genre}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
data.author ?
|
data.author ?
|
||||||
<HistoryRowParameter
|
<HistoryRowParameter
|
||||||
@@ -243,6 +279,15 @@ class HistoryRow extends Component {
|
|||||||
/> :
|
/> :
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
data.publisher ?
|
||||||
|
<HistoryRowParameter
|
||||||
|
title='Publisher'
|
||||||
|
value={data.publisher}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ class AddIndexerModalContent extends Component {
|
|||||||
const filteredIndexers = indexers.filter((indexer) => {
|
const filteredIndexers = indexers.filter((indexer) => {
|
||||||
const { filter, filterProtocols, filterLanguages, filterPrivacyLevels } = this.state;
|
const { filter, filterProtocols, filterLanguages, filterPrivacyLevels } = this.state;
|
||||||
|
|
||||||
if (!indexer.name.toLowerCase().includes(filter.toLocaleLowerCase())) {
|
if (!indexer.name.toLowerCase().includes(filter.toLocaleLowerCase()) && !indexer.description.toLowerCase().includes(filter.toLocaleLowerCase())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -272,6 +272,7 @@ class IndexerIndex extends Component {
|
|||||||
saveError,
|
saveError,
|
||||||
isDeleting,
|
isDeleting,
|
||||||
isTestingAll,
|
isTestingAll,
|
||||||
|
isSyncingIndexers,
|
||||||
deleteError,
|
deleteError,
|
||||||
onScroll,
|
onScroll,
|
||||||
onSortSelect,
|
onSortSelect,
|
||||||
@@ -309,6 +310,15 @@ class IndexerIndex extends Component {
|
|||||||
onPress={this.onAddIndexerPress}
|
onPress={this.onAddIndexerPress}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<PageToolbarSeparator />
|
||||||
|
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('SyncAppIndexers')}
|
||||||
|
iconName={icons.REFRESH}
|
||||||
|
isSpinning={isSyncingIndexers}
|
||||||
|
onPress={this.props.onAppIndexerSyncPress}
|
||||||
|
/>
|
||||||
|
|
||||||
<PageToolbarButton
|
<PageToolbarButton
|
||||||
label={translate('TestAllIndexers')}
|
label={translate('TestAllIndexers')}
|
||||||
iconName={icons.TEST}
|
iconName={icons.TEST}
|
||||||
@@ -493,10 +503,12 @@ IndexerIndex.propTypes = {
|
|||||||
saveError: PropTypes.object,
|
saveError: PropTypes.object,
|
||||||
isDeleting: PropTypes.bool.isRequired,
|
isDeleting: PropTypes.bool.isRequired,
|
||||||
isTestingAll: PropTypes.bool.isRequired,
|
isTestingAll: PropTypes.bool.isRequired,
|
||||||
|
isSyncingIndexers: PropTypes.bool.isRequired,
|
||||||
deleteError: PropTypes.object,
|
deleteError: PropTypes.object,
|
||||||
onSortSelect: PropTypes.func.isRequired,
|
onSortSelect: PropTypes.func.isRequired,
|
||||||
onFilterSelect: PropTypes.func.isRequired,
|
onFilterSelect: PropTypes.func.isRequired,
|
||||||
onTestAllPress: PropTypes.func.isRequired,
|
onTestAllPress: PropTypes.func.isRequired,
|
||||||
|
onAppIndexerSyncPress: PropTypes.func.isRequired,
|
||||||
onScroll: PropTypes.func.isRequired,
|
onScroll: PropTypes.func.isRequired,
|
||||||
onSaveSelected: PropTypes.func.isRequired
|
onSaveSelected: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,10 +2,13 @@ import PropTypes from 'prop-types';
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
import * as commandNames from 'Commands/commandNames';
|
||||||
import withScrollPosition from 'Components/withScrollPosition';
|
import withScrollPosition from 'Components/withScrollPosition';
|
||||||
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
import { testAllIndexers } from 'Store/Actions/indexerActions';
|
import { testAllIndexers } from 'Store/Actions/indexerActions';
|
||||||
import { saveIndexerEditor, setMovieFilter, setMovieSort, setMovieTableOption } from 'Store/Actions/indexerIndexActions';
|
import { saveIndexerEditor, setMovieFilter, setMovieSort, setMovieTableOption } from 'Store/Actions/indexerIndexActions';
|
||||||
import scrollPositions from 'Store/scrollPositions';
|
import scrollPositions from 'Store/scrollPositions';
|
||||||
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
import createIndexerClientSideCollectionItemsSelector from 'Store/Selectors/createIndexerClientSideCollectionItemsSelector';
|
import createIndexerClientSideCollectionItemsSelector from 'Store/Selectors/createIndexerClientSideCollectionItemsSelector';
|
||||||
import IndexerIndex from './IndexerIndex';
|
import IndexerIndex from './IndexerIndex';
|
||||||
@@ -13,13 +16,16 @@ import IndexerIndex from './IndexerIndex';
|
|||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
createIndexerClientSideCollectionItemsSelector('indexerIndex'),
|
createIndexerClientSideCollectionItemsSelector('indexerIndex'),
|
||||||
|
createCommandExecutingSelector(commandNames.APP_INDEXER_SYNC),
|
||||||
createDimensionsSelector(),
|
createDimensionsSelector(),
|
||||||
(
|
(
|
||||||
indexers,
|
indexers,
|
||||||
|
isSyncingIndexers,
|
||||||
dimensionsState
|
dimensionsState
|
||||||
) => {
|
) => {
|
||||||
return {
|
return {
|
||||||
...indexers,
|
...indexers,
|
||||||
|
isSyncingIndexers,
|
||||||
isSmallScreen: dimensionsState.isSmallScreen
|
isSmallScreen: dimensionsState.isSmallScreen
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -46,6 +52,12 @@ function createMapDispatchToProps(dispatch, props) {
|
|||||||
|
|
||||||
onTestAllPress() {
|
onTestAllPress() {
|
||||||
dispatch(testAllIndexers());
|
dispatch(testAllIndexers());
|
||||||
|
},
|
||||||
|
|
||||||
|
onAppIndexerSyncPress() {
|
||||||
|
dispatch(executeCommand({
|
||||||
|
name: commandNames.APP_INDEXER_SYNC
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ function CapabilitiesLabel(props) {
|
|||||||
let filteredList = categories.filter((item) => item.id < 100000);
|
let filteredList = categories.filter((item) => item.id < 100000);
|
||||||
|
|
||||||
if (categoryFilter.length > 0) {
|
if (categoryFilter.length > 0) {
|
||||||
filteredList = filteredList.filter((item) => categoryFilter.includes(item.id));
|
filteredList = filteredList.filter((item) => categoryFilter.includes(item.id) || (item.subCategories && item.subCategories.some((r) => categoryFilter.includes(r.id))));
|
||||||
}
|
}
|
||||||
|
|
||||||
const nameList = filteredList.map((item) => item.name).sort();
|
const nameList = filteredList.map((item) => item.name).sort();
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
$hoverScale: 1.05;
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 0;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--cardBackgroundColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.titleRow {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
height: 38px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indexerRow {
|
||||||
|
color: var(--disabledColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoRow {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
overflow: hidden;
|
||||||
|
width: 85%;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import TextTruncate from 'react-text-truncate';
|
||||||
|
import Label from 'Components/Label';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||||
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
|
import CategoryLabel from 'Search/Table/CategoryLabel';
|
||||||
|
import Peers from 'Search/Table/Peers';
|
||||||
|
import ProtocolLabel from 'Search/Table/ProtocolLabel';
|
||||||
|
import dimensions from 'Styles/Variables/dimensions';
|
||||||
|
import formatAge from 'Utilities/Number/formatAge';
|
||||||
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './SearchIndexOverview.css';
|
||||||
|
|
||||||
|
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
|
||||||
|
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
|
||||||
|
|
||||||
|
function getContentHeight(rowHeight, isSmallScreen) {
|
||||||
|
const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding;
|
||||||
|
|
||||||
|
return rowHeight - padding;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDownloadIcon(isGrabbing, isGrabbed, grabError) {
|
||||||
|
if (isGrabbing) {
|
||||||
|
return icons.SPINNER;
|
||||||
|
} else if (isGrabbed) {
|
||||||
|
return icons.DOWNLOADING;
|
||||||
|
} else if (grabError) {
|
||||||
|
return icons.DOWNLOADING;
|
||||||
|
}
|
||||||
|
|
||||||
|
return icons.DOWNLOAD;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDownloadTooltip(isGrabbing, isGrabbed, grabError) {
|
||||||
|
if (isGrabbing) {
|
||||||
|
return '';
|
||||||
|
} else if (isGrabbed) {
|
||||||
|
return translate('AddedToDownloadClient');
|
||||||
|
} else if (grabError) {
|
||||||
|
return grabError;
|
||||||
|
}
|
||||||
|
|
||||||
|
return translate('AddToDownloadClient');
|
||||||
|
}
|
||||||
|
|
||||||
|
class SearchIndexOverview extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onGrabPress = () => {
|
||||||
|
const {
|
||||||
|
guid,
|
||||||
|
indexerId,
|
||||||
|
onGrabPress
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
onGrabPress({
|
||||||
|
guid,
|
||||||
|
indexerId
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
infoUrl,
|
||||||
|
protocol,
|
||||||
|
downloadUrl,
|
||||||
|
categories,
|
||||||
|
seeders,
|
||||||
|
leechers,
|
||||||
|
size,
|
||||||
|
age,
|
||||||
|
ageHours,
|
||||||
|
ageMinutes,
|
||||||
|
indexer,
|
||||||
|
rowHeight,
|
||||||
|
isSmallScreen,
|
||||||
|
isGrabbed,
|
||||||
|
isGrabbing,
|
||||||
|
grabError
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const contentHeight = getContentHeight(rowHeight, isSmallScreen);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.content}>
|
||||||
|
<div className={styles.info} style={{ height: contentHeight }}>
|
||||||
|
<div className={styles.titleRow}>
|
||||||
|
<div className={styles.title}>
|
||||||
|
<Link
|
||||||
|
to={infoUrl}
|
||||||
|
title={title}
|
||||||
|
>
|
||||||
|
<TextTruncate
|
||||||
|
line={2}
|
||||||
|
text={title}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.actions}>
|
||||||
|
<SpinnerIconButton
|
||||||
|
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
|
||||||
|
kind={grabError ? kinds.DANGER : kinds.DEFAULT}
|
||||||
|
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
|
||||||
|
isDisabled={isGrabbed}
|
||||||
|
isSpinning={isGrabbing}
|
||||||
|
onPress={this.onGrabPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
className={styles.downloadLink}
|
||||||
|
name={icons.SAVE}
|
||||||
|
title={translate('Save')}
|
||||||
|
to={downloadUrl}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.indexerRow}>
|
||||||
|
{indexer}
|
||||||
|
</div>
|
||||||
|
<div className={styles.infoRow}>
|
||||||
|
<ProtocolLabel
|
||||||
|
protocol={protocol}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{
|
||||||
|
protocol === 'torrent' &&
|
||||||
|
<Peers
|
||||||
|
seeders={seeders}
|
||||||
|
leechers={leechers}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
<Label>
|
||||||
|
{formatBytes(size)}
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<Label>
|
||||||
|
{formatAge(age, ageHours, ageMinutes)}
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<CategoryLabel
|
||||||
|
categories={categories}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchIndexOverview.propTypes = {
|
||||||
|
guid: PropTypes.string.isRequired,
|
||||||
|
categories: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
protocol: PropTypes.string.isRequired,
|
||||||
|
age: PropTypes.number.isRequired,
|
||||||
|
ageHours: PropTypes.number.isRequired,
|
||||||
|
ageMinutes: PropTypes.number.isRequired,
|
||||||
|
publishDate: PropTypes.string.isRequired,
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
infoUrl: PropTypes.string.isRequired,
|
||||||
|
downloadUrl: PropTypes.string.isRequired,
|
||||||
|
indexerId: PropTypes.number.isRequired,
|
||||||
|
indexer: PropTypes.string.isRequired,
|
||||||
|
size: PropTypes.number.isRequired,
|
||||||
|
files: PropTypes.number,
|
||||||
|
grabs: PropTypes.number,
|
||||||
|
seeders: PropTypes.number,
|
||||||
|
leechers: PropTypes.number,
|
||||||
|
indexerFlags: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||||
|
rowHeight: PropTypes.number.isRequired,
|
||||||
|
showRelativeDates: PropTypes.bool.isRequired,
|
||||||
|
shortDateFormat: PropTypes.string.isRequired,
|
||||||
|
longDateFormat: PropTypes.string.isRequired,
|
||||||
|
timeFormat: PropTypes.string.isRequired,
|
||||||
|
isSmallScreen: PropTypes.bool.isRequired,
|
||||||
|
onGrabPress: PropTypes.func.isRequired,
|
||||||
|
isGrabbing: PropTypes.bool.isRequired,
|
||||||
|
isGrabbed: PropTypes.bool.isRequired,
|
||||||
|
grabError: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
SearchIndexOverview.defaultProps = {
|
||||||
|
isGrabbing: false,
|
||||||
|
isGrabbed: false
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SearchIndexOverview;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
.grid {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
&:hover {
|
||||||
|
.content {
|
||||||
|
background-color: var(--tableRowHoverBackgroundColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { Grid, WindowScroller } from 'react-virtualized';
|
||||||
|
import Measure from 'Components/Measure';
|
||||||
|
import SearchIndexItemConnector from 'Search/Table/SearchIndexItemConnector';
|
||||||
|
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
|
||||||
|
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
||||||
|
import SearchIndexOverview from './SearchIndexOverview';
|
||||||
|
import styles from './SearchIndexOverviews.css';
|
||||||
|
|
||||||
|
class SearchIndexOverviews extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
width: 0,
|
||||||
|
columnCount: 1,
|
||||||
|
rowHeight: 100,
|
||||||
|
scrollRestored: false
|
||||||
|
};
|
||||||
|
|
||||||
|
this._grid = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps, prevState) {
|
||||||
|
const {
|
||||||
|
items,
|
||||||
|
sortKey,
|
||||||
|
jumpToCharacter,
|
||||||
|
scrollTop,
|
||||||
|
isSmallScreen
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
width,
|
||||||
|
rowHeight,
|
||||||
|
scrollRestored
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
if (prevProps.sortKey !== sortKey) {
|
||||||
|
this.calculateGrid(this.state.width, isSmallScreen);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this._grid &&
|
||||||
|
(prevState.width !== width ||
|
||||||
|
prevState.rowHeight !== rowHeight ||
|
||||||
|
hasDifferentItemsOrOrder(prevProps.items, items, 'guid')
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// recomputeGridSize also forces Grid to discard its cache of rendered cells
|
||||||
|
this._grid.recomputeGridSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._grid && scrollTop !== 0 && !scrollRestored) {
|
||||||
|
this.setState({ scrollRestored: true });
|
||||||
|
this._grid.scrollToPosition({ scrollTop });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) {
|
||||||
|
const index = getIndexOfFirstCharacter(items, jumpToCharacter);
|
||||||
|
|
||||||
|
if (this._grid && index != null) {
|
||||||
|
|
||||||
|
this._grid.scrollToCell({
|
||||||
|
rowIndex: index,
|
||||||
|
columnIndex: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Control
|
||||||
|
|
||||||
|
setGridRef = (ref) => {
|
||||||
|
this._grid = ref;
|
||||||
|
};
|
||||||
|
|
||||||
|
calculateGrid = (width = this.state.width, isSmallScreen) => {
|
||||||
|
const rowHeight = 100;
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
width,
|
||||||
|
rowHeight
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
cellRenderer = ({ key, rowIndex, style }) => {
|
||||||
|
const {
|
||||||
|
items,
|
||||||
|
showRelativeDates,
|
||||||
|
shortDateFormat,
|
||||||
|
longDateFormat,
|
||||||
|
timeFormat,
|
||||||
|
isSmallScreen,
|
||||||
|
onGrabPress
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
rowHeight
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const release = items[rowIndex];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles.container}
|
||||||
|
key={key}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
<SearchIndexItemConnector
|
||||||
|
key={release.guid}
|
||||||
|
component={SearchIndexOverview}
|
||||||
|
rowHeight={rowHeight}
|
||||||
|
showRelativeDates={showRelativeDates}
|
||||||
|
shortDateFormat={shortDateFormat}
|
||||||
|
longDateFormat={longDateFormat}
|
||||||
|
timeFormat={timeFormat}
|
||||||
|
isSmallScreen={isSmallScreen}
|
||||||
|
style={style}
|
||||||
|
guid={release.guid}
|
||||||
|
onGrabPress={onGrabPress}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onMeasure = ({ width }) => {
|
||||||
|
this.calculateGrid(width, this.props.isSmallScreen);
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
items
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
width,
|
||||||
|
rowHeight
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Measure
|
||||||
|
whitelist={['width']}
|
||||||
|
onMeasure={this.onMeasure}
|
||||||
|
>
|
||||||
|
<WindowScroller
|
||||||
|
scrollElement={undefined}
|
||||||
|
>
|
||||||
|
{({ height, registerChild, onChildScroll, scrollTop }) => {
|
||||||
|
if (!height) {
|
||||||
|
return <div />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={registerChild}>
|
||||||
|
<Grid
|
||||||
|
ref={this.setGridRef}
|
||||||
|
className={styles.grid}
|
||||||
|
autoHeight={true}
|
||||||
|
height={height}
|
||||||
|
columnCount={1}
|
||||||
|
columnWidth={width}
|
||||||
|
rowCount={items.length}
|
||||||
|
rowHeight={rowHeight}
|
||||||
|
width={width}
|
||||||
|
onScroll={onChildScroll}
|
||||||
|
scrollTop={scrollTop}
|
||||||
|
overscanRowCount={2}
|
||||||
|
cellRenderer={this.cellRenderer}
|
||||||
|
scrollToAlignment={'start'}
|
||||||
|
isScrollingOptOut={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</WindowScroller>
|
||||||
|
</Measure>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchIndexOverviews.propTypes = {
|
||||||
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
sortKey: PropTypes.string,
|
||||||
|
scrollTop: PropTypes.number.isRequired,
|
||||||
|
jumpToCharacter: PropTypes.string,
|
||||||
|
scroller: PropTypes.instanceOf(Element).isRequired,
|
||||||
|
showRelativeDates: PropTypes.bool.isRequired,
|
||||||
|
shortDateFormat: PropTypes.string.isRequired,
|
||||||
|
longDateFormat: PropTypes.string.isRequired,
|
||||||
|
isSmallScreen: PropTypes.bool.isRequired,
|
||||||
|
timeFormat: PropTypes.string.isRequired,
|
||||||
|
onGrabPress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SearchIndexOverviews;
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { grabRelease } from 'Store/Actions/releaseActions';
|
||||||
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
|
import SearchIndexOverviews from './SearchIndexOverviews';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
createUISettingsSelector(),
|
||||||
|
createDimensionsSelector(),
|
||||||
|
(uiSettings, dimensions) => {
|
||||||
|
return {
|
||||||
|
showRelativeDates: uiSettings.showRelativeDates,
|
||||||
|
shortDateFormat: uiSettings.shortDateFormat,
|
||||||
|
longDateFormat: uiSettings.longDateFormat,
|
||||||
|
timeFormat: uiSettings.timeFormat,
|
||||||
|
isSmallScreen: dimensions.isSmallScreen
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMapDispatchToProps(dispatch, props) {
|
||||||
|
return {
|
||||||
|
onGrabPress(payload) {
|
||||||
|
dispatch(grabRelease(payload));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, createMapDispatchToProps)(SearchIndexOverviews);
|
||||||
@@ -11,8 +11,6 @@ import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
|||||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||||
import { align, icons, sortDirections } from 'Helpers/Props';
|
import { align, icons, sortDirections } from 'Helpers/Props';
|
||||||
import AddIndexerModal from 'Indexer/Add/AddIndexerModal';
|
|
||||||
import EditIndexerModalConnector from 'Indexer/Edit/EditIndexerModalConnector';
|
|
||||||
import NoIndexer from 'Indexer/NoIndexer';
|
import NoIndexer from 'Indexer/NoIndexer';
|
||||||
import * as keyCodes from 'Utilities/Constants/keyCodes';
|
import * as keyCodes from 'Utilities/Constants/keyCodes';
|
||||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||||
@@ -23,12 +21,17 @@ import selectAll from 'Utilities/Table/selectAll';
|
|||||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||||
import SearchIndexFilterMenu from './Menus/SearchIndexFilterMenu';
|
import SearchIndexFilterMenu from './Menus/SearchIndexFilterMenu';
|
||||||
import SearchIndexSortMenu from './Menus/SearchIndexSortMenu';
|
import SearchIndexSortMenu from './Menus/SearchIndexSortMenu';
|
||||||
|
import SearchIndexOverviewsConnector from './Mobile/SearchIndexOverviewsConnector';
|
||||||
import NoSearchResults from './NoSearchResults';
|
import NoSearchResults from './NoSearchResults';
|
||||||
import SearchFooterConnector from './SearchFooterConnector';
|
import SearchFooterConnector from './SearchFooterConnector';
|
||||||
import SearchIndexTableConnector from './Table/SearchIndexTableConnector';
|
import SearchIndexTableConnector from './Table/SearchIndexTableConnector';
|
||||||
import styles from './SearchIndex.css';
|
import styles from './SearchIndex.css';
|
||||||
|
|
||||||
function getViewComponent() {
|
function getViewComponent(isSmallScreen) {
|
||||||
|
if (isSmallScreen) {
|
||||||
|
return SearchIndexOverviewsConnector;
|
||||||
|
}
|
||||||
|
|
||||||
return SearchIndexTableConnector;
|
return SearchIndexTableConnector;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,8 +47,6 @@ class SearchIndex extends Component {
|
|||||||
scroller: null,
|
scroller: null,
|
||||||
jumpBarItems: { order: [] },
|
jumpBarItems: { order: [] },
|
||||||
jumpToCharacter: null,
|
jumpToCharacter: null,
|
||||||
isAddIndexerModalOpen: false,
|
|
||||||
isEditIndexerModalOpen: false,
|
|
||||||
searchType: null,
|
searchType: null,
|
||||||
lastToggled: null,
|
lastToggled: null,
|
||||||
allSelected: false,
|
allSelected: false,
|
||||||
@@ -177,21 +178,6 @@ class SearchIndex extends Component {
|
|||||||
//
|
//
|
||||||
// Listeners
|
// Listeners
|
||||||
|
|
||||||
onAddIndexerPress = () => {
|
|
||||||
this.setState({ isAddIndexerModalOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onAddIndexerModalClose = ({ indexerSelected = false } = {}) => {
|
|
||||||
this.setState({
|
|
||||||
isAddIndexerModalOpen: false,
|
|
||||||
isEditIndexerModalOpen: indexerSelected
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onEditIndexerModalClose = () => {
|
|
||||||
this.setState({ isEditIndexerModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
onJumpBarItemPress = (jumpToCharacter) => {
|
onJumpBarItemPress = (jumpToCharacter) => {
|
||||||
this.setState({ jumpToCharacter });
|
this.setState({ jumpToCharacter });
|
||||||
};
|
};
|
||||||
@@ -253,6 +239,7 @@ class SearchIndex extends Component {
|
|||||||
onScroll,
|
onScroll,
|
||||||
onSortSelect,
|
onSortSelect,
|
||||||
onFilterSelect,
|
onFilterSelect,
|
||||||
|
isSmallScreen,
|
||||||
hasIndexers,
|
hasIndexers,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
@@ -260,8 +247,6 @@ class SearchIndex extends Component {
|
|||||||
const {
|
const {
|
||||||
scroller,
|
scroller,
|
||||||
jumpBarItems,
|
jumpBarItems,
|
||||||
isAddIndexerModalOpen,
|
|
||||||
isEditIndexerModalOpen,
|
|
||||||
jumpToCharacter,
|
jumpToCharacter,
|
||||||
selectedState,
|
selectedState,
|
||||||
allSelected,
|
allSelected,
|
||||||
@@ -270,7 +255,7 @@ class SearchIndex extends Component {
|
|||||||
|
|
||||||
const selectedIndexerIds = this.getSelectedIds();
|
const selectedIndexerIds = this.getSelectedIds();
|
||||||
|
|
||||||
const ViewComponent = getViewComponent();
|
const ViewComponent = getViewComponent(isSmallScreen);
|
||||||
const isLoaded = !!(!error && isPopulated && items.length && scroller);
|
const isLoaded = !!(!error && isPopulated && items.length && scroller);
|
||||||
const hasNoIndexer = !totalItems;
|
const hasNoIndexer = !totalItems;
|
||||||
|
|
||||||
@@ -384,16 +369,6 @@ class SearchIndex extends Component {
|
|||||||
onSearchPress={this.onSearchPress}
|
onSearchPress={this.onSearchPress}
|
||||||
onBulkGrabPress={this.onBulkGrabPress}
|
onBulkGrabPress={this.onBulkGrabPress}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AddIndexerModal
|
|
||||||
isOpen={isAddIndexerModalOpen}
|
|
||||||
onModalClose={this.onAddIndexerModalClose}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<EditIndexerModalConnector
|
|
||||||
isOpen={isEditIndexerModalOpen}
|
|
||||||
onModalClose={this.onEditIndexerModalClose}
|
|
||||||
/>
|
|
||||||
</PageContent>
|
</PageContent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
.category {
|
.category {
|
||||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||||
|
|
||||||
flex: 0 0 110px;
|
flex: 0 0 130px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.age,
|
.age,
|
||||||
|
|||||||
@@ -18,20 +18,20 @@ function createMapStateToProps() {
|
|||||||
return createSelector(
|
return createSelector(
|
||||||
createReleaseSelector(),
|
createReleaseSelector(),
|
||||||
(
|
(
|
||||||
movie
|
release
|
||||||
) => {
|
) => {
|
||||||
|
|
||||||
// If a movie is deleted this selector may fire before the parent
|
// If a release is deleted this selector may fire before the parent
|
||||||
// selecors, which will result in an undefined movie, if that happens
|
// selecors, which will result in an undefined release, if that happens
|
||||||
// we want to return early here and again in the render function to avoid
|
// we want to return early here and again in the render function to avoid
|
||||||
// trying to show a movie that has no information available.
|
// trying to show a release that has no information available.
|
||||||
|
|
||||||
if (!movie) {
|
if (!release) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...movie
|
...release
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -41,7 +41,7 @@ const mapDispatchToProps = {
|
|||||||
dispatchExecuteCommand: executeCommand
|
dispatchExecuteCommand: executeCommand
|
||||||
};
|
};
|
||||||
|
|
||||||
class MovieIndexItemConnector extends Component {
|
class SearchIndexItemConnector extends Component {
|
||||||
|
|
||||||
//
|
//
|
||||||
// Render
|
// Render
|
||||||
@@ -66,9 +66,9 @@ class MovieIndexItemConnector extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MovieIndexItemConnector.propTypes = {
|
SearchIndexItemConnector.propTypes = {
|
||||||
guid: PropTypes.string,
|
guid: PropTypes.string,
|
||||||
component: PropTypes.elementType.isRequired
|
component: PropTypes.elementType.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(MovieIndexItemConnector);
|
export default connect(createMapStateToProps, mapDispatchToProps)(SearchIndexItemConnector);
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
.category {
|
.category {
|
||||||
composes: cell;
|
composes: cell;
|
||||||
|
|
||||||
flex: 0 0 110px;
|
flex: 0 0 130px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.age,
|
.age,
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import { sizes } from 'Helpers/Props';
|
||||||
|
import AddCategoryModalContentConnector from './AddCategoryModalContentConnector';
|
||||||
|
|
||||||
|
function AddCategoryModal({ isOpen, onModalClose, ...otherProps }) {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
size={sizes.MEDIUM}
|
||||||
|
isOpen={isOpen}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
>
|
||||||
|
<AddCategoryModalContentConnector
|
||||||
|
{...otherProps}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AddCategoryModal.propTypes = {
|
||||||
|
isOpen: PropTypes.bool.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddCategoryModal;
|
||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||||
|
import AddCategoryModal from './AddCategoryModal';
|
||||||
|
|
||||||
|
function createMapDispatchToProps(dispatch, props) {
|
||||||
|
const section = 'settings.downloadClientCategories';
|
||||||
|
|
||||||
|
return {
|
||||||
|
dispatchClearPendingChanges() {
|
||||||
|
dispatch(clearPendingChanges({ section }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class AddCategoryModalConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onModalClose = () => {
|
||||||
|
this.props.dispatchClearPendingChanges();
|
||||||
|
this.props.onModalClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
dispatchClearPendingChanges,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AddCategoryModal
|
||||||
|
{...otherProps}
|
||||||
|
onModalClose={this.onModalClose}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AddCategoryModalConnector.propTypes = {
|
||||||
|
onModalClose: PropTypes.func.isRequired,
|
||||||
|
dispatchClearPendingChanges: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(null, createMapDispatchToProps)(AddCategoryModalConnector);
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
.deleteButton {
|
||||||
|
composes: button from '~Components/Link/Button.css';
|
||||||
|
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
+111
@@ -0,0 +1,111 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import Form from 'Components/Form/Form';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import { inputTypes, kinds } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './AddCategoryModalContent.css';
|
||||||
|
|
||||||
|
function AddCategoryModalContent(props) {
|
||||||
|
const {
|
||||||
|
advancedSettings,
|
||||||
|
item,
|
||||||
|
onInputChange,
|
||||||
|
onFieldChange,
|
||||||
|
onCancelPress,
|
||||||
|
onSavePress,
|
||||||
|
onDeleteSpecificationPress,
|
||||||
|
...otherProps
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
clientCategory,
|
||||||
|
categories
|
||||||
|
} = item;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onCancelPress}>
|
||||||
|
<ModalHeader>
|
||||||
|
{`${id ? 'Edit' : 'Add'} Category`}
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<Form
|
||||||
|
{...otherProps}
|
||||||
|
>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>
|
||||||
|
{translate('DownloadClientCategory')}
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.TEXT}
|
||||||
|
name="clientCategory"
|
||||||
|
{...clientCategory}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>
|
||||||
|
{translate('MappedCategories')}
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CATEGORY_SELECT}
|
||||||
|
name="categories"
|
||||||
|
{...categories}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Form>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
{
|
||||||
|
id &&
|
||||||
|
<Button
|
||||||
|
className={styles.deleteButton}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
onPress={onDeleteSpecificationPress}
|
||||||
|
>
|
||||||
|
{translate('Delete')}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onPress={onCancelPress}
|
||||||
|
>
|
||||||
|
{translate('Cancel')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<SpinnerErrorButton
|
||||||
|
isSpinning={false}
|
||||||
|
onPress={onSavePress}
|
||||||
|
>
|
||||||
|
{translate('Save')}
|
||||||
|
</SpinnerErrorButton>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AddCategoryModalContent.propTypes = {
|
||||||
|
advancedSettings: PropTypes.bool.isRequired,
|
||||||
|
item: PropTypes.object.isRequired,
|
||||||
|
onInputChange: PropTypes.func.isRequired,
|
||||||
|
onFieldChange: PropTypes.func.isRequired,
|
||||||
|
onCancelPress: PropTypes.func.isRequired,
|
||||||
|
onSavePress: PropTypes.func.isRequired,
|
||||||
|
onDeleteSpecificationPress: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddCategoryModalContent;
|
||||||
+78
@@ -0,0 +1,78 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { clearDownloadClientCategoryPending, saveDownloadClientCategory, setDownloadClientCategoryFieldValue, setDownloadClientCategoryValue } from 'Store/Actions/settingsActions';
|
||||||
|
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
||||||
|
import AddCategoryModalContent from './AddCategoryModalContent';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.settings.advancedSettings,
|
||||||
|
createProviderSettingsSelector('downloadClientCategories'),
|
||||||
|
(advancedSettings, specification) => {
|
||||||
|
return {
|
||||||
|
advancedSettings,
|
||||||
|
...specification
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
setDownloadClientCategoryValue,
|
||||||
|
setDownloadClientCategoryFieldValue,
|
||||||
|
saveDownloadClientCategory,
|
||||||
|
clearDownloadClientCategoryPending
|
||||||
|
};
|
||||||
|
|
||||||
|
class AddCategoryModalContentConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onInputChange = ({ name, value }) => {
|
||||||
|
this.props.setDownloadClientCategoryValue({ name, value });
|
||||||
|
};
|
||||||
|
|
||||||
|
onFieldChange = ({ name, value }) => {
|
||||||
|
this.props.setDownloadClientCategoryFieldValue({ name, value });
|
||||||
|
};
|
||||||
|
|
||||||
|
onCancelPress = () => {
|
||||||
|
this.props.clearDownloadClientCategoryPending();
|
||||||
|
this.props.onModalClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
onSavePress = () => {
|
||||||
|
this.props.saveDownloadClientCategory({ id: this.props.id });
|
||||||
|
this.props.onModalClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<AddCategoryModalContent
|
||||||
|
{...this.props}
|
||||||
|
onCancelPress={this.onCancelPress}
|
||||||
|
onSavePress={this.onSavePress}
|
||||||
|
onInputChange={this.onInputChange}
|
||||||
|
onFieldChange={this.onFieldChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AddCategoryModalContentConnector.propTypes = {
|
||||||
|
id: PropTypes.number,
|
||||||
|
item: PropTypes.object.isRequired,
|
||||||
|
setDownloadClientCategoryValue: PropTypes.func.isRequired,
|
||||||
|
setDownloadClientCategoryFieldValue: PropTypes.func.isRequired,
|
||||||
|
clearDownloadClientCategoryPending: PropTypes.func.isRequired,
|
||||||
|
saveDownloadClientCategory: PropTypes.func.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(AddCategoryModalContentConnector);
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
.customFormat {
|
||||||
|
composes: card from '~Components/Card.css';
|
||||||
|
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nameContainer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
@add-mixin truncate;
|
||||||
|
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.labels {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 5px;
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltipLabel {
|
||||||
|
composes: label from '~Components/Label.css';
|
||||||
|
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import Card from 'Components/Card';
|
||||||
|
import Label from 'Components/Label';
|
||||||
|
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||||
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import AddCategoryModalConnector from './AddCategoryModalConnector';
|
||||||
|
import styles from './Category.css';
|
||||||
|
|
||||||
|
class Category extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isEditSpecificationModalOpen: false,
|
||||||
|
isDeleteSpecificationModalOpen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onEditSpecificationPress = () => {
|
||||||
|
this.setState({ isEditSpecificationModalOpen: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onEditSpecificationModalClose = () => {
|
||||||
|
this.setState({ isEditSpecificationModalOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
onDeleteSpecificationPress = () => {
|
||||||
|
this.setState({
|
||||||
|
isEditSpecificationModalOpen: false,
|
||||||
|
isDeleteSpecificationModalOpen: true
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onDeleteSpecificationModalClose = () => {
|
||||||
|
this.setState({ isDeleteSpecificationModalOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
onConfirmDeleteSpecification = () => {
|
||||||
|
this.props.onConfirmDeleteSpecification(this.props.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
clientCategory,
|
||||||
|
categories
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
className={styles.customFormat}
|
||||||
|
overlayContent={true}
|
||||||
|
onPress={this.onEditSpecificationPress}
|
||||||
|
>
|
||||||
|
<div className={styles.nameContainer}>
|
||||||
|
<div className={styles.name}>
|
||||||
|
{clientCategory}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Label kind={kinds.PRIMARY}>
|
||||||
|
{`${categories.length} ${categories.length > 1 ? translate('Categories') : translate('Category')}`}
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<AddCategoryModalConnector
|
||||||
|
id={id}
|
||||||
|
isOpen={this.state.isEditSpecificationModalOpen}
|
||||||
|
onModalClose={this.onEditSpecificationModalClose}
|
||||||
|
onDeleteSpecificationPress={this.onDeleteSpecificationPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={this.state.isDeleteSpecificationModalOpen}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
title={translate('DeleteClientCategory')}
|
||||||
|
message={
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
{translate('AreYouSureYouWantToDeleteCategory', [name])}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
confirmLabel={translate('Delete')}
|
||||||
|
onConfirm={this.onConfirmDeleteSpecification}
|
||||||
|
onCancel={this.onDeleteSpecificationModalClose}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Category.propTypes = {
|
||||||
|
id: PropTypes.number.isRequired,
|
||||||
|
categories: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||||
|
clientCategory: PropTypes.string.isRequired,
|
||||||
|
onConfirmDeleteSpecification: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Category;
|
||||||
+72
-2
@@ -1,11 +1,14 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import Alert from 'Components/Alert';
|
import Alert from 'Components/Alert';
|
||||||
|
import Card from 'Components/Card';
|
||||||
|
import FieldSet from 'Components/FieldSet';
|
||||||
import Form from 'Components/Form/Form';
|
import Form from 'Components/Form/Form';
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
|
import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
@@ -13,12 +16,33 @@ import ModalBody from 'Components/Modal/ModalBody';
|
|||||||
import ModalContent from 'Components/Modal/ModalContent';
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import { inputTypes, kinds } from 'Helpers/Props';
|
import { icons, inputTypes, kinds } from 'Helpers/Props';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
|
import AddCategoryModalConnector from './Categories/AddCategoryModalConnector';
|
||||||
|
import Category from './Categories/Category';
|
||||||
import styles from './EditDownloadClientModalContent.css';
|
import styles from './EditDownloadClientModalContent.css';
|
||||||
|
|
||||||
class EditDownloadClientModalContent extends Component {
|
class EditDownloadClientModalContent extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isAddCategoryModalOpen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onAddCategoryPress = () => {
|
||||||
|
this.setState({ isAddCategoryModalOpen: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onAddCategoryModalClose = () => {
|
||||||
|
this.setState({ isAddCategoryModalOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
// Render
|
// Render
|
||||||
|
|
||||||
@@ -27,6 +51,7 @@ class EditDownloadClientModalContent extends Component {
|
|||||||
advancedSettings,
|
advancedSettings,
|
||||||
isFetching,
|
isFetching,
|
||||||
error,
|
error,
|
||||||
|
categories,
|
||||||
isSaving,
|
isSaving,
|
||||||
isTesting,
|
isTesting,
|
||||||
saveError,
|
saveError,
|
||||||
@@ -37,15 +62,21 @@ class EditDownloadClientModalContent extends Component {
|
|||||||
onSavePress,
|
onSavePress,
|
||||||
onTestPress,
|
onTestPress,
|
||||||
onDeleteDownloadClientPress,
|
onDeleteDownloadClientPress,
|
||||||
|
onConfirmDeleteCategory,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
isAddCategoryModalOpen
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
implementationName,
|
implementationName,
|
||||||
name,
|
name,
|
||||||
enable,
|
enable,
|
||||||
priority,
|
priority,
|
||||||
|
supportsCategories,
|
||||||
fields,
|
fields,
|
||||||
message
|
message
|
||||||
} = item;
|
} = item;
|
||||||
@@ -136,6 +167,43 @@ class EditDownloadClientModalContent extends Component {
|
|||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
{
|
||||||
|
supportsCategories.value ?
|
||||||
|
<FieldSet legend={translate('MappedCategories')}>
|
||||||
|
<div className={styles.customFormats}>
|
||||||
|
{
|
||||||
|
categories.map((tag) => {
|
||||||
|
return (
|
||||||
|
<Category
|
||||||
|
key={tag.id}
|
||||||
|
{...tag}
|
||||||
|
onConfirmDeleteSpecification={onConfirmDeleteCategory}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
<Card
|
||||||
|
className={styles.addCategory}
|
||||||
|
onPress={this.onAddCategoryPress}
|
||||||
|
>
|
||||||
|
<div className={styles.center}>
|
||||||
|
<Icon
|
||||||
|
name={icons.ADD}
|
||||||
|
size={25}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</FieldSet> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
<AddCategoryModalConnector
|
||||||
|
isOpen={isAddCategoryModalOpen}
|
||||||
|
onModalClose={this.onAddCategoryModalClose}
|
||||||
|
/>
|
||||||
|
|
||||||
</Form>
|
</Form>
|
||||||
}
|
}
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
@@ -185,13 +253,15 @@ EditDownloadClientModalContent.propTypes = {
|
|||||||
isSaving: PropTypes.bool.isRequired,
|
isSaving: PropTypes.bool.isRequired,
|
||||||
saveError: PropTypes.object,
|
saveError: PropTypes.object,
|
||||||
isTesting: PropTypes.bool.isRequired,
|
isTesting: PropTypes.bool.isRequired,
|
||||||
|
categories: PropTypes.arrayOf(PropTypes.object),
|
||||||
item: PropTypes.object.isRequired,
|
item: PropTypes.object.isRequired,
|
||||||
onInputChange: PropTypes.func.isRequired,
|
onInputChange: PropTypes.func.isRequired,
|
||||||
onFieldChange: PropTypes.func.isRequired,
|
onFieldChange: PropTypes.func.isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired,
|
onModalClose: PropTypes.func.isRequired,
|
||||||
onSavePress: PropTypes.func.isRequired,
|
onSavePress: PropTypes.func.isRequired,
|
||||||
onTestPress: PropTypes.func.isRequired,
|
onTestPress: PropTypes.func.isRequired,
|
||||||
onDeleteDownloadClientPress: PropTypes.func
|
onDeleteDownloadClientPress: PropTypes.func,
|
||||||
|
onConfirmDeleteCategory: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default EditDownloadClientModalContent;
|
export default EditDownloadClientModalContent;
|
||||||
|
|||||||
+24
-4
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { saveDownloadClient, setDownloadClientFieldValue, setDownloadClientValue, testDownloadClient } from 'Store/Actions/settingsActions';
|
import { deleteDownloadClientCategory, fetchDownloadClientCategories, saveDownloadClient, setDownloadClientFieldValue, setDownloadClientValue, testDownloadClient } from 'Store/Actions/settingsActions';
|
||||||
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
||||||
import EditDownloadClientModalContent from './EditDownloadClientModalContent';
|
import EditDownloadClientModalContent from './EditDownloadClientModalContent';
|
||||||
|
|
||||||
@@ -10,10 +10,12 @@ function createMapStateToProps() {
|
|||||||
return createSelector(
|
return createSelector(
|
||||||
(state) => state.settings.advancedSettings,
|
(state) => state.settings.advancedSettings,
|
||||||
createProviderSettingsSelector('downloadClients'),
|
createProviderSettingsSelector('downloadClients'),
|
||||||
(advancedSettings, downloadClient) => {
|
(state) => state.settings.downloadClientCategories,
|
||||||
|
(advancedSettings, downloadClient, categories) => {
|
||||||
return {
|
return {
|
||||||
advancedSettings,
|
advancedSettings,
|
||||||
...downloadClient
|
...downloadClient,
|
||||||
|
categories: categories.items
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -23,7 +25,9 @@ const mapDispatchToProps = {
|
|||||||
setDownloadClientValue,
|
setDownloadClientValue,
|
||||||
setDownloadClientFieldValue,
|
setDownloadClientFieldValue,
|
||||||
saveDownloadClient,
|
saveDownloadClient,
|
||||||
testDownloadClient
|
testDownloadClient,
|
||||||
|
fetchDownloadClientCategories,
|
||||||
|
deleteDownloadClientCategory
|
||||||
};
|
};
|
||||||
|
|
||||||
class EditDownloadClientModalContentConnector extends Component {
|
class EditDownloadClientModalContentConnector extends Component {
|
||||||
@@ -31,6 +35,14 @@ class EditDownloadClientModalContentConnector extends Component {
|
|||||||
//
|
//
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
tagsFromId
|
||||||
|
} = this.props;
|
||||||
|
this.props.fetchDownloadClientCategories({ id: tagsFromId || id });
|
||||||
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
componentDidUpdate(prevProps, prevState) {
|
||||||
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
|
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
|
||||||
this.props.onModalClose();
|
this.props.onModalClose();
|
||||||
@@ -56,6 +68,10 @@ class EditDownloadClientModalContentConnector extends Component {
|
|||||||
this.props.testDownloadClient({ id: this.props.id });
|
this.props.testDownloadClient({ id: this.props.id });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onConfirmDeleteCategory = (id) => {
|
||||||
|
this.props.deleteDownloadClientCategory({ id });
|
||||||
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
// Render
|
// Render
|
||||||
|
|
||||||
@@ -67,6 +83,7 @@ class EditDownloadClientModalContentConnector extends Component {
|
|||||||
onTestPress={this.onTestPress}
|
onTestPress={this.onTestPress}
|
||||||
onInputChange={this.onInputChange}
|
onInputChange={this.onInputChange}
|
||||||
onFieldChange={this.onFieldChange}
|
onFieldChange={this.onFieldChange}
|
||||||
|
onConfirmDeleteCategory={this.onConfirmDeleteCategory}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -74,10 +91,13 @@ class EditDownloadClientModalContentConnector extends Component {
|
|||||||
|
|
||||||
EditDownloadClientModalContentConnector.propTypes = {
|
EditDownloadClientModalContentConnector.propTypes = {
|
||||||
id: PropTypes.number,
|
id: PropTypes.number,
|
||||||
|
tagsFromId: PropTypes.number,
|
||||||
isFetching: PropTypes.bool.isRequired,
|
isFetching: PropTypes.bool.isRequired,
|
||||||
isSaving: PropTypes.bool.isRequired,
|
isSaving: PropTypes.bool.isRequired,
|
||||||
saveError: PropTypes.object,
|
saveError: PropTypes.object,
|
||||||
item: PropTypes.object.isRequired,
|
item: PropTypes.object.isRequired,
|
||||||
|
fetchDownloadClientCategories: PropTypes.func.isRequired,
|
||||||
|
deleteDownloadClientCategory: PropTypes.func.isRequired,
|
||||||
setDownloadClientValue: PropTypes.func.isRequired,
|
setDownloadClientValue: PropTypes.func.isRequired,
|
||||||
setDownloadClientFieldValue: PropTypes.func.isRequired,
|
setDownloadClientFieldValue: PropTypes.func.isRequired,
|
||||||
saveDownloadClient: PropTypes.func.isRequired,
|
saveDownloadClient: PropTypes.func.isRequired,
|
||||||
|
|||||||
@@ -11,12 +11,20 @@ import ConfirmModal from 'Components/Modal/ConfirmModal';
|
|||||||
import { icons, inputTypes, kinds } from 'Helpers/Props';
|
import { icons, inputTypes, kinds } from 'Helpers/Props';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
const authenticationMethodOptions = [
|
export const authenticationRequiredWarning = translate('AuthenticationRequiredWarning');
|
||||||
{ key: 'none', value: 'None' },
|
|
||||||
|
export const authenticationMethodOptions = [
|
||||||
|
{ key: 'none', value: 'None', isDisabled: true },
|
||||||
|
{ key: 'external', value: 'External', isHidden: true },
|
||||||
{ key: 'basic', value: 'Basic (Browser Popup)' },
|
{ key: 'basic', value: 'Basic (Browser Popup)' },
|
||||||
{ key: 'forms', value: 'Forms (Login Page)' }
|
{ key: 'forms', value: 'Forms (Login Page)' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const authenticationRequiredOptions = [
|
||||||
|
{ key: 'enabled', value: 'Enabled' },
|
||||||
|
{ key: 'disabledForLocalAddresses', value: 'Disabled for Local Addresses' }
|
||||||
|
];
|
||||||
|
|
||||||
const certificateValidationOptions = [
|
const certificateValidationOptions = [
|
||||||
{ key: 'enabled', value: 'Enabled' },
|
{ key: 'enabled', value: 'Enabled' },
|
||||||
{ key: 'disabledForLocalAddresses', value: 'Disabled for Local Addresses' },
|
{ key: 'disabledForLocalAddresses', value: 'Disabled for Local Addresses' },
|
||||||
@@ -68,6 +76,7 @@ class SecuritySettings extends Component {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
authenticationMethod,
|
authenticationMethod,
|
||||||
|
authenticationRequired,
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
apiKey,
|
apiKey,
|
||||||
@@ -86,13 +95,31 @@ class SecuritySettings extends Component {
|
|||||||
name="authenticationMethod"
|
name="authenticationMethod"
|
||||||
values={authenticationMethodOptions}
|
values={authenticationMethodOptions}
|
||||||
helpText={translate('AuthenticationMethodHelpText')}
|
helpText={translate('AuthenticationMethodHelpText')}
|
||||||
|
helpTextWarning={authenticationRequiredWarning}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...authenticationMethod}
|
{...authenticationMethod}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
{
|
{
|
||||||
authenticationEnabled &&
|
authenticationEnabled ?
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('AuthenticationRequired')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="authenticationRequired"
|
||||||
|
values={authenticationRequiredOptions}
|
||||||
|
helpText={translate('AuthenticationRequiredHelpText')}
|
||||||
|
onChange={onInputChange}
|
||||||
|
{...authenticationRequired}
|
||||||
|
/>
|
||||||
|
</FormGroup> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
authenticationEnabled ?
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>{translate('Username')}</FormLabel>
|
<FormLabel>{translate('Username')}</FormLabel>
|
||||||
|
|
||||||
@@ -102,11 +129,12 @@ class SecuritySettings extends Component {
|
|||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...username}
|
{...username}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
authenticationEnabled &&
|
authenticationEnabled ?
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>{translate('Password')}</FormLabel>
|
<FormLabel>{translate('Password')}</FormLabel>
|
||||||
|
|
||||||
@@ -116,7 +144,8 @@ class SecuritySettings extends Component {
|
|||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...password}
|
{...password}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ function createLanguagesSelector() {
|
|||||||
return createSelector(
|
return createSelector(
|
||||||
(state) => state.localization,
|
(state) => state.localization,
|
||||||
(localization) => {
|
(localization) => {
|
||||||
console.log(localization);
|
|
||||||
|
|
||||||
const items = localization.items;
|
const items = localization.items;
|
||||||
|
|
||||||
if (!items) {
|
if (!items) {
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
import { createAction } from 'redux-actions';
|
||||||
|
import { batchActions } from 'redux-batched-actions';
|
||||||
|
import createClearReducer from 'Store/Actions/Creators/Reducers/createClearReducer';
|
||||||
|
import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
|
||||||
|
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
||||||
|
import { createThunk } from 'Store/thunks';
|
||||||
|
import getNextId from 'Utilities/State/getNextId';
|
||||||
|
import getProviderState from 'Utilities/State/getProviderState';
|
||||||
|
import getSectionState from 'Utilities/State/getSectionState';
|
||||||
|
import selectProviderSchema from 'Utilities/State/selectProviderSchema';
|
||||||
|
import { removeItem, set, update, updateItem } from '../baseActions';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Variables
|
||||||
|
|
||||||
|
const section = 'settings.downloadClientCategories';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Actions Types
|
||||||
|
|
||||||
|
export const FETCH_DOWNLOAD_CLIENT_CATEGORIES = 'settings/downloadClientCategories/fetchDownloadClientCategories';
|
||||||
|
export const FETCH_DOWNLOAD_CLIENT_CATEGORY_SCHEMA = 'settings/downloadClientCategories/fetchDownloadClientCategorySchema';
|
||||||
|
export const SELECT_DOWNLOAD_CLIENT_CATEGORY_SCHEMA = 'settings/downloadClientCategories/selectDownloadClientCategorySchema';
|
||||||
|
export const SET_DOWNLOAD_CLIENT_CATEGORY_VALUE = 'settings/downloadClientCategories/setDownloadClientCategoryValue';
|
||||||
|
export const SET_DOWNLOAD_CLIENT_CATEGORY_FIELD_VALUE = 'settings/downloadClientCategories/setDownloadClientCategoryFieldValue';
|
||||||
|
export const SAVE_DOWNLOAD_CLIENT_CATEGORY = 'settings/downloadClientCategories/saveDownloadClientCategory';
|
||||||
|
export const DELETE_DOWNLOAD_CLIENT_CATEGORY = 'settings/downloadClientCategories/deleteDownloadClientCategory';
|
||||||
|
export const DELETE_ALL_DOWNLOAD_CLIENT_CATEGORY = 'settings/downloadClientCategories/deleteAllDownloadClientCategory';
|
||||||
|
export const CLEAR_DOWNLOAD_CLIENT_CATEGORIES = 'settings/downloadClientCategories/clearDownloadClientCategories';
|
||||||
|
export const CLEAR_DOWNLOAD_CLIENT_CATEGORY_PENDING = 'settings/downloadClientCategories/clearDownloadClientCategoryPending';
|
||||||
|
//
|
||||||
|
// Action Creators
|
||||||
|
|
||||||
|
export const fetchDownloadClientCategories = createThunk(FETCH_DOWNLOAD_CLIENT_CATEGORIES);
|
||||||
|
export const fetchDownloadClientCategorySchema = createThunk(FETCH_DOWNLOAD_CLIENT_CATEGORY_SCHEMA);
|
||||||
|
export const selectDownloadClientCategorySchema = createAction(SELECT_DOWNLOAD_CLIENT_CATEGORY_SCHEMA);
|
||||||
|
|
||||||
|
export const saveDownloadClientCategory = createThunk(SAVE_DOWNLOAD_CLIENT_CATEGORY);
|
||||||
|
export const deleteDownloadClientCategory = createThunk(DELETE_DOWNLOAD_CLIENT_CATEGORY);
|
||||||
|
export const deleteAllDownloadClientCategory = createThunk(DELETE_ALL_DOWNLOAD_CLIENT_CATEGORY);
|
||||||
|
|
||||||
|
export const setDownloadClientCategoryValue = createAction(SET_DOWNLOAD_CLIENT_CATEGORY_VALUE, (payload) => {
|
||||||
|
return {
|
||||||
|
section,
|
||||||
|
...payload
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export const setDownloadClientCategoryFieldValue = createAction(SET_DOWNLOAD_CLIENT_CATEGORY_FIELD_VALUE, (payload) => {
|
||||||
|
return {
|
||||||
|
section,
|
||||||
|
...payload
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export const clearDownloadClientCategory = createAction(CLEAR_DOWNLOAD_CLIENT_CATEGORIES);
|
||||||
|
|
||||||
|
export const clearDownloadClientCategoryPending = createThunk(CLEAR_DOWNLOAD_CLIENT_CATEGORY_PENDING);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Details
|
||||||
|
|
||||||
|
export default {
|
||||||
|
|
||||||
|
//
|
||||||
|
// State
|
||||||
|
|
||||||
|
defaultState: {
|
||||||
|
isPopulated: false,
|
||||||
|
error: null,
|
||||||
|
isSchemaFetching: false,
|
||||||
|
isSchemaPopulated: false,
|
||||||
|
schemaError: null,
|
||||||
|
schema: [],
|
||||||
|
selectedSchema: {},
|
||||||
|
isSaving: false,
|
||||||
|
saveError: null,
|
||||||
|
items: [],
|
||||||
|
pendingChanges: {}
|
||||||
|
},
|
||||||
|
|
||||||
|
//
|
||||||
|
// Action Handlers
|
||||||
|
|
||||||
|
actionHandlers: {
|
||||||
|
[FETCH_DOWNLOAD_CLIENT_CATEGORIES]: (getState, payload, dispatch) => {
|
||||||
|
let tags = [];
|
||||||
|
if (payload.id) {
|
||||||
|
const cfState = getSectionState(getState(), 'settings.downloadClients', true);
|
||||||
|
const cf = cfState.items[cfState.itemMap[payload.id]];
|
||||||
|
tags = cf.categories.map((tag, i) => {
|
||||||
|
return {
|
||||||
|
id: i + 1,
|
||||||
|
...tag
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(batchActions([
|
||||||
|
update({ section, data: tags }),
|
||||||
|
set({
|
||||||
|
section,
|
||||||
|
isPopulated: true
|
||||||
|
})
|
||||||
|
]));
|
||||||
|
},
|
||||||
|
|
||||||
|
[SAVE_DOWNLOAD_CLIENT_CATEGORY]: (getState, payload, dispatch) => {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
...otherPayload
|
||||||
|
} = payload;
|
||||||
|
|
||||||
|
const saveData = getProviderState({ id, ...otherPayload }, getState, section, false);
|
||||||
|
|
||||||
|
// we have to set id since not actually posting to server yet
|
||||||
|
if (!saveData.id) {
|
||||||
|
saveData.id = getNextId(getState().settings.downloadClientCategories.items);
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(batchActions([
|
||||||
|
updateItem({ section, ...saveData }),
|
||||||
|
set({
|
||||||
|
section,
|
||||||
|
pendingChanges: {}
|
||||||
|
})
|
||||||
|
]));
|
||||||
|
},
|
||||||
|
|
||||||
|
[DELETE_DOWNLOAD_CLIENT_CATEGORY]: (getState, payload, dispatch) => {
|
||||||
|
const id = payload.id;
|
||||||
|
return dispatch(removeItem({ section, id }));
|
||||||
|
},
|
||||||
|
|
||||||
|
[DELETE_ALL_DOWNLOAD_CLIENT_CATEGORY]: (getState, payload, dispatch) => {
|
||||||
|
return dispatch(set({
|
||||||
|
section,
|
||||||
|
items: []
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
[CLEAR_DOWNLOAD_CLIENT_CATEGORY_PENDING]: (getState, payload, dispatch) => {
|
||||||
|
return dispatch(set({
|
||||||
|
section,
|
||||||
|
pendingChanges: {}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
//
|
||||||
|
// Reducers
|
||||||
|
|
||||||
|
reducers: {
|
||||||
|
[SET_DOWNLOAD_CLIENT_CATEGORY_VALUE]: createSetSettingValueReducer(section),
|
||||||
|
[SET_DOWNLOAD_CLIENT_CATEGORY_FIELD_VALUE]: createSetProviderFieldValueReducer(section),
|
||||||
|
|
||||||
|
[SELECT_DOWNLOAD_CLIENT_CATEGORY_SCHEMA]: (state, { payload }) => {
|
||||||
|
return selectProviderSchema(state, section, payload, (selectedSchema) => {
|
||||||
|
return selectedSchema;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
[CLEAR_DOWNLOAD_CLIENT_CATEGORIES]: createClearReducer(section, {
|
||||||
|
isPopulated: false,
|
||||||
|
error: null,
|
||||||
|
items: []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -9,6 +9,7 @@ import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/
|
|||||||
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
||||||
import { createThunk } from 'Store/thunks';
|
import { createThunk } from 'Store/thunks';
|
||||||
import selectProviderSchema from 'Utilities/State/selectProviderSchema';
|
import selectProviderSchema from 'Utilities/State/selectProviderSchema';
|
||||||
|
import { set } from '../baseActions';
|
||||||
|
|
||||||
//
|
//
|
||||||
// Variables
|
// Variables
|
||||||
@@ -90,10 +91,34 @@ export default {
|
|||||||
[FETCH_DOWNLOAD_CLIENTS]: createFetchHandler(section, '/downloadclient'),
|
[FETCH_DOWNLOAD_CLIENTS]: createFetchHandler(section, '/downloadclient'),
|
||||||
[FETCH_DOWNLOAD_CLIENT_SCHEMA]: createFetchSchemaHandler(section, '/downloadclient/schema'),
|
[FETCH_DOWNLOAD_CLIENT_SCHEMA]: createFetchSchemaHandler(section, '/downloadclient/schema'),
|
||||||
|
|
||||||
[SAVE_DOWNLOAD_CLIENT]: createSaveProviderHandler(section, '/downloadclient'),
|
[SAVE_DOWNLOAD_CLIENT]: (getState, payload, dispatch) => {
|
||||||
|
// move the format tags in as a pending change
|
||||||
|
const state = getState();
|
||||||
|
const pendingChanges = state.settings.downloadClients.pendingChanges;
|
||||||
|
pendingChanges.categories = state.settings.downloadClientCategories.items;
|
||||||
|
dispatch(set({
|
||||||
|
section,
|
||||||
|
pendingChanges
|
||||||
|
}));
|
||||||
|
|
||||||
|
createSaveProviderHandler(section, '/downloadclient')(getState, payload, dispatch);
|
||||||
|
},
|
||||||
|
|
||||||
[CANCEL_SAVE_DOWNLOAD_CLIENT]: createCancelSaveProviderHandler(section),
|
[CANCEL_SAVE_DOWNLOAD_CLIENT]: createCancelSaveProviderHandler(section),
|
||||||
[DELETE_DOWNLOAD_CLIENT]: createRemoveItemHandler(section, '/downloadclient'),
|
[DELETE_DOWNLOAD_CLIENT]: createRemoveItemHandler(section, '/downloadclient'),
|
||||||
[TEST_DOWNLOAD_CLIENT]: createTestProviderHandler(section, '/downloadclient'),
|
|
||||||
|
[TEST_DOWNLOAD_CLIENT]: (getState, payload, dispatch) => {
|
||||||
|
const state = getState();
|
||||||
|
const pendingChanges = state.settings.downloadClients.pendingChanges;
|
||||||
|
pendingChanges.categories = state.settings.downloadClientCategories.items;
|
||||||
|
dispatch(set({
|
||||||
|
section,
|
||||||
|
pendingChanges
|
||||||
|
}));
|
||||||
|
|
||||||
|
createTestProviderHandler(section, '/downloadclient')(getState, payload, dispatch);
|
||||||
|
},
|
||||||
|
|
||||||
[CANCEL_TEST_DOWNLOAD_CLIENT]: createCancelTestProviderHandler(section),
|
[CANCEL_TEST_DOWNLOAD_CLIENT]: createCancelTestProviderHandler(section),
|
||||||
[TEST_ALL_DOWNLOAD_CLIENTS]: createTestAllProvidersHandler(section, '/downloadclient')
|
[TEST_ALL_DOWNLOAD_CLIENTS]: createTestAllProvidersHandler(section, '/downloadclient')
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -310,8 +310,6 @@ export const actionHandlers = handleThunks({
|
|||||||
isGrabbing: true
|
isGrabbing: true
|
||||||
}));
|
}));
|
||||||
|
|
||||||
console.log(payload);
|
|
||||||
|
|
||||||
const promise = createAjaxRequest({
|
const promise = createAjaxRequest({
|
||||||
url: '/search/bulk',
|
url: '/search/bulk',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import createHandleActions from './Creators/createHandleActions';
|
|||||||
import applications from './Settings/applications';
|
import applications from './Settings/applications';
|
||||||
import appProfiles from './Settings/appProfiles';
|
import appProfiles from './Settings/appProfiles';
|
||||||
import development from './Settings/development';
|
import development from './Settings/development';
|
||||||
|
import downloadClientCategories from './Settings/downloadClientCategories';
|
||||||
import downloadClients from './Settings/downloadClients';
|
import downloadClients from './Settings/downloadClients';
|
||||||
import general from './Settings/general';
|
import general from './Settings/general';
|
||||||
import indexerCategories from './Settings/indexerCategories';
|
import indexerCategories from './Settings/indexerCategories';
|
||||||
@@ -11,6 +12,7 @@ import indexerProxies from './Settings/indexerProxies';
|
|||||||
import notifications from './Settings/notifications';
|
import notifications from './Settings/notifications';
|
||||||
import ui from './Settings/ui';
|
import ui from './Settings/ui';
|
||||||
|
|
||||||
|
export * from './Settings/downloadClientCategories';
|
||||||
export * from './Settings/downloadClients';
|
export * from './Settings/downloadClients';
|
||||||
export * from './Settings/general';
|
export * from './Settings/general';
|
||||||
export * from './Settings/indexerCategories';
|
export * from './Settings/indexerCategories';
|
||||||
@@ -32,6 +34,7 @@ export const section = 'settings';
|
|||||||
export const defaultState = {
|
export const defaultState = {
|
||||||
advancedSettings: false,
|
advancedSettings: false,
|
||||||
|
|
||||||
|
downloadClientCategories: downloadClientCategories.defaultState,
|
||||||
downloadClients: downloadClients.defaultState,
|
downloadClients: downloadClients.defaultState,
|
||||||
general: general.defaultState,
|
general: general.defaultState,
|
||||||
indexerCategories: indexerCategories.defaultState,
|
indexerCategories: indexerCategories.defaultState,
|
||||||
@@ -61,6 +64,7 @@ export const toggleAdvancedSettings = createAction(TOGGLE_ADVANCED_SETTINGS);
|
|||||||
// Action Handlers
|
// Action Handlers
|
||||||
|
|
||||||
export const actionHandlers = handleThunks({
|
export const actionHandlers = handleThunks({
|
||||||
|
...downloadClientCategories.actionHandlers,
|
||||||
...downloadClients.actionHandlers,
|
...downloadClients.actionHandlers,
|
||||||
...general.actionHandlers,
|
...general.actionHandlers,
|
||||||
...indexerCategories.actionHandlers,
|
...indexerCategories.actionHandlers,
|
||||||
@@ -81,6 +85,7 @@ export const reducers = createHandleActions({
|
|||||||
return Object.assign({}, state, { advancedSettings: !state.advancedSettings });
|
return Object.assign({}, state, { advancedSettings: !state.advancedSettings });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
...downloadClientCategories.reducers,
|
||||||
...downloadClients.reducers,
|
...downloadClients.reducers,
|
||||||
...general.reducers,
|
...general.reducers,
|
||||||
...indexerCategories.reducers,
|
...indexerCategories.reducers,
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import * as dark from './dark';
|
import * as dark from './dark';
|
||||||
import * as light from './light';
|
import * as light from './light';
|
||||||
|
|
||||||
|
const defaultDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
const auto = defaultDark ? { ...dark } : { ...light };
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
auto,
|
||||||
light,
|
light,
|
||||||
dark
|
dark
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import filesize from 'filesize';
|
import { filesize } from 'filesize';
|
||||||
|
|
||||||
function formatBytes(input) {
|
function formatBytes(input) {
|
||||||
const size = Number(input);
|
const size = Number(input);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<!-- Windows Phone -->
|
<!-- Windows Phone -->
|
||||||
<meta name="msapplication-navbutton-color" content="#3a3f51" />
|
<meta name="msapplication-navbutton-color" content="#3a3f51" />
|
||||||
|
|
||||||
<meta name="description" content="Prowlarr (Preview)" />
|
<meta name="description" content="Prowlarr" />
|
||||||
|
|
||||||
<link
|
<link
|
||||||
rel="apple-touch-icon"
|
rel="apple-touch-icon"
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
<link rel="stylesheet" type="text/css" href="/Content/Fonts/fonts.css">
|
<link rel="stylesheet" type="text/css" href="/Content/Fonts/fonts.css">
|
||||||
<!-- webpack bundles head -->
|
<!-- webpack bundles head -->
|
||||||
|
|
||||||
<title>Prowlarr (Preview)</title>
|
<title>Prowlarr</title>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
The super basic styling for .root will live here,
|
The super basic styling for .root will live here,
|
||||||
|
|||||||
+54
-53
@@ -25,107 +25,108 @@
|
|||||||
"not chrome < 60"
|
"not chrome < 60"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "6.1.1",
|
"@fortawesome/fontawesome-free": "6.2.1",
|
||||||
"@fortawesome/fontawesome-svg-core": "6.1.1",
|
"@fortawesome/fontawesome-svg-core": "6.2.1",
|
||||||
"@fortawesome/free-regular-svg-icons": "6.1.1",
|
"@fortawesome/free-regular-svg-icons": "6.2.1",
|
||||||
"@fortawesome/free-solid-svg-icons": "6.1.1",
|
"@fortawesome/free-solid-svg-icons": "6.2.1",
|
||||||
"@fortawesome/react-fontawesome": "0.1.18",
|
"@fortawesome/react-fontawesome": "0.2.0",
|
||||||
"@microsoft/signalr": "6.0.6",
|
"@microsoft/signalr": "6.0.11",
|
||||||
"@sentry/browser": "6.19.2",
|
"@sentry/browser": "7.28.0",
|
||||||
"@sentry/integrations": "6.19.2",
|
"@sentry/integrations": "7.28.0",
|
||||||
"chart.js": "3.7.1",
|
"chart.js": "4.1.1",
|
||||||
"classnames": "2.3.1",
|
"classnames": "2.3.2",
|
||||||
"clipboard": "2.0.10",
|
"clipboard": "2.0.11",
|
||||||
"connected-react-router": "6.9.1",
|
"connected-react-router": "6.9.3",
|
||||||
"element-class": "0.2.2",
|
"element-class": "0.2.2",
|
||||||
"filesize": "6.3.0",
|
"filesize": "10.0.6",
|
||||||
"history": "4.10.1",
|
"history": "4.10.1",
|
||||||
"https-browserify": "1.0.0",
|
"https-browserify": "1.0.0",
|
||||||
"jdu": "1.0.0",
|
"jdu": "1.0.0",
|
||||||
"jquery": "3.6.0",
|
"jquery": "3.6.2",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"mobile-detect": "1.4.5",
|
"mobile-detect": "1.4.5",
|
||||||
"moment": "2.29.2",
|
"moment": "2.29.4",
|
||||||
"mousetrap": "1.6.5",
|
"mousetrap": "1.6.5",
|
||||||
"normalize.css": "8.0.1",
|
"normalize.css": "8.0.1",
|
||||||
"prop-types": "15.8.1",
|
"prop-types": "15.8.1",
|
||||||
"qs": "6.10.3",
|
"qs": "6.11.0",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-addons-shallow-compare": "15.6.3",
|
"react-addons-shallow-compare": "15.6.3",
|
||||||
"react-async-script": "1.2.0",
|
"react-async-script": "1.2.0",
|
||||||
"react-autosuggest": "10.1.0",
|
"react-autosuggest": "10.1.0",
|
||||||
"react-custom-scrollbars-2": "4.4.0",
|
"react-custom-scrollbars-2": "4.5.0",
|
||||||
"react-dnd": "14.0.4",
|
"react-dnd": "14.0.4",
|
||||||
"react-dnd-html5-backend": "14.0.2",
|
"react-dnd-html5-backend": "14.0.2",
|
||||||
"react-dnd-multi-backend": "6.0.2",
|
"react-dnd-multi-backend": "6.0.2",
|
||||||
"react-dnd-touch-backend": "14.1.1",
|
"react-dnd-touch-backend": "14.1.1",
|
||||||
"react-document-title": "2.0.3",
|
"react-document-title": "2.0.3",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
"react-focus-lock": "2.5.0",
|
"react-focus-lock": "2.9.2",
|
||||||
"react-google-recaptcha": "2.1.0",
|
"react-google-recaptcha": "2.1.0",
|
||||||
"react-lazyload": "3.2.0",
|
"react-lazyload": "3.2.0",
|
||||||
"react-measure": "1.4.7",
|
"react-measure": "1.4.7",
|
||||||
"react-popper": "1.3.7",
|
"react-popper": "1.3.7",
|
||||||
"react-redux": "7.2.4",
|
"react-redux": "8.0.5",
|
||||||
"react-router": "5.2.0",
|
"react-router": "5.2.0",
|
||||||
"react-router-dom": "5.2.0",
|
"react-router-dom": "5.2.0",
|
||||||
|
"react-text-truncate": "0.19.0",
|
||||||
"react-virtualized": "9.21.1",
|
"react-virtualized": "9.21.1",
|
||||||
"redux": "4.1.0",
|
"redux": "4.2.0",
|
||||||
"redux-actions": "2.6.5",
|
"redux-actions": "2.6.5",
|
||||||
"redux-batched-actions": "0.5.0",
|
"redux-batched-actions": "0.5.0",
|
||||||
"redux-localstorage": "0.4.1",
|
"redux-localstorage": "0.4.1",
|
||||||
"redux-thunk": "2.3.0",
|
"redux-thunk": "2.4.2",
|
||||||
"reselect": "4.0.0"
|
"reselect": "4.1.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.18.2",
|
"@babel/core": "7.20.5",
|
||||||
"@babel/eslint-parser": "7.18.2",
|
"@babel/eslint-parser": "7.19.1",
|
||||||
"@babel/plugin-proposal-class-properties": "7.17.12",
|
"@babel/plugin-proposal-class-properties": "7.18.6",
|
||||||
"@babel/plugin-proposal-decorators": "7.18.2",
|
"@babel/plugin-proposal-decorators": "7.20.5",
|
||||||
"@babel/plugin-proposal-export-default-from": "7.17.12",
|
"@babel/plugin-proposal-export-default-from": "7.18.10",
|
||||||
"@babel/plugin-proposal-export-namespace-from": "7.17.12",
|
"@babel/plugin-proposal-export-namespace-from": "7.18.9",
|
||||||
"@babel/plugin-proposal-function-sent": "7.18.2",
|
"@babel/plugin-proposal-function-sent": "7.18.6",
|
||||||
"@babel/plugin-proposal-nullish-coalescing-operator": "7.17.12",
|
"@babel/plugin-proposal-nullish-coalescing-operator": "7.18.6",
|
||||||
"@babel/plugin-proposal-numeric-separator": "7.16.7",
|
"@babel/plugin-proposal-numeric-separator": "7.18.6",
|
||||||
"@babel/plugin-proposal-optional-chaining": "7.17.12",
|
"@babel/plugin-proposal-optional-chaining": "7.18.9",
|
||||||
"@babel/plugin-proposal-throw-expressions": "7.16.7",
|
"@babel/plugin-proposal-throw-expressions": "7.18.6",
|
||||||
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
||||||
"@babel/preset-env": "7.18.2",
|
"@babel/preset-env": "7.20.2",
|
||||||
"@babel/preset-react": "7.17.12",
|
"@babel/preset-react": "7.18.6",
|
||||||
"autoprefixer": "10.4.7",
|
"autoprefixer": "10.4.13",
|
||||||
"babel-loader": "8.2.5",
|
"babel-loader": "9.1.0",
|
||||||
"babel-plugin-inline-classnames": "2.0.1",
|
"babel-plugin-inline-classnames": "2.0.1",
|
||||||
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
|
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
|
||||||
"core-js": "3.22.8",
|
"core-js": "3.26.1",
|
||||||
"css-loader": "6.7.1",
|
"css-loader": "6.7.3",
|
||||||
"eslint": "8.17.0",
|
"eslint": "8.30.0",
|
||||||
"eslint-plugin-filenames": "1.3.2",
|
"eslint-plugin-filenames": "1.3.2",
|
||||||
"eslint-plugin-import": "2.26.0",
|
"eslint-plugin-import": "2.26.0",
|
||||||
"eslint-plugin-react": "7.30.0",
|
"eslint-plugin-react": "7.31.11",
|
||||||
"eslint-plugin-simple-import-sort": "7.0.0",
|
"eslint-plugin-simple-import-sort": "8.0.0",
|
||||||
"esprint": "3.6.0",
|
"esprint": "3.6.0",
|
||||||
"file-loader": "6.2.0",
|
"file-loader": "6.2.0",
|
||||||
"filemanager-webpack-plugin": "6.1.7",
|
"filemanager-webpack-plugin": "8.0.0",
|
||||||
"html-webpack-plugin": "5.5.0",
|
"html-webpack-plugin": "5.5.0",
|
||||||
"loader-utils": "^3.0.0",
|
"loader-utils": "^3.2.1",
|
||||||
"mini-css-extract-plugin": "2.6.0",
|
"mini-css-extract-plugin": "2.7.2",
|
||||||
"postcss": "8.4.14",
|
"postcss": "8.4.20",
|
||||||
"postcss-color-function": "4.1.0",
|
"postcss-color-function": "4.1.0",
|
||||||
"postcss-loader": "6.2.1",
|
"postcss-loader": "7.0.2",
|
||||||
"postcss-mixins": "9.0.2",
|
"postcss-mixins": "9.0.4",
|
||||||
"postcss-nested": "5.0.6",
|
"postcss-nested": "6.0.0",
|
||||||
"postcss-simple-vars": "6.0.3",
|
"postcss-simple-vars": "7.0.1",
|
||||||
"postcss-url": "10.1.3",
|
"postcss-url": "10.1.3",
|
||||||
"require-nocache": "1.0.0",
|
"require-nocache": "1.0.0",
|
||||||
"rimraf": "3.0.2",
|
"rimraf": "3.0.2",
|
||||||
"run-sequence": "2.2.1",
|
"run-sequence": "2.2.1",
|
||||||
"streamqueue": "1.1.2",
|
"streamqueue": "1.1.2",
|
||||||
"style-loader": "3.3.1",
|
"style-loader": "3.3.1",
|
||||||
"stylelint": "14.8.5",
|
"stylelint": "14.16.0",
|
||||||
"stylelint-order": "5.0.0",
|
"stylelint-order": "5.0.0",
|
||||||
"url-loader": "4.1.1",
|
"url-loader": "4.1.1",
|
||||||
"webpack": "5.73.0",
|
"webpack": "5.75.0",
|
||||||
"webpack-cli": "4.9.2",
|
"webpack-cli": "5.0.1",
|
||||||
"webpack-livereload-plugin": "3.0.2"
|
"webpack-livereload-plugin": "3.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,7 +94,7 @@
|
|||||||
|
|
||||||
<!-- Standard testing packages -->
|
<!-- Standard testing packages -->
|
||||||
<ItemGroup Condition="'$(TestProject)'=='true'">
|
<ItemGroup Condition="'$(TestProject)'=='true'">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
|
||||||
<PackageReference Include="NunitXml.TestLogger" Version="3.0.117" />
|
<PackageReference Include="NunitXml.TestLogger" Version="3.0.117" />
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ namespace NzbDrone.Automation.Test
|
|||||||
|
|
||||||
_runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger(), null);
|
_runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger(), null);
|
||||||
_runner.KillAll();
|
_runner.KillAll();
|
||||||
_runner.Start();
|
_runner.Start(true);
|
||||||
|
|
||||||
driver.Url = "http://localhost:9696";
|
driver.Url = "http://localhost:9696";
|
||||||
|
|
||||||
|
|||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using FluentAssertions;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
|
|
||||||
|
namespace NzbDrone.Common.Test.ExtensionTests.StringExtensionTests
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class IsValidIPAddressFixture
|
||||||
|
{
|
||||||
|
[TestCase("192.168.0.1")]
|
||||||
|
[TestCase("::1")]
|
||||||
|
[TestCase("2001:db8:4006:812::200e")]
|
||||||
|
public void should_validate_ip_address(string input)
|
||||||
|
{
|
||||||
|
input.IsValidIpAddress().Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase("sonarr.tv")]
|
||||||
|
public void should_not_parse_non_ip_address(string input)
|
||||||
|
{
|
||||||
|
input.IsValidIpAddress().Should().BeFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -212,6 +212,7 @@ namespace NzbDrone.Common.Test.Http
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
[Platform(Exclude = "MacOsX", Reason = "Azure agent update prevents brotli on OSX")]
|
||||||
public void should_execute_get_using_brotli()
|
public void should_execute_get_using_brotli()
|
||||||
{
|
{
|
||||||
var request = new HttpRequest($"https://{_httpBinHost}/brotli");
|
var request = new HttpRequest($"https://{_httpBinHost}/brotli");
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NzbDrone.Common.Http;
|
using NzbDrone.Common.Http;
|
||||||
using NzbDrone.Test.Common;
|
using NzbDrone.Test.Common;
|
||||||
@@ -10,6 +10,7 @@ namespace NzbDrone.Common.Test.Http
|
|||||||
[TestCase("abc://my_host.com:8080/root/api/")]
|
[TestCase("abc://my_host.com:8080/root/api/")]
|
||||||
[TestCase("abc://my_host.com:8080//root/api/")]
|
[TestCase("abc://my_host.com:8080//root/api/")]
|
||||||
[TestCase("abc://my_host.com:8080/root//api/")]
|
[TestCase("abc://my_host.com:8080/root//api/")]
|
||||||
|
[TestCase("abc://[::1]:8080/root//api/")]
|
||||||
public void should_parse(string uri)
|
public void should_parse(string uri)
|
||||||
{
|
{
|
||||||
var newUri = new HttpUri(uri);
|
var newUri = new HttpUri(uri);
|
||||||
|
|||||||
@@ -7,34 +7,50 @@ namespace NzbDrone.Common.Extensions
|
|||||||
{
|
{
|
||||||
public static bool IsLocalAddress(this IPAddress ipAddress)
|
public static bool IsLocalAddress(this IPAddress ipAddress)
|
||||||
{
|
{
|
||||||
if (ipAddress.IsIPv6LinkLocal)
|
// Map back to IPv4 if mapped to IPv6, for example "::ffff:1.2.3.4" to "1.2.3.4".
|
||||||
|
if (ipAddress.IsIPv4MappedToIPv6)
|
||||||
{
|
{
|
||||||
return true;
|
ipAddress = ipAddress.MapToIPv4();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Checks loopback ranges for both IPv4 and IPv6.
|
||||||
if (IPAddress.IsLoopback(ipAddress))
|
if (IPAddress.IsLoopback(ipAddress))
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IPv4
|
||||||
if (ipAddress.AddressFamily == AddressFamily.InterNetwork)
|
if (ipAddress.AddressFamily == AddressFamily.InterNetwork)
|
||||||
{
|
{
|
||||||
byte[] bytes = ipAddress.GetAddressBytes();
|
return IsLocalIPv4(ipAddress.GetAddressBytes());
|
||||||
switch (bytes[0])
|
}
|
||||||
{
|
|
||||||
case 10:
|
// IPv6
|
||||||
case 127:
|
if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6)
|
||||||
return true;
|
{
|
||||||
case 172:
|
return ipAddress.IsIPv6LinkLocal ||
|
||||||
return bytes[1] < 32 && bytes[1] >= 16;
|
ipAddress.IsIPv6UniqueLocal ||
|
||||||
case 192:
|
ipAddress.IsIPv6SiteLocal;
|
||||||
return bytes[1] == 168;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IsLocalIPv4(byte[] ipv4Bytes)
|
||||||
|
{
|
||||||
|
// Link local (no IP assigned by DHCP): 169.254.0.0 to 169.254.255.255 (169.254.0.0/16)
|
||||||
|
bool IsLinkLocal() => ipv4Bytes[0] == 169 && ipv4Bytes[1] == 254;
|
||||||
|
|
||||||
|
// Class A private range: 10.0.0.0 – 10.255.255.255 (10.0.0.0/8)
|
||||||
|
bool IsClassA() => ipv4Bytes[0] == 10;
|
||||||
|
|
||||||
|
// Class B private range: 172.16.0.0 – 172.31.255.255 (172.16.0.0/12)
|
||||||
|
bool IsClassB() => ipv4Bytes[0] == 172 && ipv4Bytes[1] >= 16 && ipv4Bytes[1] <= 31;
|
||||||
|
|
||||||
|
// Class C private range: 192.168.0.0 – 192.168.255.255 (192.168.0.0/16)
|
||||||
|
bool IsClassC() => ipv4Bytes[0] == 192 && ipv4Bytes[1] == 168;
|
||||||
|
|
||||||
|
return IsLinkLocal() || IsClassA() || IsClassC() || IsClassB();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
@@ -231,5 +232,30 @@ namespace NzbDrone.Common.Extensions
|
|||||||
.Replace("'", "%27")
|
.Replace("'", "%27")
|
||||||
.Replace("%7E", "~");
|
.Replace("%7E", "~");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool IsValidIpAddress(this string value)
|
||||||
|
{
|
||||||
|
if (!IPAddress.TryParse(value, out var parsedAddress))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedAddress.Equals(IPAddress.Parse("255.255.255.255")))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedAddress.IsIPv6Multicast)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedAddress.AddressFamily == AddressFamily.InterNetwork || parsedAddress.AddressFamily == AddressFamily.InterNetworkV6;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ToUrlHost(this string input)
|
||||||
|
{
|
||||||
|
return input.Contains(":") ? $"[{input}]" : input;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ namespace NzbDrone.Common.Http
|
|||||||
{
|
{
|
||||||
public class HttpUri : IEquatable<HttpUri>
|
public class HttpUri : IEquatable<HttpUri>
|
||||||
{
|
{
|
||||||
private static readonly Regex RegexUri = new Regex(@"^(?:(?<scheme>[a-z]+):)?(?://(?<host>[-_A-Z0-9.]+)(?::(?<port>[0-9]{1,5}))?)?(?<path>(?:(?:(?<=^)|/+)[^/?#\r\n]+)+/*|/+)?(?:\?(?<query>[^#\r\n]*))?(?:\#(?<fragment>.*))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
private static readonly Regex RegexUri = new Regex(@"^(?:(?<scheme>[a-z]+):)?(?://(?<host>[-_A-Z0-9.]+|\[[[A-F0-9:]+\])(?::(?<port>[0-9]{1,5}))?)?(?<path>(?:(?:(?<=^)|/+)[^/?#\r\n]+)+/*|/+)?(?:\?(?<query>[^#\r\n]*))?(?:\#(?<fragment>.*))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
private readonly string _uri;
|
private readonly string _uri;
|
||||||
public string FullUri => _uri;
|
public string FullUri => _uri;
|
||||||
@@ -70,6 +70,8 @@ namespace NzbDrone.Common.Http
|
|||||||
|
|
||||||
private void Parse()
|
private void Parse()
|
||||||
{
|
{
|
||||||
|
var parseSuccess = Uri.TryCreate(_uri, UriKind.RelativeOrAbsolute, out var uri);
|
||||||
|
|
||||||
var match = RegexUri.Match(_uri);
|
var match = RegexUri.Match(_uri);
|
||||||
|
|
||||||
var scheme = match.Groups["scheme"];
|
var scheme = match.Groups["scheme"];
|
||||||
@@ -79,7 +81,7 @@ namespace NzbDrone.Common.Http
|
|||||||
var query = match.Groups["query"];
|
var query = match.Groups["query"];
|
||||||
var fragment = match.Groups["fragment"];
|
var fragment = match.Groups["fragment"];
|
||||||
|
|
||||||
if (!match.Success || (scheme.Success && !host.Success && path.Success))
|
if (!parseSuccess || (scheme.Success && !host.Success && path.Success))
|
||||||
{
|
{
|
||||||
throw new ArgumentException("Uri didn't match expected pattern: " + _uri);
|
throw new ArgumentException("Uri didn't match expected pattern: " + _uri);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace NzbDrone.Common.Http
|
namespace NzbDrone.Common.Http
|
||||||
{
|
{
|
||||||
@@ -25,5 +25,11 @@ namespace NzbDrone.Common.Http
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public TooManyRequestsException(HttpRequest request, HttpResponse response, TimeSpan retryWait)
|
||||||
|
: base(request, response)
|
||||||
|
{
|
||||||
|
RetryAfter = retryWait;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,26 +11,41 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
sentryEvent.Message = CleanseLogMessage.Cleanse(sentryEvent.Message.Message);
|
if (sentryEvent.Message is not null)
|
||||||
|
{
|
||||||
|
sentryEvent.Message.Formatted = CleanseLogMessage.Cleanse(sentryEvent.Message.Formatted);
|
||||||
|
sentryEvent.Message.Message = CleanseLogMessage.Cleanse(sentryEvent.Message.Message);
|
||||||
|
sentryEvent.Message.Params = sentryEvent.Message.Params?.Select(x => CleanseLogMessage.Cleanse(x switch
|
||||||
|
{
|
||||||
|
string str => str,
|
||||||
|
_ => x.ToString()
|
||||||
|
})).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
if (sentryEvent.Fingerprint != null)
|
if (sentryEvent.Fingerprint.Any())
|
||||||
{
|
{
|
||||||
var fingerprint = sentryEvent.Fingerprint.Select(x => CleanseLogMessage.Cleanse(x)).ToList();
|
var fingerprint = sentryEvent.Fingerprint.Select(x => CleanseLogMessage.Cleanse(x)).ToList();
|
||||||
sentryEvent.SetFingerprint(fingerprint);
|
sentryEvent.SetFingerprint(fingerprint);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sentryEvent.Extra != null)
|
if (sentryEvent.Extra.Any())
|
||||||
{
|
{
|
||||||
var extras = sentryEvent.Extra.ToDictionary(x => x.Key, y => (object)CleanseLogMessage.Cleanse((string)y.Value));
|
var extras = sentryEvent.Extra.ToDictionary(x => x.Key, y => (object)CleanseLogMessage.Cleanse(y.Value as string));
|
||||||
sentryEvent.SetExtras(extras);
|
sentryEvent.SetExtras(extras);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var exception in sentryEvent.SentryExceptions)
|
if (sentryEvent.SentryExceptions is not null)
|
||||||
{
|
{
|
||||||
exception.Value = CleanseLogMessage.Cleanse(exception.Value);
|
foreach (var exception in sentryEvent.SentryExceptions)
|
||||||
foreach (var frame in exception.Stacktrace.Frames)
|
|
||||||
{
|
{
|
||||||
frame.FileName = ShortenPath(frame.FileName);
|
exception.Value = CleanseLogMessage.Cleanse(exception.Value);
|
||||||
|
if (exception.Stacktrace is not null)
|
||||||
|
{
|
||||||
|
foreach (var frame in exception.Stacktrace.Frames)
|
||||||
|
{
|
||||||
|
frame.FileName = ShortenPath(frame.FileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,10 +42,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
|||||||
"UnauthorizedAccessException",
|
"UnauthorizedAccessException",
|
||||||
|
|
||||||
// Filter out people stuck in boot loops
|
// Filter out people stuck in boot loops
|
||||||
"CorruptDatabaseException",
|
"CorruptDatabaseException"
|
||||||
|
|
||||||
// This also filters some people in boot loops
|
|
||||||
"TinyIoCResolutionException"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public static readonly List<string> FilteredExceptionMessages = new List<string>
|
public static readonly List<string> FilteredExceptionMessages = new List<string>
|
||||||
@@ -102,9 +99,6 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
|||||||
o.Dsn = dsn;
|
o.Dsn = dsn;
|
||||||
o.AttachStacktrace = true;
|
o.AttachStacktrace = true;
|
||||||
o.MaxBreadcrumbs = 200;
|
o.MaxBreadcrumbs = 200;
|
||||||
o.SendDefaultPii = false;
|
|
||||||
o.Debug = false;
|
|
||||||
o.DiagnosticLevel = SentryLevel.Debug;
|
|
||||||
o.Release = BuildInfo.Release;
|
o.Release = BuildInfo.Release;
|
||||||
o.BeforeSend = x => SentryCleanser.CleanseEvent(x);
|
o.BeforeSend = x => SentryCleanser.CleanseEvent(x);
|
||||||
o.BeforeBreadcrumb = x => SentryCleanser.CleanseBreadcrumb(x);
|
o.BeforeBreadcrumb = x => SentryCleanser.CleanseBreadcrumb(x);
|
||||||
@@ -210,7 +204,11 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
|||||||
if (ex != null)
|
if (ex != null)
|
||||||
{
|
{
|
||||||
fingerPrint.Add(ex.GetType().FullName);
|
fingerPrint.Add(ex.GetType().FullName);
|
||||||
fingerPrint.Add(ex.TargetSite.ToString());
|
if (ex.TargetSite != null)
|
||||||
|
{
|
||||||
|
fingerPrint.Add(ex.TargetSite.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
if (ex.InnerException != null)
|
if (ex.InnerException != null)
|
||||||
{
|
{
|
||||||
fingerPrint.Add(ex.InnerException.GetType().FullName);
|
fingerPrint.Add(ex.InnerException.GetType().FullName);
|
||||||
|
|||||||
@@ -4,18 +4,18 @@
|
|||||||
<DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants>
|
<DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="DryIoc.dll" Version="5.2.2" />
|
<PackageReference Include="DryIoc.dll" Version="5.3.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.1" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
|
||||||
<PackageReference Include="NLog" Version="5.0.1" />
|
<PackageReference Include="NLog" Version="5.1.0" />
|
||||||
<PackageReference Include="NLog.Extensions.Logging" Version="5.0.0" />
|
<PackageReference Include="NLog.Extensions.Logging" Version="5.2.0" />
|
||||||
<PackageReference Include="Sentry" Version="3.21.0" />
|
<PackageReference Include="Sentry" Version="3.24.1" />
|
||||||
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
|
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
|
||||||
<PackageReference Include="SharpZipLib" Version="1.3.3" />
|
<PackageReference Include="SharpZipLib" Version="1.3.3" />
|
||||||
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
||||||
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
||||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.0" />
|
<PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.1" />
|
||||||
<PackageReference Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
|
<PackageReference Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
|
||||||
<PackageReference Include="System.Runtime.Loader" Version="4.3.0" />
|
<PackageReference Include="System.Runtime.Loader" Version="4.3.0" />
|
||||||
<PackageReference Include="System.ServiceProcess.ServiceController" Version="6.0.0" />
|
<PackageReference Include="System.ServiceProcess.ServiceController" Version="6.0.0" />
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using FluentAssertions;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Common.Serializer;
|
||||||
|
using NzbDrone.Core.Datastore.Migration;
|
||||||
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Test.Datastore.Migration
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class orpheus_apiFixture : MigrationTest<orpheus_api>
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void should_convert_and_disable_orpheus_instance()
|
||||||
|
{
|
||||||
|
var db = WithMigrationTestDb(c =>
|
||||||
|
{
|
||||||
|
c.Insert.IntoTable("Indexers").Row(new
|
||||||
|
{
|
||||||
|
Enable = true,
|
||||||
|
Name = "Orpheus",
|
||||||
|
Priority = 25,
|
||||||
|
Added = DateTime.UtcNow,
|
||||||
|
Implementation = "Orpheus",
|
||||||
|
Settings = new GazelleIndexerSettings021
|
||||||
|
{
|
||||||
|
Username = "some name",
|
||||||
|
Password = "some pass"
|
||||||
|
}.ToJson(),
|
||||||
|
ConfigContract = "GazelleSettings"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var items = db.Query<IndexerDefinition022>("SELECT \"Id\", \"Enable\", \"ConfigContract\", \"Settings\" FROM \"Indexers\"");
|
||||||
|
|
||||||
|
items.Should().HaveCount(1);
|
||||||
|
items.First().ConfigContract.Should().Be("OrpheusSettings");
|
||||||
|
items.First().Enable.Should().Be(false);
|
||||||
|
items.First().Settings.Should().NotContain("username");
|
||||||
|
items.First().Settings.Should().NotContain("password");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class IndexerDefinition022
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public bool Enable { get; set; }
|
||||||
|
public string ConfigContract { get; set; }
|
||||||
|
public string Settings { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GazelleIndexerSettings021
|
||||||
|
{
|
||||||
|
public string Username { get; set; }
|
||||||
|
public string Password { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
<subcat id="5030" name="SD"/>
|
<subcat id="5030" name="SD"/>
|
||||||
<subcat id="5060" name="Sport"/>
|
<subcat id="5060" name="Sport"/>
|
||||||
<subcat id="5010" name="WEB-DL"/>
|
<subcat id="5010" name="WEB-DL"/>
|
||||||
|
<subcat id="5999" name="Other"/>
|
||||||
</category>
|
</category>
|
||||||
<category id="7000" name="Other">
|
<category id="7000" name="Other">
|
||||||
<subcat id="7010" name="Misc"/>
|
<subcat id="7010" name="Misc"/>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,144 @@
|
|||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"response": {
|
||||||
|
"currentPage": 1,
|
||||||
|
"pages": 1,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"groupId": 2497,
|
||||||
|
"groupName": "Singin' in the Rain",
|
||||||
|
"artist": "Gene Kelly & Stanley Donen",
|
||||||
|
"cover": "https:\/\/www.themoviedb.org\/t\/p\/original\/g2AaJDC2vSRcqHSDH29642xmQd.jpg",
|
||||||
|
"tags": [ "comedy", "musical", "romance" ],
|
||||||
|
"bookmarked": false,
|
||||||
|
"vanityHouse": false,
|
||||||
|
"groupYear": 1952,
|
||||||
|
"releaseType": null,
|
||||||
|
"groupTime": "1671129449",
|
||||||
|
"maxSize": 57473058680,
|
||||||
|
"totalSnatched": 25,
|
||||||
|
"totalSeeders": 9,
|
||||||
|
"totalLeechers": 0,
|
||||||
|
"torrents": [
|
||||||
|
{
|
||||||
|
"torrentId": 3599,
|
||||||
|
"editionId": 1,
|
||||||
|
"artists": [
|
||||||
|
{
|
||||||
|
"id": 126,
|
||||||
|
"name": "Gene Kelly",
|
||||||
|
"aliasid": 127
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 125,
|
||||||
|
"name": "Stanley Donen",
|
||||||
|
"aliasid": 126
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"remastered": false,
|
||||||
|
"remasterYear": 0,
|
||||||
|
"remasterCatalogueNumber": "",
|
||||||
|
"remasterTitle": "",
|
||||||
|
"media": "1080p",
|
||||||
|
"encoding": "",
|
||||||
|
"format": "",
|
||||||
|
"hasLog": false,
|
||||||
|
"logScore": 0,
|
||||||
|
"hasCue": false,
|
||||||
|
"scene": false,
|
||||||
|
"vanityHouse": false,
|
||||||
|
"fileCount": 1,
|
||||||
|
"time": "2017-09-10 11:47:27",
|
||||||
|
"size": 24724893991,
|
||||||
|
"snatches": 14,
|
||||||
|
"seeders": 1,
|
||||||
|
"leechers": 0,
|
||||||
|
"isFreeleech": true,
|
||||||
|
"isNeutralLeech": false,
|
||||||
|
"isPersonalFreeleech": false,
|
||||||
|
"canUseToken": false,
|
||||||
|
"hasSnatched": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"torrentId": 45068,
|
||||||
|
"editionId": 2,
|
||||||
|
"artists": [
|
||||||
|
{
|
||||||
|
"id": 126,
|
||||||
|
"name": "Gene Kelly",
|
||||||
|
"aliasid": 127
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 125,
|
||||||
|
"name": "Stanley Donen",
|
||||||
|
"aliasid": 126
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"remastered": false,
|
||||||
|
"remasterYear": 0,
|
||||||
|
"remasterCatalogueNumber": "",
|
||||||
|
"remasterTitle": "",
|
||||||
|
"media": "2160p",
|
||||||
|
"encoding": "",
|
||||||
|
"format": "",
|
||||||
|
"hasLog": false,
|
||||||
|
"logScore": 0,
|
||||||
|
"hasCue": false,
|
||||||
|
"scene": false,
|
||||||
|
"vanityHouse": false,
|
||||||
|
"fileCount": 1,
|
||||||
|
"time": "2022-12-15 19:37:29",
|
||||||
|
"size": 57473058680,
|
||||||
|
"snatches": 6,
|
||||||
|
"seeders": 8,
|
||||||
|
"leechers": 0,
|
||||||
|
"isFreeleech": true,
|
||||||
|
"isNeutralLeech": false,
|
||||||
|
"isPersonalFreeleech": false,
|
||||||
|
"canUseToken": false,
|
||||||
|
"hasSnatched": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"torrentId": 2726,
|
||||||
|
"editionId": 3,
|
||||||
|
"artists": [
|
||||||
|
{
|
||||||
|
"id": 126,
|
||||||
|
"name": "Gene Kelly",
|
||||||
|
"aliasid": 127
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 125,
|
||||||
|
"name": "Stanley Donen",
|
||||||
|
"aliasid": 126
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"remastered": false,
|
||||||
|
"remasterYear": 0,
|
||||||
|
"remasterCatalogueNumber": "",
|
||||||
|
"remasterTitle": "",
|
||||||
|
"media": "DVD-R",
|
||||||
|
"encoding": "",
|
||||||
|
"format": "",
|
||||||
|
"hasLog": false,
|
||||||
|
"logScore": 0,
|
||||||
|
"hasCue": false,
|
||||||
|
"scene": false,
|
||||||
|
"vanityHouse": false,
|
||||||
|
"fileCount": 37,
|
||||||
|
"time": "2017-08-26 14:58:58",
|
||||||
|
"size": 10350032896,
|
||||||
|
"snatches": 5,
|
||||||
|
"seeders": 0,
|
||||||
|
"leechers": 0,
|
||||||
|
"isFreeleech": true,
|
||||||
|
"isNeutralLeech": false,
|
||||||
|
"isPersonalFreeleech": false,
|
||||||
|
"canUseToken": false,
|
||||||
|
"hasSnatched": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<caps>
|
||||||
|
<server version="1.0" title="Anime Tosho" strapline="Anime NZB/DDL mirror" url="https://animetosho.org/"/>
|
||||||
|
<limits max="200" default="75"/>
|
||||||
|
<retention days="9999"/>
|
||||||
|
<registration available="no" open="yes" />
|
||||||
|
<searching>
|
||||||
|
<search available="yes" supportedParams="q" />
|
||||||
|
<tv-search available="no" supportedParams="q" />
|
||||||
|
<movie-search available="no" supportedParams="q" />
|
||||||
|
</searching>
|
||||||
|
<categories>
|
||||||
|
<category id="5070" name="Anime" description="Anime"/>
|
||||||
|
</categories>
|
||||||
|
</caps>
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<rss version="2.0" xmlns:torznab="http://torznab.com/schemas/2015/feed">
|
||||||
|
<channel>
|
||||||
|
<item>
|
||||||
|
<title>Out of the Past 1947 720p BluRay FLAC2.0 x264-CtrlHD.mkv</title>
|
||||||
|
<guid isPermaLink="true">https://www.morethantv.me/torrents.php?id=(removed)&torrentid=836164</guid>
|
||||||
|
<link>https://www.morethantv.me/torrents.php?action=download&id=(removed)&authkey=(removed)&torrent_pass=(removed)</link>
|
||||||
|
<comments>https://www.morethantv.me/torrents.php?id=(removed)&torrentid=836164</comments>
|
||||||
|
<pubDate>Tue, 20 Dec 2022 21:32:17 +0000</pubDate>
|
||||||
|
<size>5412993028</size>
|
||||||
|
<files>1</files>
|
||||||
|
<grabs>2</grabs>
|
||||||
|
<category>2000</category>
|
||||||
|
<category>2040</category>
|
||||||
|
<description>A private eye escapes his past to run a gas station in a small town, but his past catches up with him. Now he must return to the big city world of danger, corruption, double crosses, and duplicitous dames.</description>
|
||||||
|
<enclosure url="https://www.morethantv.me/torrents.php?action=download&id=(removed)&authkey=(removed)&torrent_pass=(removed)" length="103641" type="application/x-bittorrent" />
|
||||||
|
<torznab:attr name="size" value="5412993028" />
|
||||||
|
<torznab:attr name="poster" value="anon" />
|
||||||
|
<torznab:attr name="seeders" value="3" />
|
||||||
|
<torznab:attr name="leechers" value="0" />
|
||||||
|
<torznab:attr name="peers" value="3" />
|
||||||
|
<torznab:attr name="infohash" value="(removed)" />
|
||||||
|
<torznab:attr name="downloadvolumefactor" value="1" />
|
||||||
|
<torznab:attr name="uploadvolumefactor" value="1" />
|
||||||
|
<torznab:attr name="tag" value="anonymous" />
|
||||||
|
<torznab:attr name="imdb" value="0039689" />
|
||||||
|
<torznab:attr name="imdbid" value="tt0039689" />
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>Out of the Past 1947 1080p USA Blu-ray AVC DTS-HD MA 2.0-PCH</title>
|
||||||
|
<guid isPermaLink="true">https://www.morethantv.me/torrents.php?id=(removed)&torrentid=836165</guid>
|
||||||
|
<link>https://www.morethantv.me/torrents.php?action=download&id=(removed)&authkey=(removed)&torrent_pass=(removed)</link>
|
||||||
|
<comments>https://www.morethantv.me/torrents.php?id=(removed)&torrentid=836165</comments>
|
||||||
|
<pubDate>Tue, 20 Dec 2022 21:47:40 +0000</pubDate>
|
||||||
|
<size>30524085127</size>
|
||||||
|
<files>78</files>
|
||||||
|
<grabs>0</grabs>
|
||||||
|
<category>2000</category>
|
||||||
|
<category>2040</category>
|
||||||
|
<description>A private eye escapes his past to run a gas station in a small town, but his past catches up with him. Now he must return to the big city world of danger, corruption, double crosses, and duplicitous dames.</description>
|
||||||
|
<enclosure url="https://www.morethantv.me/torrents.php?action=download&id=(removed)&authkey=(removed)&torrent_pass=(removed)" length="150224" type="application/x-bittorrent" />
|
||||||
|
<torznab:attr name="size" value="30524085127" />
|
||||||
|
<torznab:attr name="poster" value="anon" />
|
||||||
|
<torznab:attr name="seeders" value="1" />
|
||||||
|
<torznab:attr name="leechers" value="0" />
|
||||||
|
<torznab:attr name="peers" value="1" />
|
||||||
|
<torznab:attr name="infohash" value="(removed)" />
|
||||||
|
<torznab:attr name="downloadvolumefactor" value="1" />
|
||||||
|
<torznab:attr name="uploadvolumefactor" value="1" />
|
||||||
|
<torznab:attr name="tag" value="anonymous" />
|
||||||
|
<torznab:attr name="imdb" value="0039689" />
|
||||||
|
<torznab:attr name="imdbid" value="tt0039689" />
|
||||||
|
</item>
|
||||||
|
</channel>
|
||||||
|
</rss>
|
||||||
@@ -28,15 +28,15 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
|
|||||||
[Test]
|
[Test]
|
||||||
public void should_return_warning_when_branch_not_valid()
|
public void should_return_warning_when_branch_not_valid()
|
||||||
{
|
{
|
||||||
GivenValidBranch("master");
|
GivenValidBranch("test");
|
||||||
|
|
||||||
Subject.Check().ShouldBeWarning();
|
Subject.Check().ShouldBeWarning();
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("Develop")]
|
|
||||||
[TestCase("develop")]
|
|
||||||
[TestCase("nightly")]
|
[TestCase("nightly")]
|
||||||
[TestCase("Nightly")]
|
[TestCase("Nightly")]
|
||||||
|
[TestCase("develop")]
|
||||||
|
[TestCase("master")]
|
||||||
public void should_return_no_warning_when_branch_valid(string branch)
|
public void should_return_no_warning_when_branch_valid(string branch)
|
||||||
{
|
{
|
||||||
GivenValidBranch(branch);
|
GivenValidBranch(branch);
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using FizzWare.NBuilder;
|
||||||
|
using FluentAssertions;
|
||||||
|
using Moq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Core.History;
|
||||||
|
using NzbDrone.Core.Indexers;
|
||||||
|
using NzbDrone.Core.IndexerStats;
|
||||||
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Test.IndexerStatsTests
|
||||||
|
{
|
||||||
|
public class IndexerStatisticsServiceFixture : CoreTest<IndexerStatisticsService>
|
||||||
|
{
|
||||||
|
private IndexerDefinition _indexer;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
_indexer = Builder<IndexerDefinition>.CreateNew().With(x => x.Id = 5).Build();
|
||||||
|
|
||||||
|
Mocker.GetMock<IIndexerFactory>()
|
||||||
|
.Setup(o => o.All())
|
||||||
|
.Returns(new List<IndexerDefinition> { _indexer });
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_pull_stats_if_all_events_are_failures()
|
||||||
|
{
|
||||||
|
var history = new List<History.History>
|
||||||
|
{
|
||||||
|
new History.History
|
||||||
|
{
|
||||||
|
Date = DateTime.UtcNow.AddHours(-1),
|
||||||
|
EventType = HistoryEventType.IndexerRss,
|
||||||
|
Successful = false,
|
||||||
|
Id = 8,
|
||||||
|
IndexerId = 5,
|
||||||
|
Data = new Dictionary<string, string> { { "source", "prowlarr" } }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Mocker.GetMock<IHistoryService>()
|
||||||
|
.Setup(o => o.Between(It.IsAny<DateTime>(), It.IsAny<DateTime>()))
|
||||||
|
.Returns<DateTime, DateTime>((s, f) => history);
|
||||||
|
|
||||||
|
var statistics = Subject.IndexerStatistics(DateTime.UtcNow.AddMonths(-1), DateTime.UtcNow);
|
||||||
|
|
||||||
|
statistics.IndexerStatistics.Count.Should().Be(1);
|
||||||
|
statistics.IndexerStatistics.First().AverageResponseTime.Should().Be(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -51,7 +51,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
|
|||||||
torrentInfo.InfoUrl.Should().Be("https://avistaz.to/torrent/187240-japan-sinks-people-of-hope-2021-s01e05-720p-nf-web-dl-ddp20-x264-seikel");
|
torrentInfo.InfoUrl.Should().Be("https://avistaz.to/torrent/187240-japan-sinks-people-of-hope-2021-s01e05-720p-nf-web-dl-ddp20-x264-seikel");
|
||||||
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||||
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-11-14 23:26:21"));
|
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-11-15 04:26:21"));
|
||||||
torrentInfo.Size.Should().Be(935127615);
|
torrentInfo.Size.Should().Be(935127615);
|
||||||
torrentInfo.InfoHash.Should().Be("a879261d4e6e792402f92401141a21de70d51bf2");
|
torrentInfo.InfoHash.Should().Be("a879261d4e6e792402f92401141a21de70d51bf2");
|
||||||
torrentInfo.MagnetUrl.Should().Be(null);
|
torrentInfo.MagnetUrl.Should().Be(null);
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
|
|||||||
torrentInfo.InfoUrl.Should().Be("https://exoticaz.to/torrent/64040-ssis-419-my-first-experience-is-yua-mikami-from-the-day-i-lost-my-virginity-i-was-devoted-to-sex");
|
torrentInfo.InfoUrl.Should().Be("https://exoticaz.to/torrent/64040-ssis-419-my-first-experience-is-yua-mikami-from-the-day-i-lost-my-virginity-i-was-devoted-to-sex");
|
||||||
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||||
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-06-11 11:04:50"));
|
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-06-11 16:04:50"));
|
||||||
torrentInfo.Size.Should().Be(7085405541);
|
torrentInfo.Size.Should().Be(7085405541);
|
||||||
torrentInfo.InfoHash.Should().Be("asdjfiasdf54asd7f4a2sdf544asdf");
|
torrentInfo.InfoHash.Should().Be("asdjfiasdf54asd7f4a2sdf544asdf");
|
||||||
torrentInfo.MagnetUrl.Should().Be(null);
|
torrentInfo.MagnetUrl.Should().Be(null);
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
|
|||||||
torrentInfo.InfoUrl.Should().Be("https://privatehd.to/torrent/78506-godzilla-2014-2160p-uhd-bluray-remux-hdr-hevc-atmos-triton");
|
torrentInfo.InfoUrl.Should().Be("https://privatehd.to/torrent/78506-godzilla-2014-2160p-uhd-bluray-remux-hdr-hevc-atmos-triton");
|
||||||
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||||
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-03-21 00:24:49"));
|
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-03-21 05:24:49"));
|
||||||
torrentInfo.Size.Should().Be(69914591044);
|
torrentInfo.Size.Should().Be(69914591044);
|
||||||
torrentInfo.InfoHash.Should().Be("a879261d4e6e792402f92401141a21de70d51bf2");
|
torrentInfo.InfoHash.Should().Be("a879261d4e6e792402f92401141a21de70d51bf2");
|
||||||
torrentInfo.MagnetUrl.Should().Be(null);
|
torrentInfo.MagnetUrl.Should().Be(null);
|
||||||
|
|||||||
@@ -71,12 +71,12 @@ namespace NzbDrone.Core.Test.IndexerTests.CardigannTests
|
|||||||
result.Should().Be(expected);
|
result.Should().Be(expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("{{ .Today.Year }}", "2022")]
|
[TestCase("{{ .Today.Year }}")]
|
||||||
public void should_handle_variables_statements(string template, string expected)
|
public void should_handle_variables_statements(string template)
|
||||||
{
|
{
|
||||||
var result = Subject.ApplyGoTemplateText(template, _variables);
|
var result = Subject.ApplyGoTemplateText(template, _variables);
|
||||||
|
|
||||||
result.Should().Be(expected);
|
result.Should().Be(DateTime.Now.Year.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("{{if .False }}0{{else}}1{{end}}", "1")]
|
[TestCase("{{if .False }}0{{else}}1{{end}}", "1")]
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
|
|||||||
torrentInfo.InfoUrl.Should().Be("https://filelist.io/details.php?id=665873");
|
torrentInfo.InfoUrl.Should().Be("https://filelist.io/details.php?id=665873");
|
||||||
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||||
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2020-01-25 22:20:19"));
|
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2020-01-25 20:20:19").ToUniversalTime());
|
||||||
torrentInfo.Size.Should().Be(8300512414);
|
torrentInfo.Size.Should().Be(8300512414);
|
||||||
torrentInfo.InfoHash.Should().Be(null);
|
torrentInfo.InfoHash.Should().Be(null);
|
||||||
torrentInfo.MagnetUrl.Should().Be(null);
|
torrentInfo.MagnetUrl.Should().Be(null);
|
||||||
|
|||||||
+40
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
@@ -70,6 +71,45 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
|||||||
caps.LimitsMax.Value.Should().Be(60);
|
caps.LimitsMax.Value.Should().Be(60);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_map_different_categories()
|
||||||
|
{
|
||||||
|
GivenCapsResponse(_caps);
|
||||||
|
|
||||||
|
var caps = Subject.GetCapabilities(_settings, _definition);
|
||||||
|
|
||||||
|
var bookCats = caps.Categories.MapTorznabCapsToTrackers(new int[] { NewznabStandardCategory.Books.Id });
|
||||||
|
|
||||||
|
bookCats.Count.Should().Be(2);
|
||||||
|
bookCats.Should().Contain("8000");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_find_sub_categories_as_main_categories()
|
||||||
|
{
|
||||||
|
GivenCapsResponse(ReadAllText("Files/Indexers/Torznab/torznab_animetosho_caps.xml"));
|
||||||
|
|
||||||
|
var caps = Subject.GetCapabilities(_settings, _definition);
|
||||||
|
|
||||||
|
var bookCats = caps.Categories.MapTrackerCatToNewznab("5070");
|
||||||
|
|
||||||
|
bookCats.Count.Should().Be(2);
|
||||||
|
bookCats.First().Id.Should().Be(5070);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_map_by_name_when_available()
|
||||||
|
{
|
||||||
|
GivenCapsResponse(_caps);
|
||||||
|
|
||||||
|
var caps = Subject.GetCapabilities(_settings, _definition);
|
||||||
|
|
||||||
|
var bookCats = caps.Categories.MapTrackerCatToNewznab("5999");
|
||||||
|
|
||||||
|
bookCats.Count.Should().Be(2);
|
||||||
|
bookCats.First().Id.Should().Be(5050);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void should_use_default_pagesize_if_missing()
|
public void should_use_default_pagesize_if_missing()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
using System;
|
||||||
|
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.Indexers.Gazelle;
|
||||||
|
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||||
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Test.IndexerTests.OrpheusTests
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class OrpheusFixture : CoreTest<Orpheus>
|
||||||
|
{
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
Subject.Definition = new IndexerDefinition()
|
||||||
|
{
|
||||||
|
Name = "Orpheus",
|
||||||
|
Settings = new OrpheusSettings() { Apikey = "somekey" }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task should_parse_recent_feed_from_Orpheus()
|
||||||
|
{
|
||||||
|
var recentFeed = ReadAllText(@"Files/Indexers/Orpheus/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 int[] { 2000 } })).Releases;
|
||||||
|
|
||||||
|
releases.Should().HaveCount(65);
|
||||||
|
releases.First().Should().BeOfType<GazelleInfo>();
|
||||||
|
|
||||||
|
var torrentInfo = releases.First() as GazelleInfo;
|
||||||
|
|
||||||
|
torrentInfo.Title.Should().Be("The Beatles - Abbey Road (1969) [MP3 V2 (VBR)] [BD]");
|
||||||
|
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
||||||
|
torrentInfo.DownloadUrl.Should().Be("https://orpheus.network/ajax.php?action=download&id=1902448");
|
||||||
|
torrentInfo.InfoUrl.Should().Be("https://orpheus.network/torrents.php?id=466&torrentid=1902448");
|
||||||
|
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||||
|
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||||
|
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-08-08 2:07:39"));
|
||||||
|
torrentInfo.Size.Should().Be(68296866);
|
||||||
|
torrentInfo.InfoHash.Should().Be(null);
|
||||||
|
torrentInfo.MagnetUrl.Should().Be(null);
|
||||||
|
torrentInfo.Peers.Should().Be(0);
|
||||||
|
torrentInfo.Seeders.Should().Be(0);
|
||||||
|
torrentInfo.ImdbId.Should().Be(0);
|
||||||
|
torrentInfo.TmdbId.Should().Be(0);
|
||||||
|
torrentInfo.TvdbId.Should().Be(0);
|
||||||
|
torrentInfo.Languages.Should().HaveCount(0);
|
||||||
|
torrentInfo.Subs.Should().HaveCount(0);
|
||||||
|
torrentInfo.DownloadVolumeFactor.Should().Be(1);
|
||||||
|
torrentInfo.UploadVolumeFactor.Should().Be(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
using System;
|
||||||
|
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.Indexers.Gazelle;
|
||||||
|
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||||
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Test.IndexerTests.SecretCinemaTests
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class SecretCinemaFixture : CoreTest<SecretCinema>
|
||||||
|
{
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
Subject.Definition = new IndexerDefinition()
|
||||||
|
{
|
||||||
|
Name = "SecretCinema",
|
||||||
|
Settings = new GazelleSettings() { Username = "somekey", Password = "somekey" }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task should_parse_recent_feed_from_SecretCinema()
|
||||||
|
{
|
||||||
|
var recentFeed = ReadAllText(@"Files/Indexers/SecretCinema/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 int[] { 2000 } })).Releases;
|
||||||
|
|
||||||
|
releases.Should().HaveCount(3);
|
||||||
|
releases.First().Should().BeOfType<GazelleInfo>();
|
||||||
|
|
||||||
|
var torrentInfo = releases.First() as GazelleInfo;
|
||||||
|
|
||||||
|
torrentInfo.Title.Should().Be("Singin' in the Rain (1952) 2160p");
|
||||||
|
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
||||||
|
torrentInfo.DownloadUrl.Should().Be("https://secret-cinema.pw/torrents.php?action=download&useToken=0&id=45068");
|
||||||
|
torrentInfo.InfoUrl.Should().Be("https://secret-cinema.pw/torrents.php?id=2497&torrentid=45068");
|
||||||
|
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||||
|
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||||
|
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-12-15 19:37:29"));
|
||||||
|
torrentInfo.Size.Should().Be(57473058680);
|
||||||
|
torrentInfo.InfoHash.Should().Be(null);
|
||||||
|
torrentInfo.MagnetUrl.Should().Be(null);
|
||||||
|
torrentInfo.Peers.Should().Be(8);
|
||||||
|
torrentInfo.Seeders.Should().Be(8);
|
||||||
|
torrentInfo.ImdbId.Should().Be(0);
|
||||||
|
torrentInfo.TmdbId.Should().Be(0);
|
||||||
|
torrentInfo.TvdbId.Should().Be(0);
|
||||||
|
torrentInfo.Languages.Should().HaveCount(0);
|
||||||
|
torrentInfo.Subs.Should().HaveCount(0);
|
||||||
|
torrentInfo.DownloadVolumeFactor.Should().Be(0);
|
||||||
|
torrentInfo.UploadVolumeFactor.Should().Be(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,6 +34,10 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests
|
|||||||
};
|
};
|
||||||
|
|
||||||
_caps = new IndexerCapabilities();
|
_caps = new IndexerCapabilities();
|
||||||
|
|
||||||
|
_caps.Categories.AddCategoryMapping(2000, NewznabStandardCategory.Movies, "Movies");
|
||||||
|
_caps.Categories.AddCategoryMapping(2040, NewznabStandardCategory.MoviesHD, "Movies/HD");
|
||||||
|
|
||||||
Mocker.GetMock<INewznabCapabilitiesProvider>()
|
Mocker.GetMock<INewznabCapabilitiesProvider>()
|
||||||
.Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>(), It.IsAny<IndexerDefinition>()))
|
.Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>(), It.IsAny<IndexerDefinition>()))
|
||||||
.Returns(_caps);
|
.Returns(_caps);
|
||||||
@@ -129,6 +133,38 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests
|
|||||||
releaseInfo.Peers.Should().BeNull();
|
releaseInfo.Peers.Should().BeNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task should_parse_recent_feed_from_torznab_morethantv()
|
||||||
|
{
|
||||||
|
var recentFeed = ReadAllText(@"Files/Indexers/Torznab/torznab_morethantv.xml");
|
||||||
|
|
||||||
|
Mocker.GetMock<IIndexerHttpClient>()
|
||||||
|
.Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get), Subject.Definition))
|
||||||
|
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed)));
|
||||||
|
|
||||||
|
var releases = (await Subject.Fetch(new MovieSearchCriteria())).Releases;
|
||||||
|
|
||||||
|
releases.Should().HaveCount(2);
|
||||||
|
|
||||||
|
releases.First().Should().BeOfType<TorrentInfo>();
|
||||||
|
var releaseInfo = releases.First() as TorrentInfo;
|
||||||
|
|
||||||
|
releaseInfo.Title.Should().Be("Out of the Past 1947 720p BluRay FLAC2.0 x264-CtrlHD.mkv");
|
||||||
|
releaseInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
||||||
|
releaseInfo.DownloadUrl.Should().Be("https://www.morethantv.me/torrents.php?action=download&id=(removed)&authkey=(removed)&torrent_pass=(removed)");
|
||||||
|
releaseInfo.InfoUrl.Should().Be("https://www.morethantv.me/torrents.php?id=(removed)&torrentid=836164");
|
||||||
|
releaseInfo.CommentUrl.Should().Be("https://www.morethantv.me/torrents.php?id=(removed)&torrentid=836164");
|
||||||
|
releaseInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||||
|
releaseInfo.PublishDate.Should().Be(DateTime.Parse("Tue, 20 Dec 2022 21:32:17 +0000").ToUniversalTime());
|
||||||
|
releaseInfo.Size.Should().Be(5412993028);
|
||||||
|
releaseInfo.TvdbId.Should().Be(0);
|
||||||
|
releaseInfo.TvRageId.Should().Be(0);
|
||||||
|
releaseInfo.InfoHash.Should().Be("(removed)");
|
||||||
|
releaseInfo.Seeders.Should().Be(3);
|
||||||
|
releaseInfo.Peers.Should().Be(3);
|
||||||
|
releaseInfo.Categories.Count().Should().Be(4);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void should_use_pagesize_reported_by_caps()
|
public void should_use_pagesize_reported_by_caps()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<PackageReference Include="Dapper" Version="2.0.123" />
|
<PackageReference Include="Dapper" Version="2.0.123" />
|
||||||
<PackageReference Include="NBuilder" Version="6.1.0" />
|
<PackageReference Include="NBuilder" Version="6.1.0" />
|
||||||
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
||||||
<PackageReference Include="YamlDotNet" Version="12.0.1" />
|
<PackageReference Include="YamlDotNet" Version="12.3.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\NzbDrone.Test.Common\Prowlarr.Test.Common.csproj" />
|
<ProjectReference Include="..\NzbDrone.Test.Common\Prowlarr.Test.Common.csproj" />
|
||||||
@@ -21,7 +21,4 @@
|
|||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
|
||||||
<Folder Include="Datastore\Migration\" />
|
|
||||||
</ItemGroup>
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ namespace NzbDrone.Core.Test.ThingiProviderTests
|
|||||||
var status = Subject.GetBlockedProviders().FirstOrDefault();
|
var status = Subject.GetBlockedProviders().FirstOrDefault();
|
||||||
status.Should().NotBeNull();
|
status.Should().NotBeNull();
|
||||||
status.DisabledTill.Should().HaveValue();
|
status.DisabledTill.Should().HaveValue();
|
||||||
status.DisabledTill.Value.Should().BeCloseTo(_epoch + TimeSpan.FromMinutes(5), 500);
|
status.DisabledTill.Value.Should().BeCloseTo(_epoch + TimeSpan.FromMinutes(1), 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -133,7 +133,7 @@ namespace NzbDrone.Core.Test.ThingiProviderTests
|
|||||||
var status = Subject.GetBlockedProviders().FirstOrDefault();
|
var status = Subject.GetBlockedProviders().FirstOrDefault();
|
||||||
status.Should().NotBeNull();
|
status.Should().NotBeNull();
|
||||||
status.DisabledTill.Should().HaveValue();
|
status.DisabledTill.Should().HaveValue();
|
||||||
status.DisabledTill.Value.Should().BeCloseTo(_epoch + TimeSpan.FromMinutes(15), 500);
|
status.DisabledTill.Value.Should().BeCloseTo(_epoch + TimeSpan.FromMinutes(5), 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -160,7 +160,7 @@ namespace NzbDrone.Core.Test.ThingiProviderTests
|
|||||||
status.Should().NotBeNull();
|
status.Should().NotBeNull();
|
||||||
|
|
||||||
origStatus.EscalationLevel.Should().Be(3);
|
origStatus.EscalationLevel.Should().Be(3);
|
||||||
status.DisabledTill.Should().BeCloseTo(_epoch + TimeSpan.FromMinutes(5), 500);
|
status.DisabledTill.Should().BeCloseTo(_epoch + TimeSpan.FromMinutes(1), 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ namespace NzbDrone.Core.Test.UpdateTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
[Ignore("TODO No Updates On Server")]
|
|
||||||
public void finds_update_when_version_lower()
|
public void finds_update_when_version_lower()
|
||||||
{
|
{
|
||||||
UseRealHttp();
|
UseRealHttp();
|
||||||
|
|||||||
@@ -74,6 +74,8 @@ namespace NzbDrone.Core.Applications.LazyLibrarian
|
|||||||
var remoteIndexer = _lazyLibrarianV1Proxy.AddIndexer(lazyLibrarianIndexer, Settings);
|
var remoteIndexer = _lazyLibrarianV1Proxy.AddIndexer(lazyLibrarianIndexer, Settings);
|
||||||
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerName = $"{remoteIndexer.Type},{remoteIndexer.Name}" });
|
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerName = $"{remoteIndexer.Type},{remoteIndexer.Name}" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.Trace("Skipping add for indexer {0} [{1}] due to no app Sync Categories supported by the indexer", indexer.Name, indexer.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void RemoveIndexer(int indexerId)
|
public override void RemoveIndexer(int indexerId)
|
||||||
|
|||||||
@@ -91,6 +91,8 @@ namespace NzbDrone.Core.Applications.Lidarr
|
|||||||
var remoteIndexer = _lidarrV1Proxy.AddIndexer(lidarrIndexer, Settings);
|
var remoteIndexer = _lidarrV1Proxy.AddIndexer(lidarrIndexer, Settings);
|
||||||
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = remoteIndexer.Id });
|
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = remoteIndexer.Id });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.Trace("Skipping add for indexer {0} [{1}] due to no app Sync Categories supported by the indexer", indexer.Name, indexer.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void RemoveIndexer(int indexerId)
|
public override void RemoveIndexer(int indexerId)
|
||||||
@@ -126,6 +128,12 @@ namespace NzbDrone.Core.Applications.Lidarr
|
|||||||
{
|
{
|
||||||
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
|
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
|
||||||
{
|
{
|
||||||
|
// Retain user fields not-affiliated with Prowlarr
|
||||||
|
lidarrIndexer.Fields.AddRange(remoteIndexer.Fields.Where(f => !lidarrIndexer.Fields.Any(s => s.Name == f.Name)));
|
||||||
|
|
||||||
|
// Retain user settings not-affiliated with Prowlarr
|
||||||
|
lidarrIndexer.DownloadClientId = remoteIndexer.DownloadClientId;
|
||||||
|
|
||||||
// Update the indexer if it still has categories that match
|
// Update the indexer if it still has categories that match
|
||||||
_lidarrV1Proxy.UpdateIndexer(lidarrIndexer, Settings);
|
_lidarrV1Proxy.UpdateIndexer(lidarrIndexer, Settings);
|
||||||
}
|
}
|
||||||
@@ -159,6 +167,7 @@ namespace NzbDrone.Core.Applications.Lidarr
|
|||||||
{
|
{
|
||||||
var cacheKey = $"{Settings.BaseUrl}";
|
var cacheKey = $"{Settings.BaseUrl}";
|
||||||
var schemas = _schemaCache.Get(cacheKey, () => _lidarrV1Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7));
|
var schemas = _schemaCache.Get(cacheKey, () => _lidarrV1Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7));
|
||||||
|
var syncFields = new string[] { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime", "seedCriteria.discographySeedTime" };
|
||||||
|
|
||||||
var newznab = schemas.Where(i => i.Implementation == "Newznab").First();
|
var newznab = schemas.Where(i => i.Implementation == "Newznab").First();
|
||||||
var torznab = schemas.Where(i => i.Implementation == "Torznab").First();
|
var torznab = schemas.Where(i => i.Implementation == "Torznab").First();
|
||||||
@@ -175,9 +184,11 @@ namespace NzbDrone.Core.Applications.Lidarr
|
|||||||
Priority = indexer.Priority,
|
Priority = indexer.Priority,
|
||||||
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
|
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
|
||||||
ConfigContract = schema.ConfigContract,
|
ConfigContract = schema.ConfigContract,
|
||||||
Fields = schema.Fields,
|
Fields = new List<LidarrField>()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
lidarrIndexer.Fields.AddRange(schema.Fields.Where(x => syncFields.Contains(x.Name)));
|
||||||
|
|
||||||
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/";
|
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/";
|
||||||
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/api";
|
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/api";
|
||||||
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey;
|
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey;
|
||||||
@@ -191,7 +202,7 @@ namespace NzbDrone.Core.Applications.Lidarr
|
|||||||
|
|
||||||
if (lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime") != null)
|
if (lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime") != null)
|
||||||
{
|
{
|
||||||
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
|
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.PackSeedTime ?? ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ namespace NzbDrone.Core.Applications.Lidarr
|
|||||||
public string Implementation { get; set; }
|
public string Implementation { get; set; }
|
||||||
public string ConfigContract { get; set; }
|
public string ConfigContract { get; set; }
|
||||||
public string InfoLink { get; set; }
|
public string InfoLink { get; set; }
|
||||||
|
public int? DownloadClientId { get; set; }
|
||||||
public HashSet<int> Tags { get; set; }
|
public HashSet<int> Tags { get; set; }
|
||||||
public List<LidarrField> Fields { get; set; }
|
public List<LidarrField> Fields { get; set; }
|
||||||
|
|
||||||
@@ -33,7 +34,7 @@ namespace NzbDrone.Core.Applications.Lidarr
|
|||||||
|
|
||||||
var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
|
var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
|
||||||
var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
|
var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
|
||||||
var apiPathCompare = apiPath == otherApiPath;
|
var apiPathCompare = apiPath.Equals(otherApiPath);
|
||||||
|
|
||||||
var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
|
var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
|
||||||
var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
|
var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
|
||||||
|
|||||||
@@ -75,6 +75,8 @@ namespace NzbDrone.Core.Applications.Mylar
|
|||||||
var remoteIndexer = _mylarV3Proxy.AddIndexer(mylarIndexer, Settings);
|
var remoteIndexer = _mylarV3Proxy.AddIndexer(mylarIndexer, Settings);
|
||||||
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerName = $"{remoteIndexer.Type},{remoteIndexer.Name}" });
|
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerName = $"{remoteIndexer.Type},{remoteIndexer.Name}" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.Trace("Skipping add for indexer {0} [{1}] due to no app Sync Categories supported by the indexer", indexer.Name, indexer.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void RemoveIndexer(int indexerId)
|
public override void RemoveIndexer(int indexerId)
|
||||||
|
|||||||
@@ -91,6 +91,8 @@ namespace NzbDrone.Core.Applications.Radarr
|
|||||||
var remoteIndexer = _radarrV3Proxy.AddIndexer(radarrIndexer, Settings);
|
var remoteIndexer = _radarrV3Proxy.AddIndexer(radarrIndexer, Settings);
|
||||||
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = remoteIndexer.Id });
|
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = remoteIndexer.Id });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.Trace("Skipping add for indexer {0} [{1}] due to no app Sync Categories supported by the indexer", indexer.Name, indexer.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void RemoveIndexer(int indexerId)
|
public override void RemoveIndexer(int indexerId)
|
||||||
@@ -126,6 +128,12 @@ namespace NzbDrone.Core.Applications.Radarr
|
|||||||
{
|
{
|
||||||
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
|
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
|
||||||
{
|
{
|
||||||
|
// Retain user fields not-affiliated with Prowlarr
|
||||||
|
radarrIndexer.Fields.AddRange(remoteIndexer.Fields.Where(f => !radarrIndexer.Fields.Any(s => s.Name == f.Name)));
|
||||||
|
|
||||||
|
// Retain user settings not-affiliated with Prowlarr
|
||||||
|
radarrIndexer.DownloadClientId = remoteIndexer.DownloadClientId;
|
||||||
|
|
||||||
// Update the indexer if it still has categories that match
|
// Update the indexer if it still has categories that match
|
||||||
_radarrV3Proxy.UpdateIndexer(radarrIndexer, Settings);
|
_radarrV3Proxy.UpdateIndexer(radarrIndexer, Settings);
|
||||||
}
|
}
|
||||||
@@ -159,6 +167,7 @@ namespace NzbDrone.Core.Applications.Radarr
|
|||||||
{
|
{
|
||||||
var cacheKey = $"{Settings.BaseUrl}";
|
var cacheKey = $"{Settings.BaseUrl}";
|
||||||
var schemas = _schemaCache.Get(cacheKey, () => _radarrV3Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7));
|
var schemas = _schemaCache.Get(cacheKey, () => _radarrV3Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7));
|
||||||
|
var syncFields = new string[] { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime" };
|
||||||
|
|
||||||
var newznab = schemas.Where(i => i.Implementation == "Newznab").First();
|
var newznab = schemas.Where(i => i.Implementation == "Newznab").First();
|
||||||
var torznab = schemas.Where(i => i.Implementation == "Torznab").First();
|
var torznab = schemas.Where(i => i.Implementation == "Torznab").First();
|
||||||
@@ -175,9 +184,11 @@ namespace NzbDrone.Core.Applications.Radarr
|
|||||||
Priority = indexer.Priority,
|
Priority = indexer.Priority,
|
||||||
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
|
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
|
||||||
ConfigContract = schema.ConfigContract,
|
ConfigContract = schema.ConfigContract,
|
||||||
Fields = schema.Fields,
|
Fields = new List<RadarrField>()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
radarrIndexer.Fields.AddRange(schema.Fields.Where(x => syncFields.Contains(x.Name)));
|
||||||
|
|
||||||
radarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/";
|
radarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/";
|
||||||
radarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/api";
|
radarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/api";
|
||||||
radarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey;
|
radarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey;
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ namespace NzbDrone.Core.Applications.Radarr
|
|||||||
public string Implementation { get; set; }
|
public string Implementation { get; set; }
|
||||||
public string ConfigContract { get; set; }
|
public string ConfigContract { get; set; }
|
||||||
public string InfoLink { get; set; }
|
public string InfoLink { get; set; }
|
||||||
|
public int? DownloadClientId { get; set; }
|
||||||
public HashSet<int> Tags { get; set; }
|
public HashSet<int> Tags { get; set; }
|
||||||
public List<RadarrField> Fields { get; set; }
|
public List<RadarrField> Fields { get; set; }
|
||||||
|
|
||||||
@@ -33,7 +34,7 @@ namespace NzbDrone.Core.Applications.Radarr
|
|||||||
|
|
||||||
var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
|
var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
|
||||||
var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
|
var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
|
||||||
var apiPathCompare = apiPath == otherApiPath;
|
var apiPathCompare = apiPath.Equals(otherApiPath);
|
||||||
|
|
||||||
var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
|
var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
|
||||||
var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
|
var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
|
||||||
|
|||||||
@@ -91,6 +91,8 @@ namespace NzbDrone.Core.Applications.Readarr
|
|||||||
var remoteIndexer = _readarrV1Proxy.AddIndexer(readarrIndexer, Settings);
|
var remoteIndexer = _readarrV1Proxy.AddIndexer(readarrIndexer, Settings);
|
||||||
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = remoteIndexer.Id });
|
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = remoteIndexer.Id });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.Trace("Skipping add for indexer {0} [{1}] due to no app Sync Categories supported by the indexer", indexer.Name, indexer.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void RemoveIndexer(int indexerId)
|
public override void RemoveIndexer(int indexerId)
|
||||||
@@ -126,6 +128,8 @@ namespace NzbDrone.Core.Applications.Readarr
|
|||||||
{
|
{
|
||||||
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
|
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
|
||||||
{
|
{
|
||||||
|
readarrIndexer.Fields.AddRange(remoteIndexer.Fields.Where(f => !readarrIndexer.Fields.Any(s => s.Name == f.Name)));
|
||||||
|
|
||||||
// Update the indexer if it still has categories that match
|
// Update the indexer if it still has categories that match
|
||||||
_readarrV1Proxy.UpdateIndexer(readarrIndexer, Settings);
|
_readarrV1Proxy.UpdateIndexer(readarrIndexer, Settings);
|
||||||
}
|
}
|
||||||
@@ -159,6 +163,7 @@ namespace NzbDrone.Core.Applications.Readarr
|
|||||||
{
|
{
|
||||||
var cacheKey = $"{Settings.BaseUrl}";
|
var cacheKey = $"{Settings.BaseUrl}";
|
||||||
var schemas = _schemaCache.Get(cacheKey, () => _readarrV1Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7));
|
var schemas = _schemaCache.Get(cacheKey, () => _readarrV1Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7));
|
||||||
|
var syncFields = new string[] { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime", "seedCriteria.discographySeedTime" };
|
||||||
|
|
||||||
var newznab = schemas.Where(i => i.Implementation == "Newznab").First();
|
var newznab = schemas.Where(i => i.Implementation == "Newznab").First();
|
||||||
var torznab = schemas.Where(i => i.Implementation == "Torznab").First();
|
var torznab = schemas.Where(i => i.Implementation == "Torznab").First();
|
||||||
@@ -175,9 +180,11 @@ namespace NzbDrone.Core.Applications.Readarr
|
|||||||
Priority = indexer.Priority,
|
Priority = indexer.Priority,
|
||||||
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
|
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
|
||||||
ConfigContract = schema.ConfigContract,
|
ConfigContract = schema.ConfigContract,
|
||||||
Fields = schema.Fields,
|
Fields = new List<ReadarrField>()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
readarrIndexer.Fields.AddRange(schema.Fields.Where(x => syncFields.Contains(x.Name)));
|
||||||
|
|
||||||
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/";
|
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/";
|
||||||
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/api";
|
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/api";
|
||||||
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey;
|
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey;
|
||||||
@@ -191,7 +198,7 @@ namespace NzbDrone.Core.Applications.Readarr
|
|||||||
|
|
||||||
if (readarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime") != null)
|
if (readarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime") != null)
|
||||||
{
|
{
|
||||||
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
|
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.PackSeedTime ?? ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ namespace NzbDrone.Core.Applications.Readarr
|
|||||||
|
|
||||||
var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
|
var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
|
||||||
var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
|
var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
|
||||||
var apiPathCompare = apiPath == otherApiPath;
|
var apiPathCompare = apiPath.Equals(otherApiPath);
|
||||||
|
|
||||||
var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
|
var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
|
||||||
var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
|
var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
|
||||||
|
|||||||
@@ -91,6 +91,8 @@ namespace NzbDrone.Core.Applications.Sonarr
|
|||||||
var remoteIndexer = _sonarrV3Proxy.AddIndexer(sonarrIndexer, Settings);
|
var remoteIndexer = _sonarrV3Proxy.AddIndexer(sonarrIndexer, Settings);
|
||||||
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = remoteIndexer.Id });
|
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = remoteIndexer.Id });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.Trace("Skipping add for indexer {0} [{1}] due to no app Sync Categories supported by the indexer", indexer.Name, indexer.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void RemoveIndexer(int indexerId)
|
public override void RemoveIndexer(int indexerId)
|
||||||
@@ -126,6 +128,13 @@ namespace NzbDrone.Core.Applications.Sonarr
|
|||||||
{
|
{
|
||||||
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any() || indexer.Capabilities.Categories.SupportedCategories(Settings.AnimeSyncCategories.ToArray()).Any())
|
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any() || indexer.Capabilities.Categories.SupportedCategories(Settings.AnimeSyncCategories.ToArray()).Any())
|
||||||
{
|
{
|
||||||
|
// Retain user fields not-affiliated with Prowlarr
|
||||||
|
sonarrIndexer.Fields.AddRange(remoteIndexer.Fields.Where(f => !sonarrIndexer.Fields.Any(s => s.Name == f.Name)));
|
||||||
|
|
||||||
|
// Retain user settings not-affiliated with Prowlarr
|
||||||
|
sonarrIndexer.DownloadClientId = remoteIndexer.DownloadClientId;
|
||||||
|
sonarrIndexer.SeasonSearchMaximumSingleEpisodeAge = remoteIndexer.SeasonSearchMaximumSingleEpisodeAge;
|
||||||
|
|
||||||
// Update the indexer if it still has categories that match
|
// Update the indexer if it still has categories that match
|
||||||
_sonarrV3Proxy.UpdateIndexer(sonarrIndexer, Settings);
|
_sonarrV3Proxy.UpdateIndexer(sonarrIndexer, Settings);
|
||||||
}
|
}
|
||||||
@@ -159,6 +168,7 @@ namespace NzbDrone.Core.Applications.Sonarr
|
|||||||
{
|
{
|
||||||
var cacheKey = $"{Settings.BaseUrl}";
|
var cacheKey = $"{Settings.BaseUrl}";
|
||||||
var schemas = _schemaCache.Get(cacheKey, () => _sonarrV3Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7));
|
var schemas = _schemaCache.Get(cacheKey, () => _sonarrV3Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7));
|
||||||
|
var syncFields = new string[] { "baseUrl", "apiPath", "apiKey", "categories", "animeCategories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime", "seedCriteria.seasonPackSeedTime" };
|
||||||
|
|
||||||
var newznab = schemas.Where(i => i.Implementation == "Newznab").First();
|
var newznab = schemas.Where(i => i.Implementation == "Newznab").First();
|
||||||
var torznab = schemas.Where(i => i.Implementation == "Torznab").First();
|
var torznab = schemas.Where(i => i.Implementation == "Torznab").First();
|
||||||
@@ -175,9 +185,11 @@ namespace NzbDrone.Core.Applications.Sonarr
|
|||||||
Priority = indexer.Priority,
|
Priority = indexer.Priority,
|
||||||
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
|
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
|
||||||
ConfigContract = schema.ConfigContract,
|
ConfigContract = schema.ConfigContract,
|
||||||
Fields = schema.Fields,
|
Fields = new List<SonarrField>()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
sonarrIndexer.Fields.AddRange(schema.Fields.Where(x => syncFields.Contains(x.Name)));
|
||||||
|
|
||||||
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/";
|
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/";
|
||||||
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/api";
|
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/api";
|
||||||
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey;
|
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey;
|
||||||
@@ -189,7 +201,7 @@ namespace NzbDrone.Core.Applications.Sonarr
|
|||||||
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value = indexer.AppProfile.Value.MinimumSeeders;
|
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value = indexer.AppProfile.Value.MinimumSeeders;
|
||||||
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedRatio;
|
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedRatio;
|
||||||
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
|
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
|
||||||
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seasonPackSeedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
|
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seasonPackSeedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.PackSeedTime ?? ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
return sonarrIndexer;
|
return sonarrIndexer;
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ namespace NzbDrone.Core.Applications.Sonarr
|
|||||||
public string Implementation { get; set; }
|
public string Implementation { get; set; }
|
||||||
public string ConfigContract { get; set; }
|
public string ConfigContract { get; set; }
|
||||||
public string InfoLink { get; set; }
|
public string InfoLink { get; set; }
|
||||||
|
public int? DownloadClientId { get; set; }
|
||||||
|
public int? SeasonSearchMaximumSingleEpisodeAge { get; set; }
|
||||||
public HashSet<int> Tags { get; set; }
|
public HashSet<int> Tags { get; set; }
|
||||||
public List<SonarrField> Fields { get; set; }
|
public List<SonarrField> Fields { get; set; }
|
||||||
|
|
||||||
@@ -34,7 +36,7 @@ namespace NzbDrone.Core.Applications.Sonarr
|
|||||||
|
|
||||||
var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
|
var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
|
||||||
var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
|
var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
|
||||||
var apiPathCompare = apiPath == otherApiPath;
|
var apiPathCompare = apiPath.Equals(otherApiPath);
|
||||||
|
|
||||||
var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
|
var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
|
||||||
var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
|
var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
|
||||||
|
|||||||
@@ -91,6 +91,8 @@ namespace NzbDrone.Core.Applications.Whisparr
|
|||||||
var remoteIndexer = _whisparrV3Proxy.AddIndexer(radarrIndexer, Settings);
|
var remoteIndexer = _whisparrV3Proxy.AddIndexer(radarrIndexer, Settings);
|
||||||
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = remoteIndexer.Id });
|
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = remoteIndexer.Id });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.Trace("Skipping add for indexer {0} [{1}] due to no app Sync Categories supported by the indexer", indexer.Name, indexer.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void RemoveIndexer(int indexerId)
|
public override void RemoveIndexer(int indexerId)
|
||||||
@@ -126,6 +128,8 @@ namespace NzbDrone.Core.Applications.Whisparr
|
|||||||
{
|
{
|
||||||
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
|
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
|
||||||
{
|
{
|
||||||
|
whisparrIndexer.Fields.AddRange(remoteIndexer.Fields.Where(f => !whisparrIndexer.Fields.Any(s => s.Name == f.Name)));
|
||||||
|
|
||||||
// Update the indexer if it still has categories that match
|
// Update the indexer if it still has categories that match
|
||||||
_whisparrV3Proxy.UpdateIndexer(whisparrIndexer, Settings);
|
_whisparrV3Proxy.UpdateIndexer(whisparrIndexer, Settings);
|
||||||
}
|
}
|
||||||
@@ -159,6 +163,7 @@ namespace NzbDrone.Core.Applications.Whisparr
|
|||||||
{
|
{
|
||||||
var cacheKey = $"{Settings.BaseUrl}";
|
var cacheKey = $"{Settings.BaseUrl}";
|
||||||
var schemas = _schemaCache.Get(cacheKey, () => _whisparrV3Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7));
|
var schemas = _schemaCache.Get(cacheKey, () => _whisparrV3Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7));
|
||||||
|
var syncFields = new string[] { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime" };
|
||||||
|
|
||||||
var newznab = schemas.Where(i => i.Implementation == "Newznab").First();
|
var newznab = schemas.Where(i => i.Implementation == "Newznab").First();
|
||||||
var torznab = schemas.Where(i => i.Implementation == "Torznab").First();
|
var torznab = schemas.Where(i => i.Implementation == "Torznab").First();
|
||||||
@@ -175,9 +180,11 @@ namespace NzbDrone.Core.Applications.Whisparr
|
|||||||
Priority = indexer.Priority,
|
Priority = indexer.Priority,
|
||||||
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
|
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
|
||||||
ConfigContract = schema.ConfigContract,
|
ConfigContract = schema.ConfigContract,
|
||||||
Fields = schema.Fields,
|
Fields = new List<WhisparrField>()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
whisparrIndexer.Fields.AddRange(schema.Fields.Where(x => syncFields.Contains(x.Name)));
|
||||||
|
|
||||||
whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/";
|
whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/";
|
||||||
whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/api";
|
whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/api";
|
||||||
whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey;
|
whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey;
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ namespace NzbDrone.Core.Applications.Whisparr
|
|||||||
|
|
||||||
var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
|
var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
|
||||||
var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
|
var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
|
||||||
var apiPathCompare = apiPath == otherApiPath;
|
var apiPathCompare = apiPath.Equals(otherApiPath);
|
||||||
|
|
||||||
var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
|
var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
|
||||||
var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
|
var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace NzbDrone.Core.Authentication
|
||||||
|
{
|
||||||
|
public enum AuthenticationRequiredType
|
||||||
|
{
|
||||||
|
Enabled = 0,
|
||||||
|
DisabledForLocalAddresses = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
namespace NzbDrone.Core.Authentication
|
namespace NzbDrone.Core.Authentication
|
||||||
{
|
{
|
||||||
public enum AuthenticationType
|
public enum AuthenticationType
|
||||||
{
|
{
|
||||||
None = 0,
|
None = 0,
|
||||||
Basic = 1,
|
Basic = 1,
|
||||||
Forms = 2
|
Forms = 2,
|
||||||
|
External = 3
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using NzbDrone.Core.Datastore;
|
using NzbDrone.Core.Datastore;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Authentication
|
namespace NzbDrone.Core.Authentication
|
||||||
@@ -8,5 +8,7 @@ namespace NzbDrone.Core.Authentication
|
|||||||
public Guid Identifier { get; set; }
|
public Guid Identifier { get; set; }
|
||||||
public string Username { get; set; }
|
public string Username { get; set; }
|
||||||
public string Password { get; set; }
|
public string Password { get; set; }
|
||||||
|
public string Salt { get; set; }
|
||||||
|
public int Iterations { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using Microsoft.AspNetCore.Cryptography.KeyDerivation;
|
||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
using NzbDrone.Common.EnvironmentInfo;
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
@@ -21,6 +23,10 @@ namespace NzbDrone.Core.Authentication
|
|||||||
private readonly IAppFolderInfo _appFolderInfo;
|
private readonly IAppFolderInfo _appFolderInfo;
|
||||||
private readonly IDiskProvider _diskProvider;
|
private readonly IDiskProvider _diskProvider;
|
||||||
|
|
||||||
|
private static readonly int ITERATIONS = 10000;
|
||||||
|
private static readonly int SALT_SIZE = 128 / 8;
|
||||||
|
private static readonly int NUMBER_OF_BYTES = 256 / 8;
|
||||||
|
|
||||||
public UserService(IUserRepository repo, IAppFolderInfo appFolderInfo, IDiskProvider diskProvider)
|
public UserService(IUserRepository repo, IAppFolderInfo appFolderInfo, IDiskProvider diskProvider)
|
||||||
{
|
{
|
||||||
_repo = repo;
|
_repo = repo;
|
||||||
@@ -30,12 +36,15 @@ namespace NzbDrone.Core.Authentication
|
|||||||
|
|
||||||
public User Add(string username, string password)
|
public User Add(string username, string password)
|
||||||
{
|
{
|
||||||
return _repo.Insert(new User
|
var user = new User
|
||||||
{
|
{
|
||||||
Identifier = Guid.NewGuid(),
|
Identifier = Guid.NewGuid(),
|
||||||
Username = username.ToLowerInvariant(),
|
Username = username.ToLowerInvariant()
|
||||||
Password = password.SHA256Hash()
|
};
|
||||||
});
|
|
||||||
|
SetUserHashedPassword(user, password);
|
||||||
|
|
||||||
|
return _repo.Insert(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
public User Update(User user)
|
public User Update(User user)
|
||||||
@@ -54,7 +63,7 @@ namespace NzbDrone.Core.Authentication
|
|||||||
|
|
||||||
if (user.Password != password)
|
if (user.Password != password)
|
||||||
{
|
{
|
||||||
user.Password = password.SHA256Hash();
|
SetUserHashedPassword(user, password);
|
||||||
}
|
}
|
||||||
|
|
||||||
user.Username = username.ToLowerInvariant();
|
user.Username = username.ToLowerInvariant();
|
||||||
@@ -81,7 +90,20 @@ namespace NzbDrone.Core.Authentication
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.Password == password.SHA256Hash())
|
if (user.Salt.IsNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
// If password matches stored SHA256 hash, update to salted hash and verify.
|
||||||
|
if (user.Password == password.SHA256Hash())
|
||||||
|
{
|
||||||
|
SetUserHashedPassword(user, password);
|
||||||
|
|
||||||
|
return Update(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (VerifyHashedPassword(user, password))
|
||||||
{
|
{
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
@@ -93,5 +115,42 @@ namespace NzbDrone.Core.Authentication
|
|||||||
{
|
{
|
||||||
return _repo.FindUser(identifier);
|
return _repo.FindUser(identifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private User SetUserHashedPassword(User user, string password)
|
||||||
|
{
|
||||||
|
var salt = GenerateSalt();
|
||||||
|
|
||||||
|
user.Iterations = ITERATIONS;
|
||||||
|
user.Salt = Convert.ToBase64String(salt);
|
||||||
|
user.Password = GetHashedPassword(password, salt, ITERATIONS);
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] GenerateSalt()
|
||||||
|
{
|
||||||
|
var salt = new byte[SALT_SIZE];
|
||||||
|
RandomNumberGenerator.Create().GetBytes(salt);
|
||||||
|
|
||||||
|
return salt;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetHashedPassword(string password, byte[] salt, int iterations)
|
||||||
|
{
|
||||||
|
return Convert.ToBase64String(KeyDerivation.Pbkdf2(
|
||||||
|
password: password,
|
||||||
|
salt: salt,
|
||||||
|
prf: KeyDerivationPrf.HMACSHA512,
|
||||||
|
iterationCount: iterations,
|
||||||
|
numBytesRequested: NUMBER_OF_BYTES));
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool VerifyHashedPassword(User user, string password)
|
||||||
|
{
|
||||||
|
var salt = Convert.FromBase64String(user.Salt);
|
||||||
|
var hashedPassword = GetHashedPassword(password, salt, user.Iterations);
|
||||||
|
|
||||||
|
return user.Password == hashedPassword;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ namespace NzbDrone.Core.Configuration
|
|||||||
bool EnableSsl { get; }
|
bool EnableSsl { get; }
|
||||||
bool LaunchBrowser { get; }
|
bool LaunchBrowser { get; }
|
||||||
AuthenticationType AuthenticationMethod { get; }
|
AuthenticationType AuthenticationMethod { get; }
|
||||||
|
AuthenticationRequiredType AuthenticationRequired { get; }
|
||||||
bool AnalyticsEnabled { get; }
|
bool AnalyticsEnabled { get; }
|
||||||
string LogLevel { get; }
|
string LogLevel { get; }
|
||||||
string ConsoleLogLevel { get; }
|
string ConsoleLogLevel { get; }
|
||||||
@@ -193,10 +194,12 @@ namespace NzbDrone.Core.Configuration
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AuthenticationRequiredType AuthenticationRequired => GetValueEnum("AuthenticationRequired", AuthenticationRequiredType.Enabled);
|
||||||
|
|
||||||
public bool AnalyticsEnabled => GetValueBoolean("AnalyticsEnabled", true, persist: false);
|
public bool AnalyticsEnabled => GetValueBoolean("AnalyticsEnabled", true, persist: false);
|
||||||
|
|
||||||
// TODO: Change back to "master" for the first stable release.
|
// TODO: Change back to "master" for the first stable release.
|
||||||
public string Branch => GetValue("Branch", "develop").ToLowerInvariant();
|
public string Branch => GetValue("Branch", "master").ToLowerInvariant();
|
||||||
|
|
||||||
public string LogLevel => GetValue("LogLevel", "info").ToLowerInvariant();
|
public string LogLevel => GetValue("LogLevel", "info").ToLowerInvariant();
|
||||||
public string ConsoleLogLevel => GetValue("ConsoleLogLevel", string.Empty, persist: false);
|
public string ConsoleLogLevel => GetValue("ConsoleLogLevel", string.Empty, persist: false);
|
||||||
@@ -206,7 +209,7 @@ namespace NzbDrone.Core.Configuration
|
|||||||
public string PostgresMainDb => _postgresOptions?.MainDb ?? GetValue("PostgresMainDb", "prowlarr-main", persist: false);
|
public string PostgresMainDb => _postgresOptions?.MainDb ?? GetValue("PostgresMainDb", "prowlarr-main", persist: false);
|
||||||
public string PostgresLogDb => _postgresOptions?.LogDb ?? GetValue("PostgresLogDb", "prowlarr-log", persist: false);
|
public string PostgresLogDb => _postgresOptions?.LogDb ?? GetValue("PostgresLogDb", "prowlarr-log", persist: false);
|
||||||
public int PostgresPort => (_postgresOptions?.Port ?? 0) != 0 ? _postgresOptions.Port : GetValueInt("PostgresPort", 5432, persist: false);
|
public int PostgresPort => (_postgresOptions?.Port ?? 0) != 0 ? _postgresOptions.Port : GetValueInt("PostgresPort", 5432, persist: false);
|
||||||
public string Theme => GetValue("Theme", "light", persist: false);
|
public string Theme => GetValue("Theme", "auto", persist: false);
|
||||||
public bool LogSql => GetValueBoolean("LogSql", false, persist: false);
|
public bool LogSql => GetValueBoolean("LogSql", false, persist: false);
|
||||||
public int LogRotate => GetValueInt("LogRotate", 50, persist: false);
|
public int LogRotate => GetValueInt("LogRotate", 50, persist: false);
|
||||||
public bool FilterSentryEvents => GetValueBoolean("FilterSentryEvents", true, persist: false);
|
public bool FilterSentryEvents => GetValueBoolean("FilterSentryEvents", true, persist: false);
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ namespace NzbDrone.Core.Configuration
|
|||||||
var releaseInfoPath = Path.Combine(bin, "release_info");
|
var releaseInfoPath = Path.Combine(bin, "release_info");
|
||||||
|
|
||||||
PackageUpdateMechanism = UpdateMechanism.BuiltIn;
|
PackageUpdateMechanism = UpdateMechanism.BuiltIn;
|
||||||
DefaultBranch = "develop";
|
DefaultBranch = "master";
|
||||||
|
|
||||||
if (Path.GetFileName(bin) == "bin" && diskProvider.FileExists(packageInfoPath))
|
if (Path.GetFileName(bin) == "bin" && diskProvider.FileExists(packageInfoPath))
|
||||||
{
|
{
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user