Compare commits

..

3 Commits

Author SHA1 Message Date
Robin Dadswell 0150988bcd Added Indexer Factory 2021-06-04 23:08:02 +01:00
Robin Dadswell 593231c3af Updated config definition to match Jacketts config definition 2021-06-04 23:07:40 +01:00
Robin Dadswell dbbb6bf0d1 Starts of Jackett Migrations
request extension

start migration controller/resource

removed unneccesary using - probalby more to go still

more scaffolding

added more framework; contemplating using the importlists from other arrs as a base for this

Revert "jackett config"

This reverts commit 6523eaf55450ceed84b3667421595a9d9e34dc51.

added stuff from nit's radarr pr

neated code up a little bit, more to do on this though

get config sorted, api logic also added - migration todo and indexer config to do
2021-06-03 11:07:56 +01:00
248 changed files with 1541 additions and 6021 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# These are supported funding model platforms # These are supported funding model platforms
github: Prowlarr # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username patreon: # Replace with a single Patreon username
open_collective: prowlarr open_collective: prowlarr
ko_fi: # Replace with a single Ko-fi username ko_fi: # Replace with a single Ko-fi username
+1 -3
View File
@@ -7,7 +7,6 @@ assignees: ''
--- ---
<!-- Support Requests will be closed immediately, if you are unsure go to our Reddit or Discord first. Exceptions do not mean you found a bug! --> <!-- Support Requests will be closed immediately, if you are unsure go to our Reddit or Discord first. Exceptions do not mean you found a bug! -->
<!-- Note: Text between <!- and -> will be hidden -->
**Describe the bug** **Describe the bug**
<!-- A clear and concise description of what the bug is. --> <!-- A clear and concise description of what the bug is. -->
@@ -34,5 +33,4 @@ assignees: ''
Turn on Trace logs under Settings -> General and wait for the bug to occur again. Turn on Trace logs under Settings -> General and wait for the bug to occur again.
**Upload the full log file here (or another site (e.g. pastebin) and link it). Issues will be closed, if they do not include this!** **Upload the full log file here (or another site (e.g. pastebin) and link it). Issues will be closed, if they do not include this!**
<!-- Trace logs are named Prowlarr.trace.txt or Prowlarr.trace.#.txt and will contain "trace" in them--> <!-- Trace logs are named Prowlarr.trace.txt or Prowlarr.trace.#.txt and will contain "trace" in them-->
<!-- Please see the Wiki for how to provide proper and useful trace log files https://wiki.servarr.com/prowlarr/troubleshooting#logging-and-log-files -->
-3
View File
@@ -1,8 +1,5 @@
blank_issues_enabled: false blank_issues_enabled: false
contact_links: contact_links:
- name: Indexer Requests
url: https://requests.prowlarr.com/
about: Request new indexers to be added. Vote on existing requests.
- name: Support via Discord - name: Support via Discord
url: https://prowlarr.com/discord url: https://prowlarr.com/discord
about: Chat with users and devs on support and setup related topics. about: Chat with users and devs on support and setup related topics.
-1
View File
@@ -5,7 +5,6 @@ YES | NO
A few sentences describing the overall goals of the pull request's commits. A few sentences describing the overall goals of the pull request's commits.
#### Screenshot (if UI related) #### Screenshot (if UI related)
#### Todos #### Todos
- [ ] Tests - [ ] Tests
- [ ] Translation Keys - [ ] Translation Keys
-41
View File
@@ -1,41 +0,0 @@
name: Sync issue to Azure DevOps work item
on:
issues:
types:
[opened, edited, deleted, closed, reopened, labeled, unlabeled, assigned]
concurrency: azuresync-${{ github.event.issue.number }}
jobs:
alert:
runs-on: ubuntu-latest
steps:
- uses: danhellem/github-actions-issue-to-work-item@master
if: "${{ contains(github.event.issue.labels.*.name, 'Type: Bug') == true }}"
env:
ado_token: "${{ secrets.ADO_PERSONAL_ACCESS_TOKEN }}"
github_token: "${{ github.token }}"
ado_organization: "Servarr"
ado_project: "Servarr"
ado_area_path: "Servarr\\Prowlarr"
ado_wit: "Bug"
ado_new_state: "New"
ado_active_state: "Active"
ado_close_state: "Closed"
ado_bypassrules: true
log_level: 100
- uses: danhellem/github-actions-issue-to-work-item@master
if: "${{ contains(github.event.issue.labels.*.name, 'Type: Bug') == false }}"
env:
ado_token: "${{ secrets.ADO_PERSONAL_ACCESS_TOKEN }}"
github_token: "${{ github.token }}"
ado_organization: "Servarr"
ado_project: "Servarr"
ado_area_path: "Servarr\\Prowlarr"
ado_wit: "User Story"
ado_new_state: "New"
ado_active_state: "Active"
ado_close_state: "Closed"
ado_bypassrules: true
log_level: 100
+2 -22
View File
@@ -3,27 +3,11 @@
We're always looking for people to help make Prowlarr even better, there are a number of ways to contribute. We're always looking for people to help make Prowlarr even better, there are a number of ways to contribute.
## Documentation ## ## Documentation ##
Setup guides, FAQ, the more information we have on the [wiki](https://wiki.servarr.com/prowlarr) the better. Setup guides, FAQ, the more information we have on the wiki the better.
## Development ## ## Development ##
### Tools required ### See the readme for information on setting up your development environment.
- Visual Studio 2019 or higher (https://www.visualstudio.com/vs/). The community version is free and works (https://www.visualstudio.com/downloads/).
- HTML/Javascript editor of choice (VS Code/Sublime Text/Webstorm/Atom/etc)
- [Git](https://git-scm.com/downloads)
- [NodeJS](https://nodejs.org/en/download/) (Node 12.X.X or higher)
- [Yarn](https://yarnpkg.com/)
- .NET Core 5.0.
### Getting started ###
1. Fork Prowlarr
2. Clone the repository into your development machine. [*info*](https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/cloning-a-repository-from-github)
3. Install the required Node Packages `yarn install`
4. Start gulp to monitor your dev environment for any changes that need post processing using `yarn start` command.
5. Build the project in Visual Studio, Setting startup project to `Prowlarr.Console` and framework to `net5.0`
6. Debug the project in Visual Studio
7. Open http://localhost:9696
### Contributing Code ### ### Contributing Code ###
- If you're adding a new, already requested feature, please comment on [Github Issues](https://github.com/Prowlarr/Prowlarr/issues "Github Issues") so work is not duplicated (If you want to add something not already on there, please talk to us first) - If you're adding a new, already requested feature, please comment on [Github Issues](https://github.com/Prowlarr/Prowlarr/issues "Github Issues") so work is not duplicated (If you want to add something not already on there, please talk to us first)
@@ -36,10 +20,6 @@ Setup guides, FAQ, the more information we have on the [wiki](https://wiki.serva
- One feature/bug fix per pull request to keep things clean and easy to understand - One feature/bug fix per pull request to keep things clean and easy to understand
- Use 4 spaces instead of tabs, this is the default for VS 2019 and WebStorm (to my knowledge) - Use 4 spaces instead of tabs, this is the default for VS 2019 and WebStorm (to my knowledge)
### Contributing Indexers ###
- If you're contributing an indexer please phrase your commit as something like: `New: (Indexer) {Indexer Name}`, `New: (Indexer) {Usenet|Torrent} {Indexer Name}`, `New: (Indexer) {Torznab|Newznab} {Indexer Name}`
- If you're updating an indexer please phrase your commit as something like: `Fixed: (Indexer) {Indexer Name} {changes}` e.g. `Fixed: (Indexer) Changed BHD to use API`
### Pull Requesting ### ### Pull Requesting ###
- Only make pull requests to develop, never master, if you make a PR to master we'll comment on it and close it - Only make pull requests to develop, never master, if you make a PR to master we'll comment on it and close it
- You're probably going to get some comments or questions from us, they will be to ensure consistency and maintainability - You're probably going to get some comments or questions from us, they will be to ensure consistency and maintainability
+4 -13
View File
@@ -2,7 +2,7 @@
[![Build Status](https://dev.azure.com/Prowlarr/Prowlarr/_apis/build/status/Prowlarr.Prowlarr?branchName=develop)](https://dev.azure.com/Prowlarr/Prowlarr/_build/latest?definitionId=1&branchName=develop) [![Build Status](https://dev.azure.com/Prowlarr/Prowlarr/_apis/build/status/Prowlarr.Prowlarr?branchName=develop)](https://dev.azure.com/Prowlarr/Prowlarr/_build/latest?definitionId=1&branchName=develop)
[![Translated](https://translate.servarr.com/widgets/servarr/-/prowlarr/svg-badge.svg)](https://translate.servarr.com/engage/prowlarr/?utm_source=widget) [![Translated](https://translate.servarr.com/widgets/servarr/-/prowlarr/svg-badge.svg)](https://translate.servarr.com/engage/prowlarr/?utm_source=widget)
[![Docker Pulls](https://img.shields.io/docker/pulls/hotio/prowlarr.svg)](https://wiki.servarr.com/prowlarr/installation#docker) [![Docker Pulls](https://img.shields.io/docker/pulls/hotio/prowlarr.svg)](https://wiki.servarr.com/Prowlarr_Installation#Docker)
![Github Downloads](https://img.shields.io/github/downloads/Prowlarr/Prowlarr/total.svg) ![Github Downloads](https://img.shields.io/github/downloads/Prowlarr/Prowlarr/total.svg)
[![Backers on Open Collective](https://opencollective.com/Prowlarr/backers/badge.svg)](#backers) [![Backers on Open Collective](https://opencollective.com/Prowlarr/backers/badge.svg)](#backers)
[![Sponsors on Open Collective](https://opencollective.com/Prowlarr/sponsors/badge.svg)](#sponsors) [![Sponsors on Open Collective](https://opencollective.com/Prowlarr/sponsors/badge.svg)](#sponsors)
@@ -10,14 +10,12 @@
Prowlarr is a indexer manager/proxy built on the popular arr .net/reactjs base stack to integrate with your various PVR apps. Prowlarr supports both Torrent Trackers and Usenet Indexers. It integrates seamlessly with Sonarr, Radarr, Lidarr, and Readarr offering complete management of your indexers with no per app Indexer setup required (we do it all). Prowlarr is a indexer manager/proxy built on the popular arr .net/reactjs base stack to integrate with your various PVR apps. Prowlarr supports both Torrent Trackers and Usenet Indexers. It integrates seamlessly with Sonarr, Radarr, Lidarr, and Readarr offering complete management of your indexers with no per app Indexer setup required (we do it all).
## Major Features Include: ## Major Features Include:
- Usenet support for 24 indexers natively, including Headphones VIP, and support for any Newznab compatible indexer via "Generic Newznab" - Usenet support for any Newznab compatible indexer, including Headphones VIP
- Torrent support for almost 500 trackers & more coming soon - Torrent support 400+ trackers & more coming soon
- Torrent support for any Torznab compatible tracker via "Generic Torznab"
- Indexer Sync to Sonarr/Radarr/Readarr/Lidarr, so no manual configuration of the other applications are required - Indexer Sync to Sonarr/Radarr/Readarr/Lidarr, so no manual configuration of the other applications are required
- Indexer History and Statistics - Indexer History and Statistics
- Manual Searching of Trackers & Indexers at a category level - Manual Searching of Trackers & Indexers at a category level
- Support for pushing releases directly to your download clients from Prowlarr - Support for pushing releases directly to your download clients from Prowlarr
- Indexer health and status notifications
## Support ## Support
Note: Prowlarr is currently early in life, thus bugs should be expected Note: Prowlarr is currently early in life, thus bugs should be expected
@@ -25,14 +23,7 @@ Note: Prowlarr is currently early in life, thus bugs should be expected
[![Discord](https://img.shields.io/badge/discord-chat-7289DA.svg?maxAge=60)](https://prowlarr.com/discord) [![Discord](https://img.shields.io/badge/discord-chat-7289DA.svg?maxAge=60)](https://prowlarr.com/discord)
[![Reddit](https://img.shields.io/badge/reddit-discussion-FF4500.svg?maxAge=60)](https://www.reddit.com/r/Prowlarr) [![Reddit](https://img.shields.io/badge/reddit-discussion-FF4500.svg?maxAge=60)](https://www.reddit.com/r/Prowlarr)
[![GitHub - Bugs and Feature Requests Only](https://img.shields.io/badge/github-issues-red.svg?maxAge=60)](https://github.com/Prowlarr/Prowlarr/issues) [![GitHub - Bugs and Feature Requests Only](https://img.shields.io/badge/github-issues-red.svg?maxAge=60)](https://github.com/Prowlarr/Prowlarr/issues)
[![Wiki](https://img.shields.io/badge/servarr-wiki-181717.svg?maxAge=60)](https://wiki.servarr.com/prowlarr) [![Wiki](https://img.shields.io/badge/servarr-wiki-181717.svg?maxAge=60)](https://wiki.servarr.com/Prowlarr)
## Indexers/Trackers
[Supported Indexers](https://wiki.servarr.com/en/prowlarr/supported-indexers)
[Indexer Requests](https://requests.prowlarr.com)
- Request or vote on an existing request for a new tracker/indexer
## Feature Requests ## Feature Requests
+1 -1
View File
@@ -879,7 +879,7 @@ stages:
artifactName: 'WindowsAutomationScreenshots' artifactName: 'WindowsAutomationScreenshots'
targetPath: $(Build.SourcesDirectory) targetPath: $(Build.SourcesDirectory)
- checkout: none - checkout: none
- pwsh: | - powershell: |
iex ((New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/Servarr/AzureDiscordNotify/master/DiscordNotify.ps1')) iex ((New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/Servarr/AzureDiscordNotify/master/DiscordNotify.ps1'))
env: env:
SYSTEM_ACCESSTOKEN: $(System.AccessToken) SYSTEM_ACCESSTOKEN: $(System.AccessToken)
+2 -14
View File
@@ -23,16 +23,6 @@ class BarChart extends Component {
this.myChart = new Chart(this.canvasRef.current, { this.myChart = new Chart(this.canvasRef.current, {
type: 'bar', type: 'bar',
options: { options: {
x: {
ticks: {
stepSize: this.props.stepSize
}
},
y: {
ticks: {
stepSize: this.props.stepSize
}
},
indexAxis: this.props.horizontal ? 'y' : 'x', indexAxis: this.props.horizontal ? 'y' : 'x',
maintainAspectRatio: false, maintainAspectRatio: false,
plugins: { plugins: {
@@ -74,8 +64,7 @@ BarChart.propTypes = {
horizontal: PropTypes.bool, horizontal: PropTypes.bool,
legend: PropTypes.bool, legend: PropTypes.bool,
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired,
kind: PropTypes.oneOf(kinds.all).isRequired, kind: PropTypes.oneOf(kinds.all).isRequired
stepSize: PropTypes.number
}; };
BarChart.defaultProps = { BarChart.defaultProps = {
@@ -83,8 +72,7 @@ BarChart.defaultProps = {
horizontal: false, horizontal: false,
legend: false, legend: false,
title: '', title: '',
kind: kinds.INFO, kind: kinds.INFO
stepSize: 1
}; };
export default BarChart; export default BarChart;
@@ -16,16 +16,10 @@ class StackedBarChart extends Component {
maintainAspectRatio: false, maintainAspectRatio: false,
scales: { scales: {
x: { x: {
stacked: true, stacked: true
ticks: {
stepSize: this.props.stepSize
}
}, },
y: { y: {
stacked: true, stacked: true
ticks: {
stepSize: this.props.stepSize
}
} }
}, },
plugins: { plugins: {
@@ -69,13 +63,11 @@ class StackedBarChart extends Component {
StackedBarChart.propTypes = { StackedBarChart.propTypes = {
data: PropTypes.object.isRequired, data: PropTypes.object.isRequired,
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired
stepSize: PropTypes.number
}; };
StackedBarChart.defaultProps = { StackedBarChart.defaultProps = {
title: '', title: ''
stepSize: 1
}; };
export default StackedBarChart; export default StackedBarChart;
@@ -129,7 +129,7 @@ class FileBrowserModalContent extends Component {
className={styles.mappedDrivesWarning} className={styles.mappedDrivesWarning}
kind={kinds.WARNING} kind={kinds.WARNING}
> >
<Link to="https://wiki.servarr.com/prowlarr/faq#why-cant-prowlarr-see-my-files-on-a-remote-server"> <Link to="https://wiki.servarr.com/Prowlarr_FAQ#Why_cant_Prowlarr_see_my_files_on_a_remote_server">
{translate('MappedDrivesRunningAsService')} {translate('MappedDrivesRunningAsService')}
</Link> </Link>
</Alert> </Alert>
@@ -25,7 +25,7 @@ function FormInputHelpText(props) {
isCheckInput && styles.isCheckInput isCheckInput && styles.isCheckInput
)} )}
> >
<div dangerouslySetInnerHTML={{ __html: text }} /> {text}
{ {
link ? link ?
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

+1 -1
View File
@@ -73,7 +73,7 @@ class HistoryOptions extends Component {
} }
HistoryOptions.propTypes = { HistoryOptions.propTypes = {
historyCleanupDays: PropTypes.number.isRequired, historyCleanupDays: PropTypes.bool.isRequired,
dispatchSaveGeneralSettings: PropTypes.func.isRequired dispatchSaveGeneralSettings: PropTypes.func.isRequired
}; };
@@ -20,25 +20,19 @@ import styles from './AddIndexerModalContent.css';
const columns = [ const columns = [
{ {
name: 'protocol', name: 'protocol',
label: translate('Protocol'), label: 'Protocol',
isSortable: true, isSortable: true,
isVisible: true isVisible: true
}, },
{ {
name: 'name', name: 'name',
label: translate('Name'), label: 'Name',
isSortable: true,
isVisible: true
},
{
name: 'language',
label: translate('Language'),
isSortable: true, isSortable: true,
isVisible: true isVisible: true
}, },
{ {
name: 'privacy', name: 'privacy',
label: translate('Privacy'), label: 'Privacy',
isSortable: true, isSortable: true,
isVisible: true isVisible: true
} }
+1 -7
View File
@@ -26,8 +26,7 @@ class SelectIndexerRow extends Component {
const { const {
protocol, protocol,
privacy, privacy,
name, name
language
} = this.props; } = this.props;
return ( return (
@@ -42,10 +41,6 @@ class SelectIndexerRow extends Component {
{name} {name}
</TableRowCell> </TableRowCell>
<TableRowCell>
{language}
</TableRowCell>
<TableRowCell> <TableRowCell>
{privacy} {privacy}
</TableRowCell> </TableRowCell>
@@ -58,7 +53,6 @@ SelectIndexerRow.propTypes = {
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
protocol: PropTypes.string.isRequired, protocol: PropTypes.string.isRequired,
privacy: PropTypes.string.isRequired, privacy: PropTypes.string.isRequired,
language: PropTypes.string.isRequired,
implementation: PropTypes.string.isRequired, implementation: PropTypes.string.isRequired,
onIndexerSelect: PropTypes.func.isRequired onIndexerSelect: PropTypes.func.isRequired
}; };
@@ -13,7 +13,6 @@ 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 { inputTypes, kinds } from 'Helpers/Props';
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import styles from './EditIndexerModalContent.css'; import styles from './EditIndexerModalContent.css';
@@ -32,7 +31,6 @@ function EditIndexerModalContent(props) {
onSavePress, onSavePress,
onTestPress, onTestPress,
onDeleteIndexerPress, onDeleteIndexerPress,
onAdvancedSettingsPress,
...otherProps ...otherProps
} = props; } = props;
@@ -101,7 +99,7 @@ function EditIndexerModalContent(props) {
<FormInputGroup <FormInputGroup
type={inputTypes.CHECK} type={inputTypes.CHECK}
name="redirect" name="redirect"
helpText={translate('RedirectHelpText')} helpText={'Redirect incoming download requests for indexer instead of Proxying using Prowlarr'}
isDisabled={!supportsRedirect.value} isDisabled={!supportsRedirect.value}
{...redirect} {...redirect}
onChange={onInputChange} onChange={onInputChange}
@@ -115,7 +113,6 @@ function EditIndexerModalContent(props) {
type={inputTypes.APP_PROFILE_SELECT} type={inputTypes.APP_PROFILE_SELECT}
name="appProfileId" name="appProfileId"
{...appProfileId} {...appProfileId}
helpText={translate('AppProfileSelectHelpText')}
onChange={onInputChange} onChange={onInputChange}
/> />
</FormGroup> </FormGroup>
@@ -167,12 +164,6 @@ function EditIndexerModalContent(props) {
</Button> </Button>
} }
<AdvancedSettingsButton
advancedSettings={advancedSettings}
onAdvancedSettingsPress={onAdvancedSettingsPress}
showLabel={false}
/>
<SpinnerErrorButton <SpinnerErrorButton
isSpinning={isTesting} isSpinning={isTesting}
error={saveError} error={saveError}
@@ -212,7 +203,6 @@ EditIndexerModalContent.propTypes = {
onModalClose: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired, onSavePress: PropTypes.func.isRequired,
onTestPress: PropTypes.func.isRequired, onTestPress: PropTypes.func.isRequired,
onAdvancedSettingsPress: PropTypes.func.isRequired,
onDeleteIndexerPress: PropTypes.func onDeleteIndexerPress: PropTypes.func
}; };
@@ -3,7 +3,6 @@ import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { saveIndexer, setIndexerFieldValue, setIndexerValue, testIndexer } from 'Store/Actions/indexerActions'; import { saveIndexer, setIndexerFieldValue, setIndexerValue, testIndexer } from 'Store/Actions/indexerActions';
import { toggleAdvancedSettings } from 'Store/Actions/settingsActions';
import createIndexerSchemaSelector from 'Store/Selectors/createIndexerSchemaSelector'; import createIndexerSchemaSelector from 'Store/Selectors/createIndexerSchemaSelector';
import EditIndexerModalContent from './EditIndexerModalContent'; import EditIndexerModalContent from './EditIndexerModalContent';
@@ -24,8 +23,7 @@ const mapDispatchToProps = {
setIndexerValue, setIndexerValue,
setIndexerFieldValue, setIndexerFieldValue,
saveIndexer, saveIndexer,
testIndexer, testIndexer
toggleAdvancedSettings
}; };
class EditIndexerModalContentConnector extends Component { class EditIndexerModalContentConnector extends Component {
@@ -58,11 +56,6 @@ class EditIndexerModalContentConnector extends Component {
this.props.testIndexer({ id: this.props.id }); this.props.testIndexer({ id: this.props.id });
} }
onAdvancedSettingsPress = () => {
console.log('settings');
this.props.toggleAdvancedSettings();
}
// //
// Render // Render
@@ -72,7 +65,6 @@ class EditIndexerModalContentConnector extends Component {
{...this.props} {...this.props}
onSavePress={this.onSavePress} onSavePress={this.onSavePress}
onTestPress={this.onTestPress} onTestPress={this.onTestPress}
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
onInputChange={this.onInputChange} onInputChange={this.onInputChange}
onFieldChange={this.onFieldChange} onFieldChange={this.onFieldChange}
/> />
@@ -88,7 +80,6 @@ EditIndexerModalContentConnector.propTypes = {
item: PropTypes.object.isRequired, item: PropTypes.object.isRequired,
setIndexerValue: PropTypes.func.isRequired, setIndexerValue: PropTypes.func.isRequired,
setIndexerFieldValue: PropTypes.func.isRequired, setIndexerFieldValue: PropTypes.func.isRequired,
toggleAdvancedSettings: PropTypes.func.isRequired,
saveIndexer: PropTypes.func.isRequired, saveIndexer: PropTypes.func.isRequired,
testIndexer: PropTypes.func.isRequired, testIndexer: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
@@ -50,7 +50,7 @@ class TagsModalContent extends Component {
render() { render() {
const { const {
indexerTags, movieTags,
tagList, tagList,
onModalClose onModalClose
} = this.props; } = this.props;
@@ -108,7 +108,7 @@ class TagsModalContent extends Component {
<div className={styles.result}> <div className={styles.result}>
{ {
indexerTags.map((t) => { movieTags.map((t) => {
const tag = _.find(tagList, { id: t }); const tag = _.find(tagList, { id: t });
if (!tag) { if (!tag) {
@@ -140,7 +140,7 @@ class TagsModalContent extends Component {
return null; return null;
} }
if (indexerTags.indexOf(t) > -1) { if (movieTags.indexOf(t) > -1) {
return null; return null;
} }
@@ -179,7 +179,7 @@ class TagsModalContent extends Component {
} }
TagsModalContent.propTypes = { TagsModalContent.propTypes = {
indexerTags: PropTypes.arrayOf(PropTypes.number).isRequired, movieTags: PropTypes.arrayOf(PropTypes.number).isRequired,
tagList: PropTypes.arrayOf(PropTypes.object).isRequired, tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
onModalClose: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired,
onApplyTagsPress: PropTypes.func.isRequired onApplyTagsPress: PropTypes.func.isRequired
@@ -10,15 +10,15 @@ function createMapStateToProps() {
(state, { indexerIds }) => indexerIds, (state, { indexerIds }) => indexerIds,
createAllIndexersSelector(), createAllIndexersSelector(),
createTagsSelector(), createTagsSelector(),
(indexerIds, allIndexers, tagList) => { (indexerIds, allMovies, tagList) => {
const indexers = _.intersectionWith(allIndexers, indexerIds, (s, id) => { const movies = _.intersectionWith(allMovies, indexerIds, (s, id) => {
return s.id === id; return s.id === id;
}); });
const indexerTags = _.uniq(_.concat(..._.map(indexers, 'tags'))); const movieTags = _.uniq(_.concat(..._.map(movies, 'tags')));
return { return {
indexerTags, movieTags,
tagList tagList
}; };
} }
@@ -4,7 +4,7 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import withScrollPosition from 'Components/withScrollPosition'; import withScrollPosition from 'Components/withScrollPosition';
import { testAllIndexers } from 'Store/Actions/indexerActions'; import { testAllIndexers } from 'Store/Actions/indexerActions';
import { saveIndexerEditor, setMovieFilter, setMovieSort, setMovieTableOption } from 'Store/Actions/indexerIndexActions'; import { saveMovieEditor, setMovieFilter, setMovieSort, setMovieTableOption } from 'Store/Actions/indexerIndexActions';
import scrollPositions from 'Store/scrollPositions'; import scrollPositions from 'Store/scrollPositions';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createIndexerClientSideCollectionItemsSelector from 'Store/Selectors/createIndexerClientSideCollectionItemsSelector'; import createIndexerClientSideCollectionItemsSelector from 'Store/Selectors/createIndexerClientSideCollectionItemsSelector';
@@ -40,8 +40,8 @@ function createMapDispatchToProps(dispatch, props) {
dispatch(setMovieFilter({ selectedFilterKey })); dispatch(setMovieFilter({ selectedFilterKey }));
}, },
dispatchSaveIndexerEditor(payload) { dispatchSaveMovieEditor(payload) {
dispatch(saveIndexerEditor(payload)); dispatch(saveMovieEditor(payload));
}, },
onTestAllPress() { onTestAllPress() {
@@ -56,7 +56,7 @@ class IndexerIndexConnector extends Component {
// Listeners // Listeners
onSaveSelected = (payload) => { onSaveSelected = (payload) => {
this.props.dispatchSaveIndexerEditor(payload); this.props.dispatchSaveMovieEditor(payload);
} }
onScroll = ({ scrollTop }) => { onScroll = ({ scrollTop }) => {
@@ -79,7 +79,7 @@ class IndexerIndexConnector extends Component {
IndexerIndexConnector.propTypes = { IndexerIndexConnector.propTypes = {
isSmallScreen: PropTypes.bool.isRequired, isSmallScreen: PropTypes.bool.isRequired,
dispatchSaveIndexerEditor: PropTypes.func.isRequired, dispatchSaveMovieEditor: PropTypes.func.isRequired,
items: PropTypes.arrayOf(PropTypes.object) items: PropTypes.arrayOf(PropTypes.object)
}; };
@@ -71,7 +71,7 @@ class IndexerIndexRow extends Component {
const { const {
id, id,
name, name,
indexerUrls, baseUrl,
enable, enable,
redirect, redirect,
tags, tags,
@@ -248,7 +248,7 @@ class IndexerIndexRow extends Component {
className={styles.externalLink} className={styles.externalLink}
name={icons.EXTERNAL_LINK} name={icons.EXTERNAL_LINK}
title={'Website'} title={'Website'}
to={indexerUrls[0].replace('api.', '')} to={baseUrl}
/> />
<IconButton <IconButton
@@ -289,7 +289,7 @@ class IndexerIndexRow extends Component {
IndexerIndexRow.propTypes = { IndexerIndexRow.propTypes = {
id: PropTypes.number.isRequired, id: PropTypes.number.isRequired,
indexerUrls: PropTypes.arrayOf(PropTypes.string).isRequired, baseUrl: PropTypes.string.isRequired,
protocol: PropTypes.string.isRequired, protocol: PropTypes.string.isRequired,
privacy: PropTypes.string.isRequired, privacy: PropTypes.string.isRequired,
priority: PropTypes.number.isRequired, priority: PropTypes.number.isRequired,
-4
View File
@@ -19,10 +19,6 @@ function getAverageResponseTimeData(indexerStats) {
}; };
}); });
data.sort((a, b) => {
return b.value - a.value;
});
return data; return data;
} }
+3 -8
View File
@@ -4,7 +4,6 @@ import React, { Component } from 'react';
import IndexersSelectInputConnector from 'Components/Form/IndexersSelectInputConnector'; import IndexersSelectInputConnector from 'Components/Form/IndexersSelectInputConnector';
import NewznabCategorySelectInputConnector from 'Components/Form/NewznabCategorySelectInputConnector'; import NewznabCategorySelectInputConnector from 'Components/Form/NewznabCategorySelectInputConnector';
import TextInput from 'Components/Form/TextInput'; import TextInput from 'Components/Form/TextInput';
import keyboardShortcuts from 'Components/keyboardShortcuts';
import SpinnerButton from 'Components/Link/SpinnerButton'; import SpinnerButton from 'Components/Link/SpinnerButton';
import PageContentFooter from 'Components/Page/PageContentFooter'; import PageContentFooter from 'Components/Page/PageContentFooter';
import SearchFooterLabel from './SearchFooterLabel'; import SearchFooterLabel from './SearchFooterLabel';
@@ -39,11 +38,9 @@ class SearchFooter extends Component {
searchQuery searchQuery
} = this.state; } = this.state;
if (searchQuery !== '' || searchCategories.length > 0 || searchIndexerIds.length > 0) { if (searchQuery !== '' || searchCategories !== [] || searchIndexerIds !== []) {
this.onSearchPress(); this.onSearchPress();
} }
this.props.bindShortcut('enter', this.onSearchPress, { isGlobal: true });
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
@@ -117,7 +114,6 @@ class SearchFooter extends Component {
<TextInput <TextInput
name='searchQuery' name='searchQuery'
autoFocus={true}
value={searchQuery} value={searchQuery}
isDisabled={isFetching} isDisabled={isFetching}
onChange={onInputChange} onChange={onInputChange}
@@ -185,8 +181,7 @@ SearchFooter.propTypes = {
onSearchPress: PropTypes.func.isRequired, onSearchPress: PropTypes.func.isRequired,
hasIndexers: PropTypes.bool.isRequired, hasIndexers: PropTypes.bool.isRequired,
onInputChange: PropTypes.func.isRequired, onInputChange: PropTypes.func.isRequired,
searchError: PropTypes.object, searchError: PropTypes.object
bindShortcut: PropTypes.func.isRequired
}; };
export default keyboardShortcuts(SearchFooter); export default SearchFooter;
@@ -1,22 +1,10 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Label from 'Components/Label'; import Label from 'Components/Label';
import { kinds, tooltipPositions } from 'Helpers/Props';
import Tooltip from '../../Components/Tooltip/Tooltip';
function CategoryLabel({ categories }) { function CategoryLabel({ categories }) {
const sortedCategories = categories.filter((cat) => cat.name !== undefined).sort((c) => c.id); const sortedCategories = categories.filter((cat) => cat.name !== undefined).sort((c) => c.id);
if (categories?.length === 0) {
return (
<Tooltip
anchor={<Label kind={kinds.DANGER}>Unknown</Label>}
tooltip="Please report this issue to the GitHub as this shouldn't be happening"
position={tooltipPositions.LEFT}
/>
);
}
return ( return (
<span> <span>
{ {
@@ -32,10 +20,6 @@ function CategoryLabel({ categories }) {
); );
} }
CategoryLabel.defaultProps = {
categories: []
};
CategoryLabel.propTypes = { CategoryLabel.propTypes = {
categories: PropTypes.arrayOf(PropTypes.object).isRequired categories: PropTypes.arrayOf(PropTypes.object).isRequired
}; };
@@ -10,8 +10,7 @@ import styles from './AdvancedSettingsButton.css';
function AdvancedSettingsButton(props) { function AdvancedSettingsButton(props) {
const { const {
advancedSettings, advancedSettings,
onAdvancedSettingsPress, onAdvancedSettingsPress
showLabel
} = props; } = props;
return ( return (
@@ -44,27 +43,18 @@ function AdvancedSettingsButton(props) {
/> />
</span> </span>
{ <div className={styles.labelContainer}>
showLabel && <div className={styles.label}>
<div className={styles.labelContainer}> {advancedSettings ? translate('HideAdvanced') : translate('ShowAdvanced')}
<div className={styles.label}> </div>
{advancedSettings ? translate('HideAdvanced') : translate('ShowAdvanced')} </div>
</div>
</div>
}
</Link> </Link>
); );
} }
AdvancedSettingsButton.propTypes = { AdvancedSettingsButton.propTypes = {
advancedSettings: PropTypes.bool.isRequired, advancedSettings: PropTypes.bool.isRequired,
onAdvancedSettingsPress: PropTypes.func.isRequired, onAdvancedSettingsPress: PropTypes.func.isRequired
showLabel: PropTypes.bool.isRequired
};
AdvancedSettingsButton.defaultProps = {
showLabel: true
}; };
export default AdvancedSettingsButton; export default AdvancedSettingsButton;
@@ -18,9 +18,9 @@ import translate from 'Utilities/String/translate';
import styles from './EditApplicationModalContent.css'; import styles from './EditApplicationModalContent.css';
const syncLevelOptions = [ const syncLevelOptions = [
{ key: 'disabled', value: translate('Disabled') }, { key: 'disabled', value: 'Disabled' },
{ key: 'addOnly', value: translate('AddRemoveOnly') }, { key: 'addOnly', value: 'Add and Remove Only' },
{ key: 'fullSync', value: translate('FullSync') } { key: 'fullSync', value: 'Full Sync' }
]; ];
function EditApplicationModalContent(props) { function EditApplicationModalContent(props) {
@@ -53,7 +53,7 @@ function EditApplicationModalContent(props) {
return ( return (
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
<ModalHeader> <ModalHeader>
{`${id ? translate('Edit') : translate('Add')} ${translate('Application')}`} {`${id ? 'Edit' : 'Add'} Application`}
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody>
@@ -94,13 +94,13 @@ function EditApplicationModalContent(props) {
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
<FormLabel>{translate('SyncLevel')}</FormLabel> <FormLabel>{'Sync Level'}</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.SELECT} type={inputTypes.SELECT}
values={syncLevelOptions} values={syncLevelOptions}
name="syncLevel" name="syncLevel"
helpText={`${translate('SyncLevelAddRemove')}<br>${translate('SyncLevelFull')}`} helpText={'Sync Level'}
{...syncLevel} {...syncLevel}
onChange={onInputChange} onChange={onInputChange}
/> />
@@ -53,9 +53,6 @@ class AddDownloadClientModalContent extends Component {
<div> <div>
<Alert kind={kinds.INFO}> <Alert kind={kinds.INFO}>
<div>
{translate('AddDownloadClientToProwlarr')}
</div>
<div> <div>
{translate('ProwlarrSupportsAnyDownloadClient')} {translate('ProwlarrSupportsAnyDownloadClient')}
</div> </div>
@@ -55,7 +55,7 @@ function UpdateSettings(props) {
type={inputTypes.TEXT} type={inputTypes.TEXT}
name="branch" name="branch"
helpText={usingExternalUpdateMechanism ? translate('BranchUpdateMechanism') : translate('BranchUpdate')} helpText={usingExternalUpdateMechanism ? translate('BranchUpdateMechanism') : translate('BranchUpdate')}
helpLink="https://wiki.servarr.com/prowlarr/settings#updates" helpLink="https://wiki.servarr.com/Prowlarr_Settings#Updates"
{...branch} {...branch}
onChange={onInputChange} onChange={onInputChange}
readOnly={usingExternalUpdateMechanism} readOnly={usingExternalUpdateMechanism}
@@ -92,7 +92,7 @@ function UpdateSettings(props) {
name="updateMechanism" name="updateMechanism"
values={updateOptions} values={updateOptions}
helpText={translate('UpdateMechanismHelpText')} helpText={translate('UpdateMechanismHelpText')}
helpLink="https://wiki.servarr.com/prowlarr/settings#updates" helpLink="https://wiki.servarr.com/Prowlarr_Settings#Updates"
onChange={onInputChange} onChange={onInputChange}
{...updateMechanism} {...updateMechanism}
/> />
@@ -25,8 +25,8 @@ function NotificationEventItems(props) {
<FormLabel>{translate('NotificationTriggers')}</FormLabel> <FormLabel>{translate('NotificationTriggers')}</FormLabel>
<div> <div>
<FormInputHelpText <FormInputHelpText
text={translate('NotificationTriggersHelpText')} text={translate('NotifcationTriggersHelpText')}
link="https://wiki.servarr.com/prowlarr/settings#connections" link="https://wiki.servarr.com/Prowlarr_Settings#Connections"
/> />
<div className={styles.events}> <div className={styles.events}>
<div> <div>
@@ -176,7 +176,7 @@ export const SET_MOVIE_SORT = 'indexerIndex/setMovieSort';
export const SET_MOVIE_FILTER = 'indexerIndex/setMovieFilter'; export const SET_MOVIE_FILTER = 'indexerIndex/setMovieFilter';
export const SET_MOVIE_VIEW = 'indexerIndex/setMovieView'; export const SET_MOVIE_VIEW = 'indexerIndex/setMovieView';
export const SET_MOVIE_TABLE_OPTION = 'indexerIndex/setMovieTableOption'; export const SET_MOVIE_TABLE_OPTION = 'indexerIndex/setMovieTableOption';
export const SAVE_INDEXER_EDITOR = 'indexerIndex/saveIndexerEditor'; export const SAVE_MOVIE_EDITOR = 'indexerIndex/saveMovieEditor';
export const BULK_DELETE_INDEXERS = 'indexerIndex/bulkDeleteIndexers'; export const BULK_DELETE_INDEXERS = 'indexerIndex/bulkDeleteIndexers';
// //
@@ -186,14 +186,14 @@ export const setMovieSort = createAction(SET_MOVIE_SORT);
export const setMovieFilter = createAction(SET_MOVIE_FILTER); export const setMovieFilter = createAction(SET_MOVIE_FILTER);
export const setMovieView = createAction(SET_MOVIE_VIEW); export const setMovieView = createAction(SET_MOVIE_VIEW);
export const setMovieTableOption = createAction(SET_MOVIE_TABLE_OPTION); export const setMovieTableOption = createAction(SET_MOVIE_TABLE_OPTION);
export const saveIndexerEditor = createThunk(SAVE_INDEXER_EDITOR); export const saveMovieEditor = createThunk(SAVE_MOVIE_EDITOR);
export const bulkDeleteIndexers = createThunk(BULK_DELETE_INDEXERS); export const bulkDeleteIndexers = createThunk(BULK_DELETE_INDEXERS);
// //
// Action Handlers // Action Handlers
export const actionHandlers = handleThunks({ export const actionHandlers = handleThunks({
[SAVE_INDEXER_EDITOR]: function(getState, payload, dispatch) { [SAVE_MOVIE_EDITOR]: function(getState, payload, dispatch) {
dispatch(set({ dispatch(set({
section, section,
isSaving: true isSaving: true
+1 -2
View File
@@ -202,8 +202,7 @@ export const defaultState = {
export const persistState = [ export const persistState = [
'releases.customFilters', 'releases.customFilters',
'releases.selectedFilterKey', 'releases.selectedFilterKey'
'releases.columns'
]; ];
// //
@@ -13,7 +13,7 @@ function createHealthCheckSelector() {
source: 'UI', source: 'UI',
type: 'warning', type: 'warning',
message: translate('CouldNotConnectSignalR'), message: translate('CouldNotConnectSignalR'),
wikiUrl: 'https://wiki.servarr.com/prowlarr/system#could-not-connect-to-signalr' wikiUrl: 'https://wiki.servarr.com/Prowlarr_System#Could_not_connect_to_signalR'
}); });
} }
@@ -1,64 +0,0 @@
import React, { Component } from 'react';
import FieldSet from 'Components/FieldSet';
import Link from 'Components/Link/Link';
import translate from 'Utilities/String/translate';
import styles from '../styles.css';
class Donations extends Component {
//
// Render
render() {
return (
<FieldSet legend={translate('Donations')}>
<div className={styles.logoContainer} title="Radarr">
<Link to="https://opencollective.com/radarr">
<img
className={styles.logo}
src={`${window.Prowlarr.urlBase}/Content/Images/Icons/logo-radarr.png`}
/>
</Link>
</div>
<div className={styles.logoContainer} title="Lidarr">
<Link to="https://opencollective.com/lidarr">
<img
className={styles.logo}
src={`${window.Prowlarr.urlBase}/Content/Images/Icons/logo-lidarr.png`}
/>
</Link>
</div>
<div className={styles.logoContainer} title="Readarr">
<Link to="https://opencollective.com/readarr">
<img
className={styles.logo}
src={`${window.Prowlarr.urlBase}/Content/Images/Icons/logo-readarr.png`}
/>
</Link>
</div>
<div className={styles.logoContainer} title="Prowlarr">
<Link to="https://opencollective.com/prowlarr">
<img
className={styles.logo}
src={`${window.Prowlarr.urlBase}/Content/Images/Icons/logo-prowlarr.png`}
/>
</Link>
</div>
<div className={styles.logoContainer} title="Sonarr">
<Link to="https://opencollective.com/sonarr">
<img
className={styles.logo}
src={`${window.Prowlarr.urlBase}/Content/Images/Icons/logo-sonarr.png`}
/>
</Link>
</div>
</FieldSet>
);
}
}
Donations.propTypes = {
};
export default Donations;
+14 -14
View File
@@ -15,32 +15,32 @@ class MoreInfo extends Component {
return ( return (
<FieldSet legend={translate('MoreInfo')}> <FieldSet legend={translate('MoreInfo')}>
<DescriptionList> <DescriptionList>
<DescriptionListItemTitle>{translate('HomePage')}</DescriptionListItemTitle> <DescriptionListItemTitle>Home page</DescriptionListItemTitle>
<DescriptionListItemDescription> <DescriptionListItemDescription>
<Link to="https://prowlarr.com/">prowlarr.com</Link> <Link to="https://prowlarr.com/">prowlarr.com</Link>
</DescriptionListItemDescription> </DescriptionListItemDescription>
<DescriptionListItemTitle>{translate('Wiki')}</DescriptionListItemTitle> <DescriptionListItemTitle>Discord</DescriptionListItemTitle>
<DescriptionListItemDescription>
<Link to="https://wiki.servarr.com/prowlarr">wiki.servarr.com/prowlarr</Link>
</DescriptionListItemDescription>
<DescriptionListItemTitle>{translate('Reddit')}</DescriptionListItemTitle>
<DescriptionListItemDescription>
<Link to="https://reddit.com/r/prowlarr">r/prowlarr</Link>
</DescriptionListItemDescription>
<DescriptionListItemTitle>{translate('Discord')}</DescriptionListItemTitle>
<DescriptionListItemDescription> <DescriptionListItemDescription>
<Link to="https://prowlarr.com/discord">prowlarr.com/discord</Link> <Link to="https://prowlarr.com/discord">prowlarr.com/discord</Link>
</DescriptionListItemDescription> </DescriptionListItemDescription>
<DescriptionListItemTitle>{translate('Source')}</DescriptionListItemTitle> <DescriptionListItemTitle>Wiki</DescriptionListItemTitle>
<DescriptionListItemDescription>
<Link to="https://wiki.servarr.com/Prowlarr">wiki.servarr.com/Prowlarr</Link>
</DescriptionListItemDescription>
<DescriptionListItemTitle>Donations</DescriptionListItemTitle>
<DescriptionListItemDescription>
<Link to="https://opencollective.com/prowlarr">opencollective.com/prowlarr</Link>
</DescriptionListItemDescription>
<DescriptionListItemTitle>Source</DescriptionListItemTitle>
<DescriptionListItemDescription> <DescriptionListItemDescription>
<Link to="https://github.com/Prowlarr/Prowlarr/">github.com/Prowlarr/Prowlarr</Link> <Link to="https://github.com/Prowlarr/Prowlarr/">github.com/Prowlarr/Prowlarr</Link>
</DescriptionListItemDescription> </DescriptionListItemDescription>
<DescriptionListItemTitle>{translate('FeatureRequests')}</DescriptionListItemTitle> <DescriptionListItemTitle>Feature Requests</DescriptionListItemTitle>
<DescriptionListItemDescription> <DescriptionListItemDescription>
<Link to="https://github.com/Prowlarr/Prowlarr/issues">github.com/Prowlarr/Prowlarr/issues</Link> <Link to="https://github.com/Prowlarr/Prowlarr/issues">github.com/Prowlarr/Prowlarr/issues</Link>
</DescriptionListItemDescription> </DescriptionListItemDescription>
-2
View File
@@ -3,7 +3,6 @@ import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody'; import PageContentBody from 'Components/Page/PageContentBody';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import AboutConnector from './About/AboutConnector'; import AboutConnector from './About/AboutConnector';
import Donations from './Donations/Donations';
import HealthConnector from './Health/HealthConnector'; import HealthConnector from './Health/HealthConnector';
import MoreInfo from './MoreInfo/MoreInfo'; import MoreInfo from './MoreInfo/MoreInfo';
@@ -19,7 +18,6 @@ class Status extends Component {
<HealthConnector /> <HealthConnector />
<AboutConnector /> <AboutConnector />
<MoreInfo /> <MoreInfo />
<Donations />
</PageContentBody> </PageContentBody>
</PageContent> </PageContent>
); );
-17
View File
@@ -1,17 +0,0 @@
.logo {
margin: auto;
padding: 9px;
}
.logoContainer {
display: inline-block;
margin: 0.5em;
width: 50px;
height: 50px;
outline: none;
border: solid 1px #e6e6e6;
border-radius: 0.5em;
background: #f8f8ff;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
cursor: pointer;
}
+1 -1
View File
@@ -24,7 +24,7 @@ class UpdateChanges extends Component {
<ul> <ul>
{ {
changes.map((change, index) => { changes.map((change, index) => {
const checkChange = change.replace(/#\d{3,5}\b/g, (match, contents) => { const checkChange = change.replace(/#\d{4,5}\b/g, (match, contents) => {
return `[${match}](https://github.com/Prowlarr/Prowlarr/issues/${match.substring(1)})`; return `[${match}](https://github.com/Prowlarr/Prowlarr/issues/${match.substring(1)})`;
}); });
+1 -1
View File
@@ -252,7 +252,7 @@
</span> </span>
<a <a
href="https://wiki.servarr.com/prowlarr/faq#help-i-have-locked-myself-out" href="https://wiki.servarr.com/Prowlarr_FAQ#Help_I_have_locked_my_self_out_of_Prowlarr_and_do_not_know_the_password"
class="forgot-password" class="forgot-password"
>Forgot your password?</a >Forgot your password?</a
> >
@@ -17,11 +17,6 @@ namespace NzbDrone.Common.Test.InstrumentationTests
[TestCase(@"https://baconbits.org/feeds.php?feed=torrents_tv&user=12345&auth=2b51db35e1910123321025a12b9933d2&passkey=mySecret&authkey=2b51db35e1910123321025a12b9933d2")] [TestCase(@"https://baconbits.org/feeds.php?feed=torrents_tv&user=12345&auth=2b51db35e1910123321025a12b9933d2&passkey=mySecret&authkey=2b51db35e1910123321025a12b9933d2")]
[TestCase(@"http://127.0.0.1:9117/dl/indexername?jackett_apikey=flwjiefewklfjacketmySecretsdfldskjfsdlk&path=we0re9f0sdfbase64sfdkfjsdlfjk&file=The+Torrent+File+Name.torrent")] [TestCase(@"http://127.0.0.1:9117/dl/indexername?jackett_apikey=flwjiefewklfjacketmySecretsdfldskjfsdlk&path=we0re9f0sdfbase64sfdkfjsdlfjk&file=The+Torrent+File+Name.torrent")]
[TestCase(@"http://nzb.su/getnzb/2b51db35e1912ffc138825a12b9933d2.nzb&i=37292&r=2b51db35e1910123321025a12b9933d2")] [TestCase(@"http://nzb.su/getnzb/2b51db35e1912ffc138825a12b9933d2.nzb&i=37292&r=2b51db35e1910123321025a12b9933d2")]
[TestCase(@"https://horrorcharnel.org/takeloginhorror.php: username=mySecret&password=mySecret&use_sslvalue==&perm_ssl=1&submitme=X&use_ssl=1&returnto=%2F&captchaSelection=1230456")]
[TestCase(@"https://torrentdb.net/login: _token=2b51db35e1912ffc138825a12b9933d2&username=mySecret&password=mySecret&remember=on")]
[TestCase(@" var authkey = ""2b51db35e1910123321025a12b9933d2"";")]
[TestCase(@"https://hd-space.org/index.php?page=login: uid=mySecret&pwd=mySecret")]
[TestCase(@"https://beyond-hd.me/api/torrents/2b51db35e1912ffc138825a12b9933d2")]
// NzbGet // NzbGet
[TestCase(@"{ ""Name"" : ""ControlUsername"", ""Value"" : ""mySecret"" }, { ""Name"" : ""ControlPassword"", ""Value"" : ""mySecret"" }, ")] [TestCase(@"{ ""Name"" : ""ControlUsername"", ""Value"" : ""mySecret"" }, { ""Name"" : ""ControlPassword"", ""Value"" : ""mySecret"" }, ")]
@@ -93,14 +88,5 @@ namespace NzbDrone.Common.Test.InstrumentationTests
cleansedMessage.Should().Be(message); cleansedMessage.Should().Be(message);
} }
[TestCase(@"&useToken=2b51db35e1910123321025a12b9933d2")]
[TestCase(@"&useToken=2b51db35e1910123321025a12b9933d2")]
public void should_not_clean_usetoken(string message)
{
var cleansedMessage = CleanseLogMessage.Cleanse(message);
cleansedMessage.Should().Be(message);
}
} }
} }
-7
View File
@@ -84,13 +84,6 @@ namespace NzbDrone.Common.Http
throw new WebException($"Too many automatic redirections were attempted for {autoRedirectChain.Join(" -> ")}", WebExceptionStatus.ProtocolError); throw new WebException($"Too many automatic redirections were attempted for {autoRedirectChain.Join(" -> ")}", WebExceptionStatus.ProtocolError);
} }
// 302 or 303 should default to GET on redirect even if POST on original
if (response.StatusCode == HttpStatusCode.Redirect || response.StatusCode == HttpStatusCode.RedirectMethod)
{
request.Method = HttpMethod.GET;
request.ContentData = null;
}
response = await ExecuteRequestAsync(request, cookieContainer); response = await ExecuteRequestAsync(request, cookieContainer);
} }
while (response.HasHttpRedirect); while (response.HasHttpRedirect);
-14
View File
@@ -62,20 +62,6 @@ namespace NzbDrone.Common.Http
StatusCode == HttpStatusCode.TemporaryRedirect || StatusCode == HttpStatusCode.TemporaryRedirect ||
StatusCode == HttpStatusCode.Found; StatusCode == HttpStatusCode.Found;
public string RedirectUrl
{
get
{
var newUrl = Headers["Location"];
if (newUrl == null)
{
return string.Empty;
}
return (Request.Url += new HttpUri(newUrl)).FullUri;
}
}
public string[] GetCookieHeaders() public string[] GetCookieHeaders()
{ {
return Headers.GetValues("Set-Cookie") ?? Array.Empty<string>(); return Headers.GetValues("Set-Cookie") ?? Array.Empty<string>();
@@ -11,15 +11,13 @@ namespace NzbDrone.Common.Instrumentation
private static readonly Regex[] CleansingRules = new[] private static readonly Regex[] CleansingRules = new[]
{ {
// Url // Url
new Regex(@"(?<=\?|&|: |;)(apikey|token|passkey|auth|authkey|user|uid|api|[a-z_]*apikey|account|passwd|pwd)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), new Regex(@"(?<=\?|&|: |;)(apikey|token|passkey|auth|authkey|user|uid|api|[a-z_]*apikey|account|passwd)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?<=\?|&| )[^=]*?(_?(?<!use)token|username|passwo?rd)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), new Regex(@"(?<=\?|&)[^=]*?(username|password)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"torrentleech\.org/(?!rss)(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase), new Regex(@"torrentleech\.org/(?!rss)(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"torrentleech\.org/rss/download/[0-9]+/(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase), new Regex(@"torrentleech\.org/rss/download/[0-9]+/(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"iptorrents\.com/[/a-z0-9?&;]*?(?:[?&;](u|tp)=(?<secret>[^&=;]+?))+(?= |;|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), new Regex(@"iptorrents\.com/[/a-z0-9?&;]*?(?:[?&;](u|tp)=(?<secret>[^&=;]+?))+(?= |;|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"/fetch/[a-z0-9]{32}/(?<secret>[a-z0-9]{32})", RegexOptions.Compiled), new Regex(@"/fetch/[a-z0-9]{32}/(?<secret>[a-z0-9]{32})", RegexOptions.Compiled),
new Regex(@"getnzb.*?(?<=\?|&)(r)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), new Regex(@"getnzb.*?(?<=\?|&)(r)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?<=authkey = "")(?<secret>[^&=]+?)(?="")", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?<=beyond-hd\.[a-z]+/api/torrents/)(?<secret>[^&=][a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Path // Path
new Regex(@"""C:\\Users\\(?<secret>[^\""]+?)(\\|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), new Regex(@"""C:\\Users\\(?<secret>[^\""]+?)(\\|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
@@ -79,6 +77,7 @@ namespace NzbDrone.Common.Instrumentation
private static string CleanseRemoteIP(Match match) private static string CleanseRemoteIP(Match match)
{ {
var group = match.Groups[1]; var group = match.Groups[1];
var valueAll = match.Value;
var valueIP = group.Value; var valueIP = group.Value;
if (IPAddress.TryParse(valueIP, out var address) && !address.IsLocalAddress()) if (IPAddress.TryParse(valueIP, out var address) && !address.IsLocalAddress())
@@ -1,227 +0,0 @@
using System;
using System.Collections.Generic;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.Download;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.Download
{
[TestFixture]
public class DownloadClientProviderFixture : CoreTest<DownloadClientProvider>
{
private List<IDownloadClient> _downloadClients;
private List<DownloadClientStatus> _blockedProviders;
private int _nextId;
[SetUp]
public void SetUp()
{
_downloadClients = new List<IDownloadClient>();
_blockedProviders = new List<DownloadClientStatus>();
_nextId = 1;
Mocker.GetMock<IDownloadClientFactory>()
.Setup(v => v.GetAvailableProviders())
.Returns(_downloadClients);
Mocker.GetMock<IDownloadClientStatusService>()
.Setup(v => v.GetBlockedProviders())
.Returns(_blockedProviders);
}
private Mock<IDownloadClient> WithUsenetClient(int priority = 0)
{
var mock = new Mock<IDownloadClient>(MockBehavior.Default);
mock.SetupGet(s => s.Definition)
.Returns(Builder<DownloadClientDefinition>
.CreateNew()
.With(v => v.Id = _nextId++)
.With(v => v.Priority = priority)
.Build());
_downloadClients.Add(mock.Object);
mock.SetupGet(v => v.Protocol).Returns(DownloadProtocol.Usenet);
return mock;
}
private Mock<IDownloadClient> WithTorrentClient(int priority = 0)
{
var mock = new Mock<IDownloadClient>(MockBehavior.Default);
mock.SetupGet(s => s.Definition)
.Returns(Builder<DownloadClientDefinition>
.CreateNew()
.With(v => v.Id = _nextId++)
.With(v => v.Priority = priority)
.Build());
_downloadClients.Add(mock.Object);
mock.SetupGet(v => v.Protocol).Returns(DownloadProtocol.Torrent);
return mock;
}
private void GivenBlockedClient(int id)
{
_blockedProviders.Add(new DownloadClientStatus
{
ProviderId = id,
DisabledTill = DateTime.UtcNow.AddHours(3)
});
}
[Test]
public void should_roundrobin_over_usenet_client()
{
WithUsenetClient();
WithUsenetClient();
WithUsenetClient();
WithTorrentClient();
var client1 = Subject.GetDownloadClient(DownloadProtocol.Usenet);
var client2 = Subject.GetDownloadClient(DownloadProtocol.Usenet);
var client3 = Subject.GetDownloadClient(DownloadProtocol.Usenet);
var client4 = Subject.GetDownloadClient(DownloadProtocol.Usenet);
var client5 = Subject.GetDownloadClient(DownloadProtocol.Usenet);
client1.Definition.Id.Should().Be(1);
client2.Definition.Id.Should().Be(2);
client3.Definition.Id.Should().Be(3);
client4.Definition.Id.Should().Be(1);
client5.Definition.Id.Should().Be(2);
}
[Test]
public void should_roundrobin_over_torrent_client()
{
WithUsenetClient();
WithTorrentClient();
WithTorrentClient();
WithTorrentClient();
var client1 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
var client2 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
var client3 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
var client4 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
var client5 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
client1.Definition.Id.Should().Be(2);
client2.Definition.Id.Should().Be(3);
client3.Definition.Id.Should().Be(4);
client4.Definition.Id.Should().Be(2);
client5.Definition.Id.Should().Be(3);
}
[Test]
public void should_roundrobin_over_protocol_separately()
{
WithUsenetClient();
WithTorrentClient();
WithTorrentClient();
var client1 = Subject.GetDownloadClient(DownloadProtocol.Usenet);
var client2 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
var client3 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
var client4 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
client1.Definition.Id.Should().Be(1);
client2.Definition.Id.Should().Be(2);
client3.Definition.Id.Should().Be(3);
client4.Definition.Id.Should().Be(2);
}
[Test]
public void should_skip_blocked_torrent_client()
{
WithUsenetClient();
WithTorrentClient();
WithTorrentClient();
WithTorrentClient();
GivenBlockedClient(3);
var client1 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
var client2 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
var client3 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
var client4 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
var client5 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
client1.Definition.Id.Should().Be(2);
client2.Definition.Id.Should().Be(4);
client3.Definition.Id.Should().Be(2);
client4.Definition.Id.Should().Be(4);
}
[Test]
public void should_not_skip_blocked_torrent_client_if_all_blocked()
{
WithUsenetClient();
WithTorrentClient();
WithTorrentClient();
WithTorrentClient();
GivenBlockedClient(2);
GivenBlockedClient(3);
GivenBlockedClient(4);
var client1 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
var client2 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
var client3 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
var client4 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
var client5 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
client1.Definition.Id.Should().Be(2);
client2.Definition.Id.Should().Be(3);
client3.Definition.Id.Should().Be(4);
client4.Definition.Id.Should().Be(2);
}
[Test]
public void should_skip_secondary_prio_torrent_client()
{
WithUsenetClient();
WithTorrentClient(2);
WithTorrentClient();
WithTorrentClient();
var client1 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
var client2 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
var client3 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
var client4 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
var client5 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
client1.Definition.Id.Should().Be(3);
client2.Definition.Id.Should().Be(4);
client3.Definition.Id.Should().Be(3);
client4.Definition.Id.Should().Be(4);
}
[Test]
public void should_not_skip_secondary_prio_torrent_client_if_primary_blocked()
{
WithUsenetClient();
WithTorrentClient(2);
WithTorrentClient(2);
WithTorrentClient();
GivenBlockedClient(4);
var client1 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
var client2 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
var client3 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
var client4 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
var client5 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
client1.Definition.Id.Should().Be(2);
client2.Definition.Id.Should().Be(3);
client3.Definition.Id.Should().Be(2);
client4.Definition.Id.Should().Be(3);
}
}
}
@@ -1,161 +0,0 @@
using System;
using System.Linq;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Download;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.Download
{
public class DownloadClientStatusServiceFixture : CoreTest<DownloadClientStatusService>
{
private DateTime _epoch;
[SetUp]
public void SetUp()
{
_epoch = DateTime.UtcNow;
Mocker.GetMock<IRuntimeInfo>()
.SetupGet(v => v.StartTime)
.Returns(_epoch - TimeSpan.FromHours(1));
}
private DownloadClientStatus WithStatus(DownloadClientStatus status)
{
Mocker.GetMock<IDownloadClientStatusRepository>()
.Setup(v => v.FindByProviderId(1))
.Returns(status);
Mocker.GetMock<IDownloadClientStatusRepository>()
.Setup(v => v.All())
.Returns(new[] { status });
return status;
}
private void VerifyUpdate()
{
Mocker.GetMock<IDownloadClientStatusRepository>()
.Verify(v => v.Upsert(It.IsAny<DownloadClientStatus>()), Times.Once());
}
private void VerifyNoUpdate()
{
Mocker.GetMock<IDownloadClientStatusRepository>()
.Verify(v => v.Upsert(It.IsAny<DownloadClientStatus>()), Times.Never());
}
[Test]
public void should_not_consider_blocked_within_5_minutes_since_initial_failure()
{
WithStatus(new DownloadClientStatus
{
InitialFailure = _epoch - TimeSpan.FromMinutes(4),
MostRecentFailure = _epoch - TimeSpan.FromSeconds(4),
EscalationLevel = 3
});
Subject.RecordFailure(1);
VerifyUpdate();
var status = Subject.GetBlockedProviders().FirstOrDefault();
status.Should().BeNull();
}
[Test]
public void should_consider_blocked_after_5_minutes_since_initial_failure()
{
WithStatus(new DownloadClientStatus
{
InitialFailure = _epoch - TimeSpan.FromMinutes(6),
MostRecentFailure = _epoch - TimeSpan.FromSeconds(120),
EscalationLevel = 3
});
Subject.RecordFailure(1);
VerifyUpdate();
var status = Subject.GetBlockedProviders().FirstOrDefault();
status.Should().NotBeNull();
}
[Test]
public void should_not_escalate_further_till_after_5_minutes_since_initial_failure()
{
var origStatus = WithStatus(new DownloadClientStatus
{
InitialFailure = _epoch - TimeSpan.FromMinutes(4),
MostRecentFailure = _epoch - TimeSpan.FromSeconds(4),
EscalationLevel = 3
});
Subject.RecordFailure(1);
Subject.RecordFailure(1);
Subject.RecordFailure(1);
Subject.RecordFailure(1);
Subject.RecordFailure(1);
Subject.RecordFailure(1);
Subject.RecordFailure(1);
var status = Subject.GetBlockedProviders().FirstOrDefault();
status.Should().BeNull();
origStatus.EscalationLevel.Should().Be(3);
}
[Test]
public void should_escalate_further_after_5_minutes_since_initial_failure()
{
WithStatus(new DownloadClientStatus
{
InitialFailure = _epoch - TimeSpan.FromMinutes(6),
MostRecentFailure = _epoch - TimeSpan.FromSeconds(120),
EscalationLevel = 3
});
Subject.RecordFailure(1);
Subject.RecordFailure(1);
Subject.RecordFailure(1);
Subject.RecordFailure(1);
Subject.RecordFailure(1);
Subject.RecordFailure(1);
Subject.RecordFailure(1);
var status = Subject.GetBlockedProviders().FirstOrDefault();
status.Should().NotBeNull();
status.EscalationLevel.Should().BeGreaterThan(3);
}
[Test]
public void should_not_escalate_beyond_3_hours()
{
WithStatus(new DownloadClientStatus
{
InitialFailure = _epoch - TimeSpan.FromMinutes(6),
MostRecentFailure = _epoch - TimeSpan.FromSeconds(120),
EscalationLevel = 3
});
Subject.RecordFailure(1);
Subject.RecordFailure(1);
Subject.RecordFailure(1);
Subject.RecordFailure(1);
Subject.RecordFailure(1);
Subject.RecordFailure(1);
Subject.RecordFailure(1);
Subject.RecordFailure(1);
Subject.RecordFailure(1);
var status = Subject.GetBlockedProviders().FirstOrDefault();
status.Should().NotBeNull();
status.DisabledTill.Should().HaveValue();
status.DisabledTill.Should().NotBeAfter(_epoch + TimeSpan.FromHours(3.1));
}
}
}
@@ -33,8 +33,14 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
Subject.Check().ShouldBeWarning(); Subject.Check().ShouldBeWarning();
} }
[TestCase("Develop")]
[TestCase("develop")] [TestCase("develop")]
public void should_return_error_when_branch_is_v1(string branch)
{
GivenValidBranch(branch);
Subject.Check().ShouldBeError();
}
[TestCase("nightly")] [TestCase("nightly")]
[TestCase("Nightly")] [TestCase("Nightly")]
public void should_return_no_warning_when_branch_valid(string branch) public void should_return_no_warning_when_branch_valid(string branch)
@@ -1,4 +1,4 @@
using FluentAssertions; using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Core.HealthCheck; using NzbDrone.Core.HealthCheck;
using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Test.Framework;
@@ -10,9 +10,9 @@ namespace NzbDrone.Core.Test.HealthCheck
{ {
private const string WikiRoot = "https://wiki.servarr.com/"; private const string WikiRoot = "https://wiki.servarr.com/";
[TestCase("I blew up because of some weird user mistake", null, WikiRoot + "prowlarr/system#i-blew-up-because-of-some-weird-user-mistake")] [TestCase("I blew up because of some weird user mistake", null, WikiRoot + "Prowlarr_System#i_blew_up_because_of_some_weird_user_mistake")]
[TestCase("I blew up because of some weird user mistake", "#my-health-check", WikiRoot + "prowlarr/system#my-health-check")] [TestCase("I blew up because of some weird user mistake", "#my_health_check", WikiRoot + "Prowlarr_System#my_health_check")]
[TestCase("I blew up because of some weird user mistake", "custom-page#my-health-check", WikiRoot + "prowlarr/custom-page#my-health-check")] [TestCase("I blew up because of some weird user mistake", "Custom-Page#my_health_check", WikiRoot + "Custom-Page#my_health_check")]
public void should_format_wiki_url(string message, string wikiFragment, string expectedUrl) public void should_format_wiki_url(string message, string wikiFragment, string expectedUrl)
{ {
var subject = new NzbDrone.Core.HealthCheck.HealthCheck(typeof(HealthCheckBase), HealthCheckResult.Warning, message, wikiFragment); var subject = new NzbDrone.Core.HealthCheck.HealthCheck(typeof(HealthCheckBase), HealthCheckResult.Warning, message, wikiFragment);
@@ -2,7 +2,6 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using FluentAssertions; using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.FileList; using NzbDrone.Core.Indexers.FileList;
using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Test.Framework;
@@ -19,42 +18,16 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
Subject.Settings = new FileListSettings() Subject.Settings = new FileListSettings()
{ {
Passkey = "abcd", Passkey = "abcd",
Username = "somename", Username = "somename"
BaseUrl = "https://filelist.io"
}; };
Subject.Capabilities = new IndexerCapabilities
{
TvSearchParams = new List<TvSearchParam>
{
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
},
MovieSearchParams = new List<MovieSearchParam>
{
MovieSearchParam.Q, MovieSearchParam.ImdbId
},
MusicSearchParams = new List<MusicSearchParam>
{
MusicSearchParam.Q
},
BookSearchParams = new List<BookSearchParam>
{
BookSearchParam.Q
},
Flags = new List<IndexerFlag>
{
IndexerFlag.FreeLeech
}
};
Subject.Capabilities.Categories.AddCategoryMapping(1, NewznabStandardCategory.MoviesSD, "Filme SD");
Subject.Capabilities.Categories.AddCategoryMapping(2, NewznabStandardCategory.MoviesDVD, "Filme DVD");
_movieSearchCriteria = new MovieSearchCriteria _movieSearchCriteria = new MovieSearchCriteria
{ {
SearchTerm = "Star Wars", SearchTerm = "Star Wars",
Categories = new int[] { 2000 } Categories = new int[] { 2000 }
}; };
Subject.BaseUrl = "https://filelist.io";
} }
private void MovieWithoutIMDB() private void MovieWithoutIMDB()
@@ -65,7 +38,7 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
[Test] [Test]
public void should_use_categories_for_feed() public void should_use_categories_for_feed()
{ {
var results = Subject.GetSearchRequests(new MovieSearchCriteria { Categories = new int[] { NewznabStandardCategory.MoviesSD.Id, NewznabStandardCategory.MoviesDVD.Id } }); var results = Subject.GetSearchRequests(new MovieSearchCriteria { Categories = new int[] { 1, 2 } });
results.GetAllTiers().Should().HaveCount(1); results.GetAllTiers().Should().HaveCount(1);
@@ -77,7 +50,7 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
[Test] [Test]
public void should_not_search_by_imdbid_if_not_supported() public void should_not_search_by_imdbid_if_not_supported()
{ {
_movieSearchCriteria.ImdbId = "0076759"; _movieSearchCriteria.ImdbId = "tt0076759";
var results = Subject.GetSearchRequests(_movieSearchCriteria); var results = Subject.GetSearchRequests(_movieSearchCriteria);
results.GetAllTiers().Should().HaveCount(1); results.GetAllTiers().Should().HaveCount(1);
@@ -34,7 +34,7 @@ namespace NzbDrone.Core.Test.IndexerTests.HDBitsTests
_movieSearchCriteria = new MovieSearchCriteria _movieSearchCriteria = new MovieSearchCriteria
{ {
Categories = new int[] { 2000, 2010 }, Categories = new int[] { 2000, 2010 },
ImdbId = "0076759" ImdbId = "tt0076759"
}; };
} }
@@ -48,7 +48,7 @@ namespace NzbDrone.Core.Test.IndexerTests.HDBitsTests
_movieSearchCriteria = new MovieSearchCriteria _movieSearchCriteria = new MovieSearchCriteria
{ {
Categories = new int[] { 2000, 2010 }, Categories = new int[] { 2000, 2010 },
ImdbId = "0076759" ImdbId = "tt0076759"
}; };
} }
@@ -56,7 +56,7 @@ namespace NzbDrone.Core.Test.IndexerTests.HDBitsTests
public void should_search_by_imdbid_if_supported() public void should_search_by_imdbid_if_supported()
{ {
var results = Subject.GetSearchRequests(_movieSearchCriteria); var results = Subject.GetSearchRequests(_movieSearchCriteria);
var imdbQuery = int.Parse(_movieSearchCriteria.ImdbId); var imdbQuery = int.Parse(_movieSearchCriteria.ImdbId.Substring(2));
results.GetAllTiers().Should().HaveCount(1); results.GetAllTiers().Should().HaveCount(1);
@@ -1,17 +1,15 @@
using NLog; using NLog;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Download;
using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Core.Test.IndexerTests namespace NzbDrone.Core.Test.IndexerTests
{ {
public class TestIndexer : UsenetIndexerBase<TestIndexerSettings> public class TestIndexer : HttpIndexerBase<TestIndexerSettings>
{ {
public override string Name => "Test Indexer"; public override string Name => "Test Indexer";
public override string[] IndexerUrls => new string[] { "http://testindexer.com" }; public override string BaseUrl => "http://testindexer.com";
public override string Description => "";
public override DownloadProtocol Protocol => DownloadProtocol.Usenet; public override DownloadProtocol Protocol => DownloadProtocol.Usenet;
@@ -20,8 +18,8 @@ namespace NzbDrone.Core.Test.IndexerTests
public int _supportedPageSize; public int _supportedPageSize;
public override int PageSize => _supportedPageSize; public override int PageSize => _supportedPageSize;
public TestIndexer(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, IValidateNzbs nzbValidationService, Logger logger) public TestIndexer(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, nzbValidationService, logger) : base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{ {
} }
@@ -1,10 +1,12 @@
using System; using System;
using System.Collections.Generic;
using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Test.IndexerTests namespace NzbDrone.Core.Test.IndexerTests
{ {
public class TestIndexerSettings : IIndexerSettings public class TestIndexerSettings : IProviderConfig
{ {
public NzbDroneValidationResult Validate() public NzbDroneValidationResult Validate()
{ {
@@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net;
using FluentValidation.Results; using FluentValidation.Results;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using NLog; using NLog;
@@ -32,25 +31,7 @@ namespace NzbDrone.Core.Applications.Lidarr
{ {
var failures = new List<ValidationFailure>(); var failures = new List<ValidationFailure>();
var testIndexer = new IndexerDefinition failures.AddIfNotNull(_lidarrV1Proxy.Test(Settings));
{
Id = 0,
Name = "Test",
Protocol = DownloadProtocol.Usenet,
Capabilities = new IndexerCapabilities()
};
testIndexer.Capabilities.Categories.AddCategoryMapping(1, NewznabStandardCategory.Audio);
try
{
failures.AddIfNotNull(_lidarrV1Proxy.TestConnection(BuildLidarrIndexer(testIndexer, DownloadProtocol.Usenet), Settings));
}
catch (WebException ex)
{
_logger.Error(ex, "Unable to send test message");
failures.AddIfNotNull(new ValidationFailure("BaseUrl", "Unable to complete application test, cannot connect to Lidarr"));
}
return new ValidationResult(failures); return new ValidationResult(failures);
} }
@@ -165,7 +146,7 @@ namespace NzbDrone.Core.Applications.Lidarr
Fields = schema.Fields, Fields = schema.Fields,
}; };
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/"; lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl}/{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;
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "categories").Value = JArray.FromObject(indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray())); lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "categories").Value = JArray.FromObject(indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()));
@@ -33,7 +33,7 @@ namespace NzbDrone.Core.Applications.Lidarr
return other.EnableRss == EnableRss && return other.EnableRss == EnableRss &&
other.EnableAutomaticSearch == EnableAutomaticSearch && other.EnableAutomaticSearch == EnableAutomaticSearch &&
other.EnableInteractiveSearch == EnableInteractiveSearch && other.EnableInteractiveSearch == EnableAutomaticSearch &&
other.Name == Name && other.Name == Name &&
other.Implementation == Implementation && other.Implementation == Implementation &&
other.Priority == Priority && other.Priority == Priority &&
@@ -28,7 +28,7 @@ namespace NzbDrone.Core.Applications.Lidarr
public IEnumerable<int> SyncCategories { get; set; } public IEnumerable<int> SyncCategories { get; set; }
[FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Lidarr sees it, including http(s)://, port, and urlbase if needed")] [FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Lidarr sees it, including http(s):// and port if needed")]
public string ProwlarrUrl { get; set; } public string ProwlarrUrl { get; set; }
[FieldDefinition(1, Label = "Lidarr Server", HelpText = "Lidarr server URL, including http(s):// and port if needed")] [FieldDefinition(1, Label = "Lidarr Server", HelpText = "Lidarr server URL, including http(s):// and port if needed")]
@@ -17,7 +17,7 @@ namespace NzbDrone.Core.Applications.Lidarr
List<LidarrIndexer> GetIndexerSchema(LidarrSettings settings); List<LidarrIndexer> GetIndexerSchema(LidarrSettings settings);
void RemoveIndexer(int indexerId, LidarrSettings settings); void RemoveIndexer(int indexerId, LidarrSettings settings);
LidarrIndexer UpdateIndexer(LidarrIndexer indexer, LidarrSettings settings); LidarrIndexer UpdateIndexer(LidarrIndexer indexer, LidarrSettings settings);
ValidationFailure TestConnection(LidarrIndexer indexer, LidarrSettings settings); ValidationFailure Test(LidarrSettings settings);
} }
public class LidarrV1Proxy : ILidarrV1Proxy public class LidarrV1Proxy : ILidarrV1Proxy
@@ -91,15 +91,11 @@ namespace NzbDrone.Core.Applications.Lidarr
return Execute<LidarrIndexer>(request); return Execute<LidarrIndexer>(request);
} }
public ValidationFailure TestConnection(LidarrIndexer indexer, LidarrSettings settings) public ValidationFailure Test(LidarrSettings settings)
{ {
var request = BuildRequest(settings, $"/api/v1/indexer/test", HttpMethod.POST);
request.SetContent(indexer.ToJson());
try try
{ {
Execute<LidarrIndexer>(request); GetStatus(settings);
} }
catch (HttpException ex) catch (HttpException ex)
{ {
@@ -109,14 +105,8 @@ namespace NzbDrone.Core.Applications.Lidarr
return new ValidationFailure("ApiKey", "API Key is invalid"); return new ValidationFailure("ApiKey", "API Key is invalid");
} }
if (ex.Response.StatusCode == HttpStatusCode.BadRequest)
{
_logger.Error(ex, "Prowlarr URL is invalid");
return new ValidationFailure("ProwlarrUrl", "Prowlarr url is invalid, Lidarr cannot connect to Prowlarr");
}
_logger.Error(ex, "Unable to send test message"); _logger.Error(ex, "Unable to send test message");
return new ValidationFailure("BaseUrl", "Unable to complete application test"); return new ValidationFailure("ApiKey", "Unable to send test message");
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net;
using FluentValidation.Results; using FluentValidation.Results;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using NLog; using NLog;
@@ -32,25 +31,7 @@ namespace NzbDrone.Core.Applications.Radarr
{ {
var failures = new List<ValidationFailure>(); var failures = new List<ValidationFailure>();
var testIndexer = new IndexerDefinition failures.AddIfNotNull(_radarrV3Proxy.Test(Settings));
{
Id = 0,
Name = "Test",
Protocol = DownloadProtocol.Usenet,
Capabilities = new IndexerCapabilities()
};
testIndexer.Capabilities.Categories.AddCategoryMapping(1, NewznabStandardCategory.Movies);
try
{
failures.AddIfNotNull(_radarrV3Proxy.TestConnection(BuildRadarrIndexer(testIndexer, DownloadProtocol.Usenet), Settings));
}
catch (WebException ex)
{
_logger.Error(ex, "Unable to send test message");
failures.AddIfNotNull(new ValidationFailure("BaseUrl", "Unable to complete application test, cannot connect to Radarr"));
}
return new ValidationResult(failures); return new ValidationResult(failures);
} }
@@ -165,7 +146,7 @@ namespace NzbDrone.Core.Applications.Radarr
Fields = schema.Fields, Fields = schema.Fields,
}; };
radarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/"; radarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl}/{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;
radarrIndexer.Fields.FirstOrDefault(x => x.Name == "categories").Value = JArray.FromObject(indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray())); radarrIndexer.Fields.FirstOrDefault(x => x.Name == "categories").Value = JArray.FromObject(indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()));
@@ -33,7 +33,7 @@ namespace NzbDrone.Core.Applications.Radarr
return other.EnableRss == EnableRss && return other.EnableRss == EnableRss &&
other.EnableAutomaticSearch == EnableAutomaticSearch && other.EnableAutomaticSearch == EnableAutomaticSearch &&
other.EnableInteractiveSearch == EnableInteractiveSearch && other.EnableInteractiveSearch == EnableAutomaticSearch &&
other.Name == Name && other.Name == Name &&
other.Implementation == Implementation && other.Implementation == Implementation &&
other.Priority == Priority && other.Priority == Priority &&
@@ -28,7 +28,7 @@ namespace NzbDrone.Core.Applications.Radarr
public IEnumerable<int> SyncCategories { get; set; } public IEnumerable<int> SyncCategories { get; set; }
[FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Radarr sees it, including http(s)://, port, and urlbase if needed")] [FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Radarr sees it, including http(s):// and port if needed")]
public string ProwlarrUrl { get; set; } public string ProwlarrUrl { get; set; }
[FieldDefinition(1, Label = "Radarr Server", HelpText = "Radarr server URL, including http(s):// and port if needed")] [FieldDefinition(1, Label = "Radarr Server", HelpText = "Radarr server URL, including http(s):// and port if needed")]
@@ -17,7 +17,7 @@ namespace NzbDrone.Core.Applications.Radarr
List<RadarrIndexer> GetIndexerSchema(RadarrSettings settings); List<RadarrIndexer> GetIndexerSchema(RadarrSettings settings);
void RemoveIndexer(int indexerId, RadarrSettings settings); void RemoveIndexer(int indexerId, RadarrSettings settings);
RadarrIndexer UpdateIndexer(RadarrIndexer indexer, RadarrSettings settings); RadarrIndexer UpdateIndexer(RadarrIndexer indexer, RadarrSettings settings);
ValidationFailure TestConnection(RadarrIndexer indexer, RadarrSettings settings); ValidationFailure Test(RadarrSettings settings);
} }
public class RadarrV3Proxy : IRadarrV3Proxy public class RadarrV3Proxy : IRadarrV3Proxy
@@ -91,15 +91,11 @@ namespace NzbDrone.Core.Applications.Radarr
return Execute<RadarrIndexer>(request); return Execute<RadarrIndexer>(request);
} }
public ValidationFailure TestConnection(RadarrIndexer indexer, RadarrSettings settings) public ValidationFailure Test(RadarrSettings settings)
{ {
var request = BuildRequest(settings, $"/api/v3/indexer/test", HttpMethod.POST);
request.SetContent(indexer.ToJson());
try try
{ {
Execute<RadarrIndexer>(request); GetStatus(settings);
} }
catch (HttpException ex) catch (HttpException ex)
{ {
@@ -109,14 +105,8 @@ namespace NzbDrone.Core.Applications.Radarr
return new ValidationFailure("ApiKey", "API Key is invalid"); return new ValidationFailure("ApiKey", "API Key is invalid");
} }
if (ex.Response.StatusCode == HttpStatusCode.BadRequest)
{
_logger.Error(ex, "Prowlarr URL is invalid");
return new ValidationFailure("ProwlarrUrl", "Prowlarr url is invalid, Radarr cannot connect to Prowlarr");
}
_logger.Error(ex, "Unable to send test message"); _logger.Error(ex, "Unable to send test message");
return new ValidationFailure("BaseUrl", "Unable to complete application test"); return new ValidationFailure("ApiKey", "Unable to send test message");
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net;
using FluentValidation.Results; using FluentValidation.Results;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using NLog; using NLog;
@@ -32,25 +31,7 @@ namespace NzbDrone.Core.Applications.Readarr
{ {
var failures = new List<ValidationFailure>(); var failures = new List<ValidationFailure>();
var testIndexer = new IndexerDefinition failures.AddIfNotNull(_readarrV1Proxy.Test(Settings));
{
Id = 0,
Name = "Test",
Protocol = DownloadProtocol.Usenet,
Capabilities = new IndexerCapabilities()
};
testIndexer.Capabilities.Categories.AddCategoryMapping(1, NewznabStandardCategory.Books);
try
{
failures.AddIfNotNull(_readarrV1Proxy.TestConnection(BuildReadarrIndexer(testIndexer, DownloadProtocol.Usenet), Settings));
}
catch (WebException ex)
{
_logger.Error(ex, "Unable to send test message");
failures.AddIfNotNull(new ValidationFailure("BaseUrl", "Unable to complete application test, cannot connect to Readarr"));
}
return new ValidationResult(failures); return new ValidationResult(failures);
} }
@@ -165,7 +146,7 @@ namespace NzbDrone.Core.Applications.Readarr
Fields = schema.Fields, Fields = schema.Fields,
}; };
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/"; readarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl}/{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;
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "categories").Value = JArray.FromObject(indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray())); readarrIndexer.Fields.FirstOrDefault(x => x.Name == "categories").Value = JArray.FromObject(indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()));
@@ -33,7 +33,7 @@ namespace NzbDrone.Core.Applications.Readarr
return other.EnableRss == EnableRss && return other.EnableRss == EnableRss &&
other.EnableAutomaticSearch == EnableAutomaticSearch && other.EnableAutomaticSearch == EnableAutomaticSearch &&
other.EnableInteractiveSearch == EnableInteractiveSearch && other.EnableInteractiveSearch == EnableAutomaticSearch &&
other.Name == Name && other.Name == Name &&
other.Implementation == Implementation && other.Implementation == Implementation &&
other.Priority == Priority && other.Priority == Priority &&
@@ -28,7 +28,7 @@ namespace NzbDrone.Core.Applications.Readarr
public IEnumerable<int> SyncCategories { get; set; } public IEnumerable<int> SyncCategories { get; set; }
[FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Readarr sees it, including http(s)://, port, and urlbase if needed")] [FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Readarr sees it, including http(s):// and port if needed")]
public string ProwlarrUrl { get; set; } public string ProwlarrUrl { get; set; }
[FieldDefinition(1, Label = "Readarr Server", HelpText = "Readarr server URL, including http(s):// and port if needed")] [FieldDefinition(1, Label = "Readarr Server", HelpText = "Readarr server URL, including http(s):// and port if needed")]
@@ -17,7 +17,7 @@ namespace NzbDrone.Core.Applications.Readarr
List<ReadarrIndexer> GetIndexerSchema(ReadarrSettings settings); List<ReadarrIndexer> GetIndexerSchema(ReadarrSettings settings);
void RemoveIndexer(int indexerId, ReadarrSettings settings); void RemoveIndexer(int indexerId, ReadarrSettings settings);
ReadarrIndexer UpdateIndexer(ReadarrIndexer indexer, ReadarrSettings settings); ReadarrIndexer UpdateIndexer(ReadarrIndexer indexer, ReadarrSettings settings);
ValidationFailure TestConnection(ReadarrIndexer indexer, ReadarrSettings settings); ValidationFailure Test(ReadarrSettings settings);
} }
public class ReadarrV1Proxy : IReadarrV1Proxy public class ReadarrV1Proxy : IReadarrV1Proxy
@@ -91,15 +91,11 @@ namespace NzbDrone.Core.Applications.Readarr
return Execute<ReadarrIndexer>(request); return Execute<ReadarrIndexer>(request);
} }
public ValidationFailure TestConnection(ReadarrIndexer indexer, ReadarrSettings settings) public ValidationFailure Test(ReadarrSettings settings)
{ {
var request = BuildRequest(settings, $"/api/v1/indexer/test", HttpMethod.POST);
request.SetContent(indexer.ToJson());
try try
{ {
Execute<ReadarrIndexer>(request); GetStatus(settings);
} }
catch (HttpException ex) catch (HttpException ex)
{ {
@@ -109,14 +105,8 @@ namespace NzbDrone.Core.Applications.Readarr
return new ValidationFailure("ApiKey", "API Key is invalid"); return new ValidationFailure("ApiKey", "API Key is invalid");
} }
if (ex.Response.StatusCode == HttpStatusCode.BadRequest)
{
_logger.Error(ex, "Prowlarr URL is invalid");
return new ValidationFailure("ProwlarrUrl", "Prowlarr url is invalid, Readarr cannot connect to Prowlarr");
}
_logger.Error(ex, "Unable to send test message"); _logger.Error(ex, "Unable to send test message");
return new ValidationFailure("BaseUrl", "Unable to complete application test"); return new ValidationFailure("ApiKey", "Unable to send test message");
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net;
using FluentValidation.Results; using FluentValidation.Results;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using NLog; using NLog;
@@ -32,25 +31,7 @@ namespace NzbDrone.Core.Applications.Sonarr
{ {
var failures = new List<ValidationFailure>(); var failures = new List<ValidationFailure>();
var testIndexer = new IndexerDefinition failures.AddIfNotNull(_sonarrV3Proxy.Test(Settings));
{
Id = 0,
Name = "Test",
Protocol = DownloadProtocol.Usenet,
Capabilities = new IndexerCapabilities()
};
testIndexer.Capabilities.Categories.AddCategoryMapping(1, NewznabStandardCategory.TV);
try
{
failures.AddIfNotNull(_sonarrV3Proxy.TestConnection(BuildSonarrIndexer(testIndexer, DownloadProtocol.Usenet), Settings));
}
catch (WebException ex)
{
_logger.Error(ex, "Unable to send test message");
failures.AddIfNotNull(new ValidationFailure("BaseUrl", "Unable to complete application test, cannot connect to Sonarr"));
}
return new ValidationResult(failures); return new ValidationResult(failures);
} }
@@ -165,7 +146,7 @@ namespace NzbDrone.Core.Applications.Sonarr
Fields = schema.Fields, Fields = schema.Fields,
}; };
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/"; sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl}/{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;
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "categories").Value = JArray.FromObject(indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray())); sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "categories").Value = JArray.FromObject(indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()));
@@ -34,7 +34,7 @@ namespace NzbDrone.Core.Applications.Sonarr
return other.EnableRss == EnableRss && return other.EnableRss == EnableRss &&
other.EnableAutomaticSearch == EnableAutomaticSearch && other.EnableAutomaticSearch == EnableAutomaticSearch &&
other.EnableInteractiveSearch == EnableInteractiveSearch && other.EnableInteractiveSearch == EnableAutomaticSearch &&
other.Name == Name && other.Name == Name &&
other.Implementation == Implementation && other.Implementation == Implementation &&
other.Priority == Priority && other.Priority == Priority &&
@@ -28,7 +28,7 @@ namespace NzbDrone.Core.Applications.Sonarr
public IEnumerable<int> SyncCategories { get; set; } public IEnumerable<int> SyncCategories { get; set; }
[FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Sonarr sees it, including http(s)://, port, and urlbase if needed")] [FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Sonarr sees it, including http(s):// and port if needed")]
public string ProwlarrUrl { get; set; } public string ProwlarrUrl { get; set; }
[FieldDefinition(1, Label = "Sonarr Server", HelpText = "Sonarr server URL, including http(s):// and port if needed")] [FieldDefinition(1, Label = "Sonarr Server", HelpText = "Sonarr server URL, including http(s):// and port if needed")]
@@ -17,7 +17,7 @@ namespace NzbDrone.Core.Applications.Sonarr
List<SonarrIndexer> GetIndexerSchema(SonarrSettings settings); List<SonarrIndexer> GetIndexerSchema(SonarrSettings settings);
void RemoveIndexer(int indexerId, SonarrSettings settings); void RemoveIndexer(int indexerId, SonarrSettings settings);
SonarrIndexer UpdateIndexer(SonarrIndexer indexer, SonarrSettings settings); SonarrIndexer UpdateIndexer(SonarrIndexer indexer, SonarrSettings settings);
ValidationFailure TestConnection(SonarrIndexer indexer, SonarrSettings settings); ValidationFailure Test(SonarrSettings settings);
} }
public class SonarrV3Proxy : ISonarrV3Proxy public class SonarrV3Proxy : ISonarrV3Proxy
@@ -91,15 +91,11 @@ namespace NzbDrone.Core.Applications.Sonarr
return Execute<SonarrIndexer>(request); return Execute<SonarrIndexer>(request);
} }
public ValidationFailure TestConnection(SonarrIndexer indexer, SonarrSettings settings) public ValidationFailure Test(SonarrSettings settings)
{ {
var request = BuildRequest(settings, $"/api/v3/indexer/test", HttpMethod.POST);
request.SetContent(indexer.ToJson());
try try
{ {
Execute<SonarrIndexer>(request); GetStatus(settings);
} }
catch (HttpException ex) catch (HttpException ex)
{ {
@@ -109,14 +105,8 @@ namespace NzbDrone.Core.Applications.Sonarr
return new ValidationFailure("ApiKey", "API Key is invalid"); return new ValidationFailure("ApiKey", "API Key is invalid");
} }
if (ex.Response.StatusCode == HttpStatusCode.BadRequest)
{
_logger.Error(ex, "Prowlarr URL is invalid");
return new ValidationFailure("ProwlarrUrl", "Prowlarr url is invalid, Sonarr cannot connect to Prowlarr");
}
_logger.Error(ex, "Unable to send test message"); _logger.Error(ex, "Unable to send test message");
return new ValidationFailure("BaseUrl", "Unable to complete application test"); return new ValidationFailure("ApiKey", "Unable to send test message");
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -1,7 +1,11 @@
using System; using System;
using System.Linq;
using System.Xml.Linq;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Core.Authentication namespace NzbDrone.Core.Authentication
{ {
@@ -180,7 +180,7 @@ namespace NzbDrone.Core.Configuration
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", "nightly").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);
@@ -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 = "nightly";
if (Path.GetFileName(bin) == "bin" && diskProvider.FileExists(packageInfoPath)) if (Path.GetFileName(bin) == "bin" && diskProvider.FileExists(packageInfoPath))
{ {
@@ -1,36 +0,0 @@
using System.Collections.Generic;
using System.Data;
using System.Text.Json;
using Dapper;
using NzbDrone.Common.Serializer;
namespace NzbDrone.Core.Datastore.Converters
{
public class CookieConverter : SqlMapper.TypeHandler<IDictionary<string, string>>
{
protected readonly JsonSerializerOptions SerializerSettings;
public CookieConverter()
{
var serializerSettings = new JsonSerializerOptions
{
AllowTrailingCommas = true,
IgnoreNullValues = true,
PropertyNameCaseInsensitive = true,
WriteIndented = true
};
SerializerSettings = serializerSettings;
}
public override void SetValue(IDbDataParameter parameter, IDictionary<string, string> value)
{
parameter.Value = JsonSerializer.Serialize(value, SerializerSettings);
}
public override IDictionary<string, string> Parse(object value)
{
return JsonSerializer.Deserialize<Dictionary<string, string>>((string)value, SerializerSettings);
}
}
}
+2 -2
View File
@@ -108,10 +108,10 @@ namespace NzbDrone.Core.Datastore
if (OsInfo.IsOsx) if (OsInfo.IsOsx)
{ {
throw new CorruptDatabaseException("Database file: {0} is corrupt, restore from backup if available. See: https://wiki.servarr.com/prowlarr/faq#i-use-prowlarr-on-a-mac-and-it-suddenly-stopped-working-what-happened", e, fileName); throw new CorruptDatabaseException("Database file: {0} is corrupt, restore from backup if available. See: https://wiki.servarr.com/Prowlarr_FAQ#I_use_Prowlarr_on_a_Mac_and_it_suddenly_stopped_working_What_happened", e, fileName);
} }
throw new CorruptDatabaseException("Database file: {0} is corrupt, restore from backup if available. See: https://wiki.servarr.com/prowlarr/faq#i-am-getting-an-error-database-disk-image-is-malformed", e, fileName); throw new CorruptDatabaseException("Database file: {0} is corrupt, restore from backup if available. See: https://wiki.servarr.com/Prowlarr_FAQ#I_am_getting_an_error_Database_disk_image_is_malformed", e, fileName);
} }
catch (Exception e) catch (Exception e)
{ {
@@ -1,3 +1,6 @@
using System;
using System.Collections.Generic;
using System.Data;
using FluentMigrator; using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework; using NzbDrone.Core.Datastore.Migration.Framework;
@@ -1,4 +1,5 @@
using FluentMigrator; using FluentMigrator;
using Newtonsoft.Json.Linq;
using NzbDrone.Core.Datastore.Migration.Framework; using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration namespace NzbDrone.Core.Datastore.Migration
+2 -2
View File
@@ -46,7 +46,7 @@ namespace NzbDrone.Core.Datastore
.Ignore(i => i.Description) .Ignore(i => i.Description)
.Ignore(i => i.Language) .Ignore(i => i.Language)
.Ignore(i => i.Encoding) .Ignore(i => i.Encoding)
.Ignore(i => i.IndexerUrls) .Ignore(i => i.BaseUrl)
.Ignore(i => i.Protocol) .Ignore(i => i.Protocol)
.Ignore(i => i.Privacy) .Ignore(i => i.Privacy)
.Ignore(i => i.SupportsRss) .Ignore(i => i.SupportsRss)
@@ -100,7 +100,7 @@ namespace NzbDrone.Core.Datastore
SqlMapper.RemoveTypeMap(typeof(DateTime)); SqlMapper.RemoveTypeMap(typeof(DateTime));
SqlMapper.AddTypeHandler(new DapperUtcConverter()); SqlMapper.AddTypeHandler(new DapperUtcConverter());
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<Dictionary<string, string>>()); SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<Dictionary<string, string>>());
SqlMapper.AddTypeHandler(new CookieConverter()); SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<IDictionary<string, string>>());
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<int>>()); SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<int>>());
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<KeyValuePair<string, int>>>()); SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<KeyValuePair<string, int>>>());
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<KeyValuePair<string, int>>()); SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<KeyValuePair<string, int>>());
@@ -31,8 +31,9 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
IHttpClient httpClient, IHttpClient httpClient,
IConfigService configService, IConfigService configService,
IDiskProvider diskProvider, IDiskProvider diskProvider,
IValidateNzbs nzbValidationService,
Logger logger) Logger logger)
: base(httpClient, configService, diskProvider, logger) : base(httpClient, configService, diskProvider, nzbValidationService, logger)
{ {
_dsInfoProxy = dsInfoProxy; _dsInfoProxy = dsInfoProxy;
_dsTaskProxy = dsTaskProxy; _dsTaskProxy = dsTaskProxy;
@@ -20,8 +20,9 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex
IHttpClient httpClient, IHttpClient httpClient,
IConfigService configService, IConfigService configService,
IDiskProvider diskProvider, IDiskProvider diskProvider,
IValidateNzbs nzbValidationService,
Logger logger) Logger logger)
: base(httpClient, configService, diskProvider, logger) : base(httpClient, configService, diskProvider, nzbValidationService, logger)
{ {
_proxy = proxy; _proxy = proxy;
} }
@@ -25,8 +25,9 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
IHttpClient httpClient, IHttpClient httpClient,
IConfigService configService, IConfigService configService,
IDiskProvider diskProvider, IDiskProvider diskProvider,
IValidateNzbs nzbValidationService,
Logger logger) Logger logger)
: base(httpClient, configService, diskProvider, logger) : base(httpClient, configService, diskProvider, nzbValidationService, logger)
{ {
_proxy = proxy; _proxy = proxy;
} }
@@ -41,10 +41,10 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
[FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)] [FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)]
public int Port { get; set; } public int Port { get; set; }
[FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secure connection when connecting to NZBGet")] [FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secure connection when connecting to Sabnzbd")]
public bool UseSsl { get; set; } public bool UseSsl { get; set; }
[FieldDefinition(3, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the NZBGet url, e.g. http://[host]:[port]/[urlBase]/jsonrpc")] [FieldDefinition(3, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the nzbget url, e.g. http://[host]:[port]/[urlBase]/jsonrpc")]
public string UrlBase { get; set; } public string UrlBase { get; set; }
[FieldDefinition(4, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)] [FieldDefinition(4, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)]
@@ -1,11 +1,10 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Threading.Tasks;
using FluentValidation.Results; using FluentValidation.Results;
using NLog; using NLog;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers;
using NzbDrone.Core.Parser; using NzbDrone.Core.Parser;
@@ -15,20 +14,24 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic
{ {
public class Pneumatic : DownloadClientBase<PneumaticSettings> public class Pneumatic : DownloadClientBase<PneumaticSettings>
{ {
public Pneumatic(IConfigService configService, private readonly IHttpClient _httpClient;
public Pneumatic(IHttpClient httpClient,
IConfigService configService,
IDiskProvider diskProvider, IDiskProvider diskProvider,
Logger logger) Logger logger)
: base(configService, diskProvider, logger) : base(configService, diskProvider, logger)
{ {
_httpClient = httpClient;
} }
public override string Name => "Pneumatic"; public override string Name => "Pneumatic";
public override DownloadProtocol Protocol => DownloadProtocol.Usenet; public override DownloadProtocol Protocol => DownloadProtocol.Usenet;
public override async Task<string> Download(ReleaseInfo release, bool redirect, IIndexer indexer) public override string Download(ReleaseInfo release, bool redirect)
{ {
var url = new Uri(release.DownloadUrl); var url = release.DownloadUrl;
var title = release.Title; var title = release.Title;
title = StringUtil.CleanFileName(title); title = StringUtil.CleanFileName(title);
@@ -37,10 +40,7 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic
var nzbFile = Path.Combine(Settings.NzbFolder, title + ".nzb"); var nzbFile = Path.Combine(Settings.NzbFolder, title + ".nzb");
_logger.Debug("Downloading NZB from: {0} to: {1}", url, nzbFile); _logger.Debug("Downloading NZB from: {0} to: {1}", url, nzbFile);
_httpClient.DownloadFile(url, nzbFile);
var nzbData = await indexer.Download(url);
File.WriteAllBytes(nzbFile, nzbData);
_logger.Debug("NZB Download succeeded, saved to: {0}", nzbFile); _logger.Debug("NZB Download succeeded, saved to: {0}", nzbFile);
@@ -22,8 +22,9 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
IHttpClient httpClient, IHttpClient httpClient,
IConfigService configService, IConfigService configService,
IDiskProvider diskProvider, IDiskProvider diskProvider,
IValidateNzbs nzbValidationService,
Logger logger) Logger logger)
: base(httpClient, configService, diskProvider, logger) : base(httpClient, configService, diskProvider, nzbValidationService, logger)
{ {
_proxy = proxy; _proxy = proxy;
} }
@@ -66,7 +66,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
{ {
_logger.Debug("rTorrent didn't add the torrent within {0} seconds: {1}.", tries * retryDelay / 1000, filename); _logger.Debug("rTorrent didn't add the torrent within {0} seconds: {1}.", tries * retryDelay / 1000, filename);
throw new ReleaseDownloadException("Downloading torrent failed"); throw new ReleaseDownloadException(release, "Downloading torrent failed");
} }
return hash; return hash;
@@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks;
using FluentValidation.Results; using FluentValidation.Results;
using NLog; using NLog;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
@@ -55,7 +54,7 @@ namespace NzbDrone.Core.Download
get; get;
} }
public abstract Task<string> Download(ReleaseInfo release, bool redirect, IIndexer indexer); public abstract string Download(ReleaseInfo release, bool redirect);
public ValidationResult Test() public ValidationResult Test()
{ {
@@ -17,7 +17,7 @@ namespace NzbDrone.Core.Download
{ {
public interface IDownloadService public interface IDownloadService
{ {
Task SendReportToClient(ReleaseInfo release, string source, string host, bool redirect); void SendReportToClient(ReleaseInfo release, string source, string host, bool redirect);
Task<byte[]> DownloadReport(string link, int indexerId, string source, string host, string title); Task<byte[]> DownloadReport(string link, int indexerId, string source, string host, string title);
void RecordRedirect(string link, int indexerId, string source, string host, string title); void RecordRedirect(string link, int indexerId, string source, string host, string title);
} }
@@ -49,7 +49,7 @@ namespace NzbDrone.Core.Download
_logger = logger; _logger = logger;
} }
public async Task SendReportToClient(ReleaseInfo release, string source, string host, bool redirect) public void SendReportToClient(ReleaseInfo release, string source, string host, bool redirect)
{ {
var downloadTitle = release.Title; var downloadTitle = release.Title;
var downloadClient = _downloadClientProvider.GetDownloadClient(release.DownloadProtocol); var downloadClient = _downloadClientProvider.GetDownloadClient(release.DownloadProtocol);
@@ -69,12 +69,10 @@ namespace NzbDrone.Core.Download
_rateLimitService.WaitAndPulse(url.Host, TimeSpan.FromSeconds(2)); _rateLimitService.WaitAndPulse(url.Host, TimeSpan.FromSeconds(2));
} }
var indexer = _indexerFactory.GetInstance(_indexerFactory.Get(release.IndexerId));
string downloadClientId; string downloadClientId;
try try
{ {
downloadClientId = await downloadClient.Download(release, redirect, indexer); downloadClientId = downloadClient.Download(release, redirect);
_downloadClientStatusService.RecordSuccess(downloadClient.Definition.Id); _downloadClientStatusService.RecordSuccess(downloadClient.Definition.Id);
_indexerStatusService.RecordSuccess(release.IndexerId); _indexerStatusService.RecordSuccess(release.IndexerId);
} }
@@ -1,4 +1,3 @@
using System.Threading.Tasks;
using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.ThingiProvider;
@@ -8,6 +7,6 @@ namespace NzbDrone.Core.Download
public interface IDownloadClient : IProvider public interface IDownloadClient : IProvider
{ {
DownloadProtocol Protocol { get; } DownloadProtocol Protocol { get; }
Task<string> Download(ReleaseInfo release, bool redirect, IIndexer indexer); string Download(ReleaseInfo release, bool redirect);
} }
} }
@@ -8,12 +8,12 @@ namespace NzbDrone.Core.Download
{ {
public interface IValidateNzbs public interface IValidateNzbs
{ {
void Validate(byte[] fileContent); void Validate(string filename, byte[] fileContent);
} }
public class NzbValidationService : IValidateNzbs public class NzbValidationService : IValidateNzbs
{ {
public void Validate(byte[] fileContent) public void Validate(string filename, byte[] fileContent)
{ {
var reader = new StreamReader(new MemoryStream(fileContent)); var reader = new StreamReader(new MemoryStream(fileContent));
@@ -24,7 +24,7 @@ namespace NzbDrone.Core.Download
if (nzb == null) if (nzb == null)
{ {
throw new InvalidNzbException("Invalid NZB: No Root element"); throw new InvalidNzbException("Invalid NZB: No Root element [{0}]", filename);
} }
// nZEDb has an bug in their error reporting code spitting out invalid http status codes // nZEDb has an bug in their error reporting code spitting out invalid http status codes
@@ -37,7 +37,7 @@ namespace NzbDrone.Core.Download
if (!nzb.Name.LocalName.Equals("nzb")) if (!nzb.Name.LocalName.Equals("nzb"))
{ {
throw new InvalidNzbException("Invalid NZB: Unexpected root element. Expected 'nzb' found '{0}'", nzb.Name.LocalName); throw new InvalidNzbException("Invalid NZB: Unexpected root element. Expected 'nzb' found '{0}' [{1}]", nzb.Name.LocalName, filename);
} }
var ns = nzb.Name.Namespace; var ns = nzb.Name.Namespace;
@@ -45,7 +45,7 @@ namespace NzbDrone.Core.Download
if (files.Empty()) if (files.Empty())
{ {
throw new InvalidNzbException("Invalid NZB: No files"); throw new InvalidNzbException("Invalid NZB: No files [{0}]", filename);
} }
} }
} }
+63 -21
View File
@@ -1,6 +1,5 @@
using System; using System;
using System.Text; using System.Net;
using System.Threading.Tasks;
using MonoTorrent; using MonoTorrent;
using NLog; using NLog;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
@@ -40,7 +39,7 @@ namespace NzbDrone.Core.Download
protected abstract string AddFromTorrentFile(ReleaseInfo release, string hash, string filename, byte[] fileContent); protected abstract string AddFromTorrentFile(ReleaseInfo release, string hash, string filename, byte[] fileContent);
protected abstract string AddFromTorrentLink(ReleaseInfo release, string hash, string torrentLink); protected abstract string AddFromTorrentLink(ReleaseInfo release, string hash, string torrentLink);
public override async Task<string> Download(ReleaseInfo release, bool redirect, IIndexer indexer) public override string Download(ReleaseInfo release, bool redirect)
{ {
var torrentInfo = release as TorrentInfo; var torrentInfo = release as TorrentInfo;
@@ -67,7 +66,7 @@ namespace NzbDrone.Core.Download
{ {
try try
{ {
return await DownloadFromWebUrl(release, indexer, torrentUrl); return DownloadFromWebUrl(release, torrentUrl);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -88,7 +87,7 @@ namespace NzbDrone.Core.Download
} }
catch (NotSupportedException ex) catch (NotSupportedException ex)
{ {
throw new ReleaseDownloadException("Magnet not supported by download client. ({0})", ex.Message); throw new ReleaseDownloadException(release, "Magnet not supported by download client. ({0})", ex.Message);
} }
} }
} }
@@ -104,7 +103,7 @@ namespace NzbDrone.Core.Download
{ {
if (torrentUrl.IsNullOrWhiteSpace()) if (torrentUrl.IsNullOrWhiteSpace())
{ {
throw new ReleaseDownloadException("Magnet not supported by download client. ({0})", ex.Message); throw new ReleaseDownloadException(release, "Magnet not supported by download client. ({0})", ex.Message);
} }
_logger.Debug("Magnet not supported by download client, trying torrent. ({0})", ex.Message); _logger.Debug("Magnet not supported by download client, trying torrent. ({0})", ex.Message);
@@ -113,31 +112,74 @@ namespace NzbDrone.Core.Download
if (torrentUrl.IsNotNullOrWhiteSpace()) if (torrentUrl.IsNotNullOrWhiteSpace())
{ {
return await DownloadFromWebUrl(release, indexer, torrentUrl); return DownloadFromWebUrl(release, torrentUrl);
} }
} }
return null; return null;
} }
private async Task<string> DownloadFromWebUrl(ReleaseInfo release, IIndexer indexer, string torrentUrl) private string DownloadFromWebUrl(ReleaseInfo release, string torrentUrl)
{ {
byte[] torrentFile = null; byte[] torrentFile = null;
torrentFile = await indexer.Download(new Uri(torrentUrl)); try
// handle magnet URLs
if (torrentFile.Length >= 7
&& torrentFile[0] == 0x6d
&& torrentFile[1] == 0x61
&& torrentFile[2] == 0x67
&& torrentFile[3] == 0x6e
&& torrentFile[4] == 0x65
&& torrentFile[5] == 0x74
&& torrentFile[6] == 0x3a)
{ {
var magnetUrl = Encoding.UTF8.GetString(torrentFile); var request = new HttpRequest(torrentUrl);
return DownloadFromMagnetUrl(release, magnetUrl); request.Headers.Accept = "application/x-bittorrent";
request.AllowAutoRedirect = false;
var response = _httpClient.Get(request);
if (response.StatusCode == HttpStatusCode.MovedPermanently ||
response.StatusCode == HttpStatusCode.Found ||
response.StatusCode == HttpStatusCode.SeeOther)
{
var locationHeader = response.Headers.GetSingleValue("Location");
_logger.Trace("Torrent request is being redirected to: {0}", locationHeader);
if (locationHeader != null)
{
if (locationHeader.StartsWith("magnet:"))
{
return DownloadFromMagnetUrl(release, locationHeader);
}
return DownloadFromWebUrl(release, locationHeader);
}
throw new WebException("Remote website tried to redirect without providing a location.");
}
torrentFile = response.ResponseData;
_logger.Debug("Downloading torrent for release '{0}' finished ({1} bytes from {2})", release.Title, torrentFile.Length, torrentUrl);
}
catch (HttpException ex)
{
if (ex.Response.StatusCode == HttpStatusCode.NotFound)
{
_logger.Error(ex, "Downloading torrent file for release '{0}' failed since it no longer exists ({1})", release.Title, torrentUrl);
throw new ReleaseUnavailableException(release, "Downloading torrent failed", ex);
}
if ((int)ex.Response.StatusCode == 429)
{
_logger.Error("API Grab Limit reached for {0}", torrentUrl);
}
else
{
_logger.Error(ex, "Downloading torrent file for release '{0}' failed ({1})", release.Title, torrentUrl);
}
throw new ReleaseDownloadException(release, "Downloading torrent failed", ex);
}
catch (WebException ex)
{
_logger.Error(ex, "Downloading torrent file for release '{0}' failed ({1})", release.Title, torrentUrl);
throw new ReleaseDownloadException(release, "Downloading torrent failed", ex);
} }
var filename = string.Format("{0}.torrent", StringUtil.CleanFileName(release.Title)); var filename = string.Format("{0}.torrent", StringUtil.CleanFileName(release.Title));
+41 -5
View File
@@ -1,9 +1,9 @@
using System; using System.Net;
using System.Threading.Tasks;
using NLog; using NLog;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Exceptions;
using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers;
using NzbDrone.Core.Parser; using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
@@ -15,14 +15,17 @@ namespace NzbDrone.Core.Download
where TSettings : IProviderConfig, new() where TSettings : IProviderConfig, new()
{ {
protected readonly IHttpClient _httpClient; protected readonly IHttpClient _httpClient;
private readonly IValidateNzbs _nzbValidationService;
protected UsenetClientBase(IHttpClient httpClient, protected UsenetClientBase(IHttpClient httpClient,
IConfigService configService, IConfigService configService,
IDiskProvider diskProvider, IDiskProvider diskProvider,
IValidateNzbs nzbValidationService,
Logger logger) Logger logger)
: base(configService, diskProvider, logger) : base(configService, diskProvider, logger)
{ {
_httpClient = httpClient; _httpClient = httpClient;
_nzbValidationService = nzbValidationService;
} }
public override DownloadProtocol Protocol => DownloadProtocol.Usenet; public override DownloadProtocol Protocol => DownloadProtocol.Usenet;
@@ -30,9 +33,9 @@ namespace NzbDrone.Core.Download
protected abstract string AddFromNzbFile(ReleaseInfo release, string filename, byte[] fileContents); protected abstract string AddFromNzbFile(ReleaseInfo release, string filename, byte[] fileContents);
protected abstract string AddFromLink(ReleaseInfo release); protected abstract string AddFromLink(ReleaseInfo release);
public override async Task<string> Download(ReleaseInfo release, bool redirect, IIndexer indexer) public override string Download(ReleaseInfo release, bool redirect)
{ {
var url = new Uri(release.DownloadUrl); var url = release.DownloadUrl;
if (redirect) if (redirect)
{ {
@@ -43,7 +46,40 @@ namespace NzbDrone.Core.Download
byte[] nzbData; byte[] nzbData;
nzbData = await indexer.Download(url); try
{
var request = new HttpRequest(url);
nzbData = _httpClient.Get(request).ResponseData;
_logger.Debug("Downloaded nzb for release '{0}' finished ({1} bytes from {2})", release.Title, nzbData.Length, url);
}
catch (HttpException ex)
{
if (ex.Response.StatusCode == HttpStatusCode.NotFound)
{
_logger.Error(ex, "Downloading nzb file for release '{0}' failed since it no longer exists ({1})", release.Title, url);
throw new ReleaseUnavailableException(release, "Downloading nzb failed", ex);
}
if ((int)ex.Response.StatusCode == 429)
{
_logger.Error("API Grab Limit reached for {0}", url);
}
else
{
_logger.Error(ex, "Downloading nzb for release '{0}' failed ({1})", release.Title, url);
}
throw new ReleaseDownloadException(release, "Downloading nzb failed", ex);
}
catch (WebException ex)
{
_logger.Error(ex, "Downloading nzb for release '{0}' failed ({1})", release.Title, url);
throw new ReleaseDownloadException(release, "Downloading nzb failed", ex);
}
_nzbValidationService.Validate(filename, nzbData);
_logger.Info("Adding report [{0}] to the queue.", release.Title); _logger.Info("Adding report [{0}] to the queue.", release.Title);
return AddFromNzbFile(release, filename, nzbData); return AddFromNzbFile(release, filename, nzbData);
@@ -1,33 +1,28 @@
using System; using System;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Exceptions namespace NzbDrone.Core.Exceptions
{ {
public class DownloadClientRejectedReleaseException : ReleaseDownloadException public class DownloadClientRejectedReleaseException : ReleaseDownloadException
{ {
public ReleaseInfo Release { get; set; }
public DownloadClientRejectedReleaseException(ReleaseInfo release, string message, params object[] args) public DownloadClientRejectedReleaseException(ReleaseInfo release, string message, params object[] args)
: base(message, args) : base(release, message, args)
{ {
Release = release;
} }
public DownloadClientRejectedReleaseException(ReleaseInfo release, string message) public DownloadClientRejectedReleaseException(ReleaseInfo release, string message)
: base(message) : base(release, message)
{ {
Release = release;
} }
public DownloadClientRejectedReleaseException(ReleaseInfo release, string message, Exception innerException, params object[] args) public DownloadClientRejectedReleaseException(ReleaseInfo release, string message, Exception innerException, params object[] args)
: base(message, innerException, args) : base(release, message, innerException, args)
{ {
Release = release;
} }
public DownloadClientRejectedReleaseException(ReleaseInfo release, string message, Exception innerException) public DownloadClientRejectedReleaseException(ReleaseInfo release, string message, Exception innerException)
: base(message, innerException) : base(release, message, innerException)
{ {
Release = release;
} }
} }
} }
@@ -1,28 +1,35 @@
using System; using System;
using NzbDrone.Common.Exceptions; using NzbDrone.Common.Exceptions;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Exceptions namespace NzbDrone.Core.Exceptions
{ {
public class ReleaseDownloadException : NzbDroneException public class ReleaseDownloadException : NzbDroneException
{ {
public ReleaseDownloadException(string message, params object[] args) public ReleaseInfo Release { get; set; }
public ReleaseDownloadException(ReleaseInfo release, string message, params object[] args)
: base(message, args) : base(message, args)
{ {
Release = release;
} }
public ReleaseDownloadException(string message) public ReleaseDownloadException(ReleaseInfo release, string message)
: base(message) : base(message)
{ {
Release = release;
} }
public ReleaseDownloadException(string message, Exception innerException, params object[] args) public ReleaseDownloadException(ReleaseInfo release, string message, Exception innerException, params object[] args)
: base(message, innerException, args) : base(message, innerException, args)
{ {
Release = release;
} }
public ReleaseDownloadException(string message, Exception innerException) public ReleaseDownloadException(ReleaseInfo release, string message, Exception innerException)
: base(message, innerException) : base(message, innerException)
{ {
Release = release;
} }
} }
} }
@@ -1,26 +1,27 @@
using System; using System;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Exceptions namespace NzbDrone.Core.Exceptions
{ {
public class ReleaseUnavailableException : ReleaseDownloadException public class ReleaseUnavailableException : ReleaseDownloadException
{ {
public ReleaseUnavailableException(string message, params object[] args) public ReleaseUnavailableException(ReleaseInfo release, string message, params object[] args)
: base(message, args) : base(release, message, args)
{ {
} }
public ReleaseUnavailableException(string message) public ReleaseUnavailableException(ReleaseInfo release, string message)
: base(message) : base(release, message)
{ {
} }
public ReleaseUnavailableException(string message, Exception innerException, params object[] args) public ReleaseUnavailableException(ReleaseInfo release, string message, Exception innerException, params object[] args)
: base(message, innerException, args) : base(release, message, innerException, args)
{ {
} }
public ReleaseUnavailableException(string message, Exception innerException) public ReleaseUnavailableException(ReleaseInfo release, string message, Exception innerException)
: base(message, innerException) : base(release, message, innerException)
{ {
} }
} }
@@ -44,14 +44,14 @@ namespace NzbDrone.Core.HealthCheck.Checks
return new HealthCheck(GetType(), return new HealthCheck(GetType(),
HealthCheckResult.Error, HealthCheckResult.Error,
_localizationService.GetLocalizedString("ApplicationStatusCheckAllClientMessage"), _localizationService.GetLocalizedString("ApplicationStatusCheckAllClientMessage"),
"#applications-are-unavailable-due-to-failures"); "#applications_are_unavailable_due_to_failures");
} }
return new HealthCheck(GetType(), return new HealthCheck(GetType(),
HealthCheckResult.Warning, HealthCheckResult.Warning,
string.Format(_localizationService.GetLocalizedString("ApplicationStatusCheckSingleClientMessage"), string.Format(_localizationService.GetLocalizedString("ApplicationStatusCheckSingleClientMessage"),
string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name))), string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name))),
"#applications-are-unavailable-due-to-failures"); "#applications_are_unavailable_due_to_failures");
} }
} }
} }
@@ -37,10 +37,10 @@ namespace NzbDrone.Core.HealthCheck.Checks
if (backOffProviders.Count == enabledProviders.Count) if (backOffProviders.Count == enabledProviders.Count)
{ {
return new HealthCheck(GetType(), HealthCheckResult.Error, _localizationService.GetLocalizedString("DownloadClientStatusCheckAllClientMessage"), "#download-clients-are-unavailable-due-to-failures"); return new HealthCheck(GetType(), HealthCheckResult.Error, _localizationService.GetLocalizedString("DownloadClientStatusCheckAllClientMessage"), "#download_clients_are_unavailable_due_to_failures");
} }
return new HealthCheck(GetType(), HealthCheckResult.Warning, string.Format(_localizationService.GetLocalizedString("DownloadClientStatusCheckSingleClientMessage"), string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name))), "#download-clients-are-unavailable-due-to-failures"); return new HealthCheck(GetType(), HealthCheckResult.Warning, string.Format(_localizationService.GetLocalizedString("DownloadClientStatusCheckSingleClientMessage"), string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name))), "#download_clients_are_unavailable_due_to_failures");
} }
} }
} }
@@ -46,14 +46,14 @@ namespace NzbDrone.Core.HealthCheck.Checks
return new HealthCheck(GetType(), return new HealthCheck(GetType(),
HealthCheckResult.Error, HealthCheckResult.Error,
_localizationService.GetLocalizedString("IndexerLongTermStatusCheckAllClientMessage"), _localizationService.GetLocalizedString("IndexerLongTermStatusCheckAllClientMessage"),
"#indexers-are-unavailable-due-to-failures"); "#indexers_are_unavailable_due_to_failures");
} }
return new HealthCheck(GetType(), return new HealthCheck(GetType(),
HealthCheckResult.Warning, HealthCheckResult.Warning,
string.Format(_localizationService.GetLocalizedString("IndexerLongTermStatusCheckSingleClientMessage"), string.Format(_localizationService.GetLocalizedString("IndexerLongTermStatusCheckSingleClientMessage"),
string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name))), string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name))),
"#indexers-are-unavailable-due-to-failures"); "#indexers_are_unavailable_due_to_failures");
} }
} }
} }
@@ -44,14 +44,14 @@ namespace NzbDrone.Core.HealthCheck.Checks
return new HealthCheck(GetType(), return new HealthCheck(GetType(),
HealthCheckResult.Error, HealthCheckResult.Error,
_localizationService.GetLocalizedString("IndexerStatusCheckAllClientMessage"), _localizationService.GetLocalizedString("IndexerStatusCheckAllClientMessage"),
"#indexers-are-unavailable-due-to-failures"); "#indexers_are_unavailable_due_to_failures");
} }
return new HealthCheck(GetType(), return new HealthCheck(GetType(),
HealthCheckResult.Warning, HealthCheckResult.Warning,
string.Format(_localizationService.GetLocalizedString("IndexerStatusCheckSingleClientMessage"), string.Format(_localizationService.GetLocalizedString("IndexerStatusCheckSingleClientMessage"),
string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name))), string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name))),
"#indexers-are-unavailable-due-to-failures"); "#indexers_are_unavailable_due_to_failures");
} }
} }
} }

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