Compare commits

...

74 Commits

Author SHA1 Message Date
Bogdan
f20319fff1 Bump version to 1.6.3 2023-06-25 19:15:06 +03:00
Bogdan
20bcc00662 Fix apprise server url migration 2023-06-25 08:38:39 +03:00
Bogdan
c4af3e746f Add more trace logs related info to bug_report.yml [skip ci]
Co-authored-by: Bakerboy448 <55419169+bakerboy448@users.noreply.github.com>
2023-06-24 08:38:50 +03:00
Bogdan
660a162b7e Fixed: (Cardigann) Throw exception only when all download selectors fail 2023-06-23 11:18:14 +03:00
Bogdan
20a3cad7fb Add indexer id in logs for invalid dates in Cardigann definitions 2023-06-23 10:41:25 +03:00
Bogdan
77fe3f78fe Fixed: (Cardigann) Skip to next download selector when max redirects reached
Fixes #578
2023-06-22 17:01:25 +03:00
Bogdan
d777cb8e29 Fixed: (API) Prevent NullRef when searching empty query with a non-default type 2023-06-22 10:36:53 +03:00
Bogdan
15e7cc7ea8 New: (UI) Show indexer categories in info modal 2023-06-20 13:23:06 +03:00
Shivam Dua
04cf061275 Fixed: (UI) Add New Indexer button on search page when no indexers are present
Add missing listeners and components to make add indexer button work on
search page when no indexers are present
2023-06-20 07:33:34 +03:00
Bogdan
d4cdeac69a Fixed: (Cardigann) Definitions with category mapping Other to use 8000 (Other) 2023-06-20 07:21:35 +03:00
Bogdan
e60fe05ee0 Revert "Fix typo botton to bottom"
This reverts commit e2e65627ee.
2023-06-20 05:20:26 +03:00
Bogdan
9a4c23797a Display error when search failed due to all indexers being disabled 2023-06-20 03:05:55 +03:00
Bogdan
acfdb5bae3 New: (UI) Show disabled indexers as disabled options in search page 2023-06-20 03:05:55 +03:00
Bogdan
e2e65627ee Fix typo botton to bottom 2023-06-19 14:31:37 +03:00
Bogdan
4b8906ea62 Cleanup redundant DownloadProtocol in indexers 2023-06-19 04:26:45 +03:00
Bogdan
f0c5d8ceea Minor refactoring in Cardigann definition 2023-06-19 04:08:01 +03:00
Bogdan
427802a50e Update status translations for Indexer index 2023-06-18 15:46:43 +03:00
Bogdan
0c9eae244a Add skip ci to API docs update commit 2023-06-18 15:45:04 +03:00
Bogdan
75ff2f41d3 Update description for freeleech only in BakaBT 2023-06-18 09:37:33 +03:00
Bakerboy448
d1ba208243 Fixed: (HttpIndexerBase) Better HTTP error handling 2023-06-18 08:15:23 +03:00
Bogdan
4e03ebadc4 New: (UI) Add filter by categories in add indexer modal
Fixes #872
Closes #1731
2023-06-18 08:14:39 +03:00
Bogdan
0155ff60fd Map Cardigann capabilities from meta definition 2023-06-18 08:14:35 +03:00
Bogdan
f0915638f3 New: (Apps) Sync Anime Standard Search with Sonarr
Fixes #998
Closes #1732
2023-06-18 07:05:08 +03:00
Bogdan
56eb58aed1 Bump version to 1.6.2 2023-06-18 07:01:38 +03:00
Bogdan
8a891d07cf Test eligibility of the first request in AvistazBase 2023-06-17 14:22:57 +03:00
Bogdan
40a932cd28 Improved page loading errors 2023-06-17 03:36:40 +03:00
Mark McDowall
4a81630073 Fixed: Clearing logs not updating UI once complete
(cherry picked from commit 56b3acddc9f50f59c78c03ca072fe802752b88a7)
2023-06-17 02:27:45 +03:00
Bogdan
0ff0fe2e68 Prevent NullRef when deleting missing backups 2023-06-17 02:08:40 +03:00
Bogdan
51e33740b0 Update import path in CategoryLabel 2023-06-17 01:08:06 +03:00
Bogdan
119164f729 Show indexer privacy in search results 2023-06-16 05:23:07 +03:00
Bogdan
ef0f8e25fd Sort limits in IndexerCapabilities 2023-06-16 05:23:07 +03:00
Bogdan
d21debe77f Convert to 'using' declaration in Housekeeping Tasks 2023-06-16 02:40:47 +03:00
Bogdan
a3ccc3d0cf Close database connections in housekeeping tasks 2023-06-16 02:40:39 +03:00
Bogdan
46d930e903 Apply template text to switch cases in Cardigann 2023-06-16 00:06:11 +03:00
Bogdan
4561859c2b Fixed: (UI) Case-insensitive sorting for add indexer modal 2023-06-14 10:03:02 +03:00
Bogdan
83166fb0b5 Allow array of string as value in EnhancedSelectInput 2023-06-14 07:11:50 +03:00
Bogdan
b98f9a945d Fix use of TmdbId in NewznabRequestGenerator 2023-06-14 04:11:26 +03:00
Bogdan
e658e3fe48 Fixed: (Cardigann) Skip duplicated GET requests 2023-06-12 03:58:02 +03:00
Bogdan
9042525f22 Bump version to 1.6.1 2023-06-11 09:33:46 +03:00
Bogdan
7b551a0af1 Update Anidub description 2023-06-11 07:42:41 +03:00
Bogdan
31c2917bad Fixed: (Indexers) Allow RSS searches in HttpIndexerBase 2023-06-11 03:25:00 +03:00
Bogdan
419cce53f7 Fixed: (Transmission) Set seed limits in client 2023-06-11 03:02:11 +03:00
Bogdan
48cd1d9f6b Reset ContentSummary on redirect in HttpClient 2023-06-11 01:54:13 +03:00
Bogdan
8bd6a313b7 Fixed: (FreeboxDownload) Set seed limits in client 2023-06-10 23:32:06 +03:00
Bogdan
7cb465787e Use more specific styling for kinds in ProgressBar
(cherry picked from commit dd31c913d2a974d95f3be251714ce749cfd99a72)
2023-06-10 02:10:25 +03:00
Bogdan
0b610ff9c8 Cleanse messages for TL 24h RSS feed links 2023-06-09 17:29:49 +03:00
Servarr
5187460298 Automated API Docs update 2023-06-09 16:49:58 +03:00
Bogdan
f0d9b43480 Add some API attributes 2023-06-09 04:12:57 +03:00
Bogdan
a1081cc554 Bump NLog to 5.2.0 2023-06-09 04:12:20 +03:00
Bogdan
c4bb1ba69a Catch JsonReaderException when parsing JSON in Cardigann 2023-06-09 01:37:07 +03:00
Bogdan
3a4c8db98c Add all search types in TorrentRssIndexer
For apps who don't support all categories with normal search, eg. Sonarr
2023-06-08 17:54:16 +03:00
Bogdan
a522796798 Fixed: (Apps) Change the default sync level to Full Sync 2023-06-08 17:47:13 +03:00
Bogdan
e012eda0cf Use the default IndexerCapabilities for TorrentRssIndexer 2023-06-08 00:23:58 +03:00
Bogdan
72ab2b34c4 Cleanse /Users for Mac users 2023-06-07 08:20:14 +03:00
Bogdan
aaba5b7499 Add help link to finding cookies guide 2023-06-07 05:55:13 +03:00
Bogdan
455b76c45c New: Add TorrentRssIndexer
Co-authored-by: ilike2burnthing <59480337+ilike2burnthing@users.noreply.github.com>
2023-06-07 03:57:37 +03:00
Bogdan
596d3297da Move seed configuration logic to TorrentClientBase 2023-06-07 01:32:03 +03:00
Bogdan
d05128ca33 Map seed configuration on release only when it's not null
Fixes #1720
2023-06-06 23:12:52 +03:00
Bogdan
f5b57db753 Check if release still exists in cache when grabbing release 2023-06-06 23:00:12 +03:00
Bogdan
f7d7cca982 Add extension only for known protocols in ReleaseResource 2023-06-06 21:52:44 +03:00
Bogdan
7c5409383e Fixed: (NzbIndex) Use UsenetIndexerBase 2023-06-06 20:10:01 +03:00
Bogdan
98db8f8bf8 Add default definitions for download clients 2023-06-06 19:56:35 +03:00
Bogdan
88e793d76d Fixed: (Cardigann) Allow empty inputs for login.method form/post 2023-06-06 05:59:17 +03:00
Bogdan
0f31af6b89 Fixed: (Cardigann) Allow empty inputs for login.method get 2023-06-06 01:17:27 +03:00
Bogdan
65adf30f59 Fixed: (UTorrent) Set seed limits in client 2023-06-05 17:36:20 +03:00
Bogdan
da75519524 Fixed: (Deluge) Set seed limits in client 2023-06-05 17:22:30 +03:00
Bogdan
ed1fb58242 Align QBittorrent with upstream 2023-06-05 16:41:06 +03:00
Bogdan
d5daf6791c New: Support for seed configuration in DownloadService 2023-06-05 16:41:06 +03:00
Weblate
1f1a345d25 Translations update from Servarr Weblate
Co-authored-by: MoowGlax <matthieu.derouet.pro@gmail.com>
Co-authored-by: Thodoris Kalatzis <teo.kal@hotmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: emacsdias <emacs.dias@gmail.com>
Co-authored-by: reloxx <reloxx@interia.pl>
Co-authored-by: splifter <a.strahlke@gmail.com>
Co-authored-by: victor22265 <843427709@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2023-06-05 16:37:55 +03:00
Bogdan
76a2f51533 Fixed: (HDTorrents) Add login error message 2023-06-05 02:22:49 +03:00
Bogdan
8c0bc9ab4e Filter enabled indexer proxies in Active 2023-06-05 02:22:05 +03:00
Servarr
b0c2b9119b Automated API Docs update 2023-06-05 00:02:28 +03:00
Bogdan
87fdf17926 Add HelpTextWarning support in FieldDefinition 2023-06-04 23:46:02 +03:00
Qstick
0f1b466a19 Bump version to 1.6.0 2023-06-04 15:43:52 -05:00
221 changed files with 2988 additions and 2620 deletions

View File

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

View File

@@ -9,7 +9,7 @@ variables:
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '1.5.2'
majorVersion: '1.6.3'
minorVersion: $[counter('minorVersion', 1)]
prowlarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
@@ -1003,7 +1003,7 @@ stages:
git add .
if git status | grep -q modified
then
git commit -am 'Automated API Docs update'
git commit -am 'Automated API Docs update [skip ci]'
git push -f --set-upstream origin api-docs
curl -X POST -H "Authorization: token ${GITHUBTOKEN}" -H "Accept: application/vnd.github.v3+json" https://api.github.com/repos/prowlarr/prowlarr/pulls -d '{"head":"api-docs","base":"develop","title":"Update API docs"}'
else

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React from 'react';
import Alert from 'Components/Alert';
import BarChart from 'Components/Chart/BarChart';
import DoughnutChart from 'Components/Chart/DoughnutChart';
import StackedBarChart from 'Components/Chart/StackedBarChart';
@@ -178,9 +179,9 @@ function Stats(props) {
{
!isFetching && !!error &&
<div className={styles.errorMessage}>
<Alert kind={kinds.DANGER}>
{getErrorMessage(error, 'Failed to load indexer stats from API')}
</div>
</Alert>
}
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -41,7 +41,7 @@ export const defaultState = {
isFetching: false,
isPopulated: false,
error: null,
sortKey: 'name',
sortKey: 'sortName',
sortDirection: sortDirections.ASCENDING,
items: []
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -109,7 +109,7 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
<PackageReference Include="NunitXml.TestLogger" Version="3.0.117" />
<PackageReference Include="NunitXml.TestLogger" Version="3.0.131" />
<PackageReference Include="coverlet.collector" Version="3.0.4-preview.27.ge7cb7c3b40" />
</ItemGroup>

View File

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

View File

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

View File

@@ -63,6 +63,8 @@ namespace NzbDrone.Common.Http
public bool HasHttpError => (int)StatusCode >= 400;
public bool HasHttpServerError => (int)StatusCode >= 500;
public bool HasHttpRedirect => StatusCode == HttpStatusCode.Moved ||
StatusCode == HttpStatusCode.Found ||
StatusCode == HttpStatusCode.SeeOther ||

View File

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

View File

@@ -8,8 +8,8 @@
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NLog" Version="5.1.0" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.2.0" />
<PackageReference Include="NLog" Version="5.2.0" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.3.0" />
<PackageReference Include="Npgsql" Version="5.0.11" />
<PackageReference Include="Sentry" Version="3.29.1" />
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />

View File

@@ -0,0 +1,62 @@
using System.Collections.Generic;
using System.Linq;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Datastore.Migration;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.Datastore.Migration
{
[TestFixture]
public class apprise_server_urlFixture : MigrationTest<apprise_server_url>
{
[Test]
public void should_rename_server_url_setting_for_apprise()
{
var db = WithMigrationTestDb(c =>
{
c.Insert.IntoTable("Notifications").Row(new
{
Name = "Apprise",
Implementation = "Apprise",
Settings = new
{
BaseUrl = "http://localhost:8000",
NotificationType = 0
}.ToJson(),
ConfigContract = "AppriseSettings",
OnHealthIssue = true,
IncludeHealthWarnings = true,
OnApplicationUpdate = true,
OnGrab = true,
IncludeManualGrabs = true
});
});
var items = db.Query<NotificationDefinition31>("SELECT * FROM \"Notifications\"");
items.Should().HaveCount(1);
items.First().Settings.Should().NotContainKey("baseUrl");
items.First().Settings.Should().ContainKey("serverUrl");
items.First().Settings.GetValueOrDefault("serverUrl").Should().Be("http://localhost:8000");
}
}
public class NotificationDefinition31
{
public int Id { get; set; }
public int Priority { get; set; }
public string Name { get; set; }
public string Implementation { get; set; }
public Dictionary<string, string> Settings { get; set; }
public string ConfigContract { get; set; }
public bool OnHealthIssue { get; set; }
public bool IncludeHealthWarnings { get; set; }
public bool OnApplicationUpdate { get; set; }
public bool OnGrab { get; set; }
public bool IncludeManualGrabs { get; set; }
public List<int> Tags { get; set; }
}
}

View File

@@ -15,6 +15,7 @@ namespace NzbDrone.Core.Annotations
public string Label { get; set; }
public string Unit { get; set; }
public string HelpText { get; set; }
public string HelpTextWarning { get; set; }
public string HelpLink { get; set; }
public FieldType Type { get; set; }
public bool Advanced { get; set; }

View File

@@ -47,7 +47,7 @@ namespace NzbDrone.Core.Applications
yield return new ApplicationDefinition
{
Name = GetType().Name,
SyncLevel = ApplicationSyncLevel.AddOnly,
SyncLevel = ApplicationSyncLevel.FullSync,
Implementation = GetType().Name,
Settings = config
};

View File

@@ -6,6 +6,6 @@ namespace NzbDrone.Core.Applications
{
public ApplicationSyncLevel SyncLevel { get; set; }
public override bool Enable => SyncLevel == ApplicationSyncLevel.AddOnly || SyncLevel == ApplicationSyncLevel.FullSync;
public override bool Enable => SyncLevel is ApplicationSyncLevel.AddOnly or ApplicationSyncLevel.FullSync;
}
}

View File

@@ -183,12 +183,12 @@ namespace NzbDrone.Core.Applications.Sonarr
{
var cacheKey = $"{Settings.BaseUrl}";
var schemas = _schemaCache.Get(cacheKey, () => _sonarrV3Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7));
var syncFields = new List<string> { "baseUrl", "apiPath", "apiKey", "categories", "animeCategories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime", "seedCriteria.seasonPackSeedTime" };
var syncFields = new List<string> { "baseUrl", "apiPath", "apiKey", "categories", "animeCategories", "animeStandardFormatSearch", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime", "seedCriteria.seasonPackSeedTime" };
if (id == 0)
{
// Ensuring backward compatibility with older versions on first sync
syncFields.AddRange(new List<string> { "animeStandardFormatSearch", "additionalParameters" });
syncFields.AddRange(new List<string> { "additionalParameters" });
}
var newznab = schemas.First(i => i.Implementation == "Newznab");
@@ -218,6 +218,11 @@ namespace NzbDrone.Core.Applications.Sonarr
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "categories").Value = JArray.FromObject(indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()));
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "animeCategories").Value = JArray.FromObject(indexer.Capabilities.Categories.SupportedCategories(Settings.AnimeSyncCategories.ToArray()));
if (sonarrIndexer.Fields.Any(x => x.Name == "animeStandardFormatSearch"))
{
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "animeStandardFormatSearch").Value = Settings.SyncAnimeStandardFormatSearch;
}
if (indexer.Protocol == DownloadProtocol.Torrent)
{
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.AppMinimumSeeders ?? indexer.AppProfile.Value.MinimumSeeders;

View File

@@ -38,6 +38,10 @@ namespace NzbDrone.Core.Applications.Sonarr
var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var apiPathCompare = apiPath.Equals(otherApiPath);
var animeStandardFormatSearch = Fields.FirstOrDefault(x => x.Name == "animeStandardFormatSearch")?.Value == null ? null : (bool?)Convert.ToBoolean(Fields.FirstOrDefault(x => x.Name == "animeStandardFormatSearch").Value);
var otherAnimeStandardFormatSearch = other.Fields.FirstOrDefault(x => x.Name == "animeStandardFormatSearch")?.Value == null ? null : (bool?)Convert.ToBoolean(other.Fields.FirstOrDefault(x => x.Name == "animeStandardFormatSearch").Value);
var animeStandardFormatSearchCompare = animeStandardFormatSearch == otherAnimeStandardFormatSearch;
var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
var minimumSeedersCompare = minimumSeeders == otherMinimumSeeders;
@@ -61,7 +65,7 @@ namespace NzbDrone.Core.Applications.Sonarr
other.Implementation == Implementation &&
other.Priority == Priority &&
other.Id == Id &&
apiKey && apiPathCompare && baseUrl && cats && animeCats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare && seasonSeedTimeCompare;
apiKey && apiPathCompare && baseUrl && cats && animeCats && animeStandardFormatSearchCompare && minimumSeedersCompare && seedRatioCompare && seedTimeCompare && seasonSeedTimeCompare;
}
}
}

View File

@@ -43,6 +43,9 @@ namespace NzbDrone.Core.Applications.Sonarr
[FieldDefinition(4, Label = "Anime Sync Categories", Type = FieldType.Select, SelectOptions = typeof(NewznabCategoryFieldConverter), Advanced = true, HelpText = "Only Indexers that support these categories will be synced")]
public IEnumerable<int> AnimeSyncCategories { get; set; }
[FieldDefinition(5, Label = "Sync Anime Standard Format Search", Type = FieldType.Checkbox, Advanced = true, HelpText = "Sync also searching for anime using the standard numbering")]
public bool SyncAnimeStandardFormatSearch { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Data;
using Dapper;
using FluentMigrator;
@@ -17,33 +18,43 @@ namespace NzbDrone.Core.Datastore.Migration
private void MigrateToServerUrl(IDbConnection conn, IDbTransaction tran)
{
using var selectCommand = conn.CreateCommand();
selectCommand.Transaction = tran;
selectCommand.CommandText = "SELECT \"Id\", \"Settings\" FROM \"Notifications\" WHERE \"Implementation\" = 'Apprise'";
var updatedNotifications = new List<object>();
using var reader = selectCommand.ExecuteReader();
while (reader.Read())
using (var selectCommand = conn.CreateCommand())
{
var id = reader.GetInt32(0);
var settings = reader.GetString(1);
selectCommand.Transaction = tran;
selectCommand.CommandText = "SELECT \"Id\", \"Settings\" FROM \"Notifications\" WHERE \"Implementation\" = 'Apprise'";
if (!string.IsNullOrWhiteSpace(settings))
using var reader = selectCommand.ExecuteReader();
while (reader.Read())
{
var jsonObject = Json.Deserialize<JObject>(settings);
var id = reader.GetInt32(0);
var settings = reader.GetString(1);
if (jsonObject.ContainsKey("baseUrl"))
if (!string.IsNullOrWhiteSpace(settings))
{
jsonObject.Add("serverUrl", jsonObject.Value<string>("baseUrl"));
jsonObject.Remove("baseUrl");
var jsonObject = Json.Deserialize<JObject>(settings);
if (jsonObject.ContainsKey("baseUrl"))
{
jsonObject.Add("serverUrl", jsonObject.Value<string>("baseUrl"));
jsonObject.Remove("baseUrl");
}
settings = jsonObject.ToJson();
}
settings = jsonObject.ToJson();
updatedNotifications.Add(new
{
Id = id,
Settings = settings
});
}
var parameters = new { Settings = settings, Id = id };
conn.Execute("UPDATE Notifications SET Settings = @Settings WHERE Id = @Id", parameters, transaction: tran);
}
var updateNotificationsSql = "UPDATE \"Notifications\" SET \"Settings\" = @Settings WHERE \"Id\" = @Id";
conn.Execute(updateNotificationsSql, updatedNotifications, transaction: tran);
}
}
}

View File

@@ -5,8 +5,8 @@ using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Validation;
@@ -22,16 +22,16 @@ namespace NzbDrone.Core.Download.Clients.Aria2
public Aria2(IAria2Proxy proxy,
ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient,
ISeedConfigProvider seedConfigProvider,
IConfigService configService,
IDiskProvider diskProvider,
Logger logger)
: base(torrentFileInfoReader, httpClient, configService, diskProvider, logger)
: base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, logger)
{
_proxy = proxy;
}
protected override string AddFromMagnetLink(ReleaseInfo release, string hash, string magnetLink)
protected override string AddFromMagnetLink(TorrentInfo release, string hash, string magnetLink)
{
var gid = _proxy.AddUri(Settings, magnetLink);
@@ -50,7 +50,7 @@ namespace NzbDrone.Core.Download.Clients.Aria2
return hash;
}
protected override string AddFromTorrentFile(ReleaseInfo release, string hash, string filename, byte[] fileContent)
protected override string AddFromTorrentFile(TorrentInfo release, string hash, string filename, byte[] fileContent)
{
var gid = _proxy.AddTorrent(Settings, fileContent);
@@ -120,7 +120,7 @@ namespace NzbDrone.Core.Download.Clients.Aria2
return null;
}
protected override string AddFromTorrentLink(ReleaseInfo release, string hash, string torrentLink)
protected override string AddFromTorrentLink(TorrentInfo release, string hash, string torrentLink)
{
var gid = _proxy.AddUri(Settings, torrentLink);

View File

@@ -6,8 +6,8 @@ using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Download.Clients.Blackhole
@@ -17,20 +17,20 @@ namespace NzbDrone.Core.Download.Clients.Blackhole
public override bool PreferTorrentFile => true;
public TorrentBlackhole(ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient,
ISeedConfigProvider seedConfigProvider,
IConfigService configService,
IDiskProvider diskProvider,
Logger logger)
: base(torrentFileInfoReader, httpClient, configService, diskProvider, logger)
: base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, logger)
{
}
protected override string AddFromTorrentLink(ReleaseInfo release, string hash, string torrentLink)
protected override string AddFromTorrentLink(TorrentInfo release, string hash, string torrentLink)
{
throw new NotImplementedException("Blackhole does not support redirected indexers.");
}
protected override string AddFromMagnetLink(ReleaseInfo release, string hash, string magnetLink)
protected override string AddFromMagnetLink(TorrentInfo release, string hash, string magnetLink)
{
if (!Settings.SaveMagnetFiles)
{
@@ -54,7 +54,7 @@ namespace NzbDrone.Core.Download.Clients.Blackhole
return null;
}
protected override string AddFromTorrentFile(ReleaseInfo release, string hash, string filename, byte[] fileContent)
protected override string AddFromTorrentFile(TorrentInfo release, string hash, string filename, byte[] fileContent)
{
var title = release.Title;

View File

@@ -7,8 +7,8 @@ using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Validation;
@@ -20,16 +20,16 @@ namespace NzbDrone.Core.Download.Clients.Deluge
public Deluge(IDelugeProxy proxy,
ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient,
ISeedConfigProvider seedConfigProvider,
IConfigService configService,
IDiskProvider diskProvider,
Logger logger)
: base(torrentFileInfoReader, httpClient, configService, diskProvider, logger)
: base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, logger)
{
_proxy = proxy;
}
protected override string AddFromMagnetLink(ReleaseInfo release, string hash, string magnetLink)
protected override string AddFromMagnetLink(TorrentInfo release, string hash, string magnetLink)
{
var actualHash = _proxy.AddTorrentFromMagnet(magnetLink, Settings);
@@ -38,8 +38,10 @@ namespace NzbDrone.Core.Download.Clients.Deluge
throw new DownloadClientException("Deluge failed to add magnet " + magnetLink);
}
// _proxy.SetTorrentSeedingConfiguration(actualHash, remoteMovie.SeedConfiguration, Settings);
_proxy.SetTorrentSeedingConfiguration(actualHash, release.SeedConfiguration, Settings);
var category = GetCategoryForRelease(release) ?? Settings.Category;
if (category.IsNotNullOrWhiteSpace())
{
_proxy.SetTorrentLabel(actualHash, category, Settings);
@@ -53,7 +55,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge
return actualHash.ToUpper();
}
protected override string AddFromTorrentFile(ReleaseInfo release, string hash, string filename, byte[] fileContent)
protected override string AddFromTorrentFile(TorrentInfo release, string hash, string filename, byte[] fileContent)
{
var actualHash = _proxy.AddTorrentFromFile(filename, fileContent, Settings);
@@ -62,8 +64,10 @@ namespace NzbDrone.Core.Download.Clients.Deluge
throw new DownloadClientException("Deluge failed to add torrent " + filename);
}
// _proxy.SetTorrentSeedingConfiguration(actualHash, release.SeedConfiguration, Settings);
_proxy.SetTorrentSeedingConfiguration(actualHash, release.SeedConfiguration, Settings);
var category = GetCategoryForRelease(release) ?? Settings.Category;
if (category.IsNotNullOrWhiteSpace())
{
_proxy.SetTorrentLabel(actualHash, category, Settings);
@@ -117,12 +121,12 @@ namespace NzbDrone.Core.Download.Clients.Deluge
case WebExceptionStatus.ConnectionClosed:
return new NzbDroneValidationFailure("UseSsl", "Verify SSL settings")
{
DetailedDescription = "Please verify your SSL configuration on both Deluge and NzbDrone."
DetailedDescription = "Please verify your SSL configuration on both Deluge and Prowlarr."
};
case WebExceptionStatus.SecureChannelFailure:
return new NzbDroneValidationFailure("UseSsl", "Unable to connect through SSL")
{
DetailedDescription = "Drone is unable to connect to Deluge using SSL. This problem could be computer related. Please try to configure both drone and Deluge to not use SSL."
DetailedDescription = "Prowlarr is unable to connect to Deluge using SSL. This problem could be computer related. Please try to configure both Prowlarr and Deluge to not use SSL."
};
default:
return new NzbDroneValidationFailure(string.Empty, "Unknown exception: " + ex.Message);
@@ -211,7 +215,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge
return null;
}
protected override string AddFromTorrentLink(ReleaseInfo release, string hash, string torrentLink)
protected override string AddFromTorrentLink(TorrentInfo release, string hash, string torrentLink)
{
throw new NotImplementedException();
}

View File

@@ -1,4 +1,4 @@
namespace NzbDrone.Core.Download.Clients.Deluge
namespace NzbDrone.Core.Download.Clients.Deluge
{
public class DelugeException : DownloadClientException
{

View File

@@ -82,7 +82,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge
var filter = new Dictionary<string, object>();
// TODO: get_torrents_status returns the files as well, which starts to cause deluge timeouts when you get enough season packs.
//var response = ProcessRequest<Dictionary<String, DelugeTorrent>>(settings, "core.get_torrents_status", filter, new String[0]);
// var response = ProcessRequest<Dictionary<String, DelugeTorrent>>(settings, "core.get_torrents_status", filter, new String[0]);
var response = ProcessRequest<DelugeUpdateUIResult>(settings, "web.update_ui", RequiredProperties, filter);
return GetTorrents(response);
@@ -93,7 +93,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge
var filter = new Dictionary<string, object>();
filter.Add("label", label);
//var response = ProcessRequest<Dictionary<String, DelugeTorrent>>(settings, "core.get_torrents_status", filter, new String[0]);
// var response = ProcessRequest<Dictionary<String, DelugeTorrent>>(settings, "core.get_torrents_status", filter, new String[0]);
var response = ProcessRequest<DelugeUpdateUIResult>(settings, "web.update_ui", RequiredProperties, filter);
return GetTorrents(response);

View File

@@ -1,6 +1,6 @@
namespace NzbDrone.Core.Download.Clients.Deluge
namespace NzbDrone.Core.Download.Clients.Deluge
{
internal class DelugeTorrentStatus
public class DelugeTorrentStatus
{
public const string Paused = "Paused";
public const string Queued = "Queued";

View File

@@ -7,9 +7,9 @@ using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Download.Clients.DownloadStation.Proxies;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
@@ -30,11 +30,11 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
IDownloadStationInfoProxy dsInfoProxy,
IDownloadStationTaskProxy dsTaskProxy,
ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient,
ISeedConfigProvider seedConfigProvider,
IConfigService configService,
IDiskProvider diskProvider,
Logger logger)
: base(torrentFileInfoReader, httpClient, configService, diskProvider, logger)
: base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, logger)
{
_dsInfoProxy = dsInfoProxy;
_dsTaskProxy = dsTaskProxy;
@@ -53,7 +53,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
return _dsTaskProxy.GetTasks(Settings).Where(v => v.Type.ToLower() == DownloadStationTaskType.BT.ToString().ToLower());
}
protected override string AddFromMagnetLink(ReleaseInfo release, string hash, string magnetLink)
protected override string AddFromMagnetLink(TorrentInfo release, string hash, string magnetLink)
{
var hashedSerialNumber = _serialNumberProvider.GetSerialNumber(Settings);
@@ -72,7 +72,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
throw new DownloadClientException("Failed to add magnet task to Download Station");
}
protected override string AddFromTorrentFile(ReleaseInfo release, string hash, string filename, byte[] fileContent)
protected override string AddFromTorrentFile(TorrentInfo release, string hash, string filename, byte[] fileContent)
{
var hashedSerialNumber = _serialNumberProvider.GetSerialNumber(Settings);
@@ -315,7 +315,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
return null;
}
protected override string AddFromTorrentLink(ReleaseInfo release, string hash, string torrentLink)
protected override string AddFromTorrentLink(TorrentInfo release, string hash, string torrentLink)
{
throw new NotImplementedException();
}

View File

@@ -4,9 +4,9 @@ using System.Linq;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Download.Clients.Flood.Models;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.ThingiProvider;
@@ -18,11 +18,11 @@ namespace NzbDrone.Core.Download.Clients.Flood
public Flood(IFloodProxy proxy,
ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient,
ISeedConfigProvider seedConfigProvider,
IConfigService configService,
IDiskProvider diskProvider,
Logger logger)
: base(torrentFileInfoReader, httpClient, configService, diskProvider, logger)
: base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, logger)
{
_proxy = proxy;
}
@@ -63,14 +63,14 @@ namespace NzbDrone.Core.Download.Clients.Flood
public override bool SupportsCategories => true;
public override ProviderMessage Message => new ProviderMessage("Prowlarr is unable to remove torrents that have finished seeding when using Flood", ProviderMessageType.Warning);
protected override string AddFromTorrentFile(ReleaseInfo release, string hash, string filename, byte[] fileContent)
protected override string AddFromTorrentFile(TorrentInfo release, string hash, string filename, byte[] fileContent)
{
_proxy.AddTorrentByFile(Convert.ToBase64String(fileContent), HandleTags(release, Settings, GetCategoryForRelease(release)), Settings);
return hash;
}
protected override string AddFromMagnetLink(ReleaseInfo release, string hash, string magnetLink)
protected override string AddFromMagnetLink(TorrentInfo release, string hash, string magnetLink)
{
_proxy.AddTorrentByUrl(magnetLink, HandleTags(release, Settings, GetCategoryForRelease(release)), Settings);
@@ -93,7 +93,7 @@ namespace NzbDrone.Core.Download.Clients.Flood
}
}
protected override string AddFromTorrentLink(ReleaseInfo release, string hash, string torrentLink)
protected override string AddFromTorrentLink(TorrentInfo release, string hash, string torrentLink)
{
throw new NotImplementedException();
}

View File

@@ -14,8 +14,8 @@ namespace NzbDrone.Core.Download.Clients.FreeboxDownload
public interface IFreeboxDownloadProxy
{
void Authenticate(FreeboxDownloadSettings settings);
string AddTaskFromUrl(string url, string directory, bool addPaused, bool addFirst, FreeboxDownloadSettings settings);
string AddTaskFromFile(string fileName, byte[] fileContent, string directory, bool addPaused, bool addFirst, FreeboxDownloadSettings settings);
string AddTaskFromUrl(string url, string directory, bool addPaused, bool addFirst, double? seedRatio, FreeboxDownloadSettings settings);
string AddTaskFromFile(string fileName, byte[] fileContent, string directory, bool addPaused, bool addFirst, double? seedRatio, FreeboxDownloadSettings settings);
void DeleteTask(string id, bool deleteData, FreeboxDownloadSettings settings);
FreeboxDownloadConfiguration GetDownloadConfiguration(FreeboxDownloadSettings settings);
List<FreeboxDownloadTask> GetTasks(FreeboxDownloadSettings settings);
@@ -46,7 +46,7 @@ namespace NzbDrone.Core.Download.Clients.FreeboxDownload
}
}
public string AddTaskFromUrl(string url, string directory, bool addPaused, bool addFirst, FreeboxDownloadSettings settings)
public string AddTaskFromUrl(string url, string directory, bool addPaused, bool addFirst, double? seedRatio, FreeboxDownloadSettings settings)
{
var request = BuildRequest(settings).Resource("/downloads/add").Post();
request.Headers.ContentType = "application/x-www-form-urlencoded";
@@ -60,12 +60,12 @@ namespace NzbDrone.Core.Download.Clients.FreeboxDownload
var response = ProcessRequest<FreeboxDownloadTask>(request.Build(), settings);
SetTorrentSettings(response.Result.Id, addPaused, addFirst, settings);
SetTorrentSettings(response.Result.Id, addPaused, addFirst, seedRatio, settings);
return response.Result.Id;
}
public string AddTaskFromFile(string fileName, byte[] fileContent, string directory, bool addPaused, bool addFirst, FreeboxDownloadSettings settings)
public string AddTaskFromFile(string fileName, byte[] fileContent, string directory, bool addPaused, bool addFirst, double? seedRatio, FreeboxDownloadSettings settings)
{
var request = BuildRequest(settings).Resource("/downloads/add").Post();
@@ -78,7 +78,7 @@ namespace NzbDrone.Core.Download.Clients.FreeboxDownload
var response = ProcessRequest<FreeboxDownloadTask>(request.Build(), settings);
SetTorrentSettings(response.Result.Id, addPaused, addFirst, settings);
SetTorrentSettings(response.Result.Id, addPaused, addFirst, seedRatio, settings);
return response.Result.Id;
}
@@ -118,7 +118,7 @@ namespace NzbDrone.Core.Download.Clients.FreeboxDownload
return $"{settings.Host}:{settings.AppId}:{settings.AppToken}";
}
private void SetTorrentSettings(string id, bool addPaused, bool addFirst, FreeboxDownloadSettings settings)
private void SetTorrentSettings(string id, bool addPaused, bool addFirst, double? seedRatio, FreeboxDownloadSettings settings)
{
var request = BuildRequest(settings).Resource("/downloads/" + id).Build();
@@ -136,6 +136,12 @@ namespace NzbDrone.Core.Download.Clients.FreeboxDownload
body.Add("queue_pos", "1");
}
if (seedRatio != null)
{
// 0 means unlimited seeding
body.Add("stop_ratio", seedRatio);
}
if (body.Count == 0)
{
return;

View File

@@ -36,7 +36,7 @@ namespace NzbDrone.Core.Download.Clients.FreeboxDownload
public class FreeboxDownloadSettings : IProviderConfig
{
private static readonly FreeboxDownloadSettingsValidator Validator = new FreeboxDownloadSettingsValidator();
private static readonly FreeboxDownloadSettingsValidator Validator = new ();
public FreeboxDownloadSettings()
{
@@ -73,7 +73,7 @@ namespace NzbDrone.Core.Download.Clients.FreeboxDownload
[FieldDefinition(8, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(FreeboxDownloadPriority), HelpText = "Priority to use when grabbing")]
public int Priority { get; set; }
[FieldDefinition(10, Label = "Add Paused", Type = FieldType.Checkbox)]
[FieldDefinition(9, Label = "Add Paused", Type = FieldType.Checkbox)]
public bool AddPaused { get; set; }
public NzbDroneValidationResult Validate()

View File

@@ -1,13 +1,11 @@
using System.Collections.Generic;
using System.Linq;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Download.Clients.FreeboxDownload.Responses;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Download.Clients.FreeboxDownload
@@ -18,49 +16,46 @@ namespace NzbDrone.Core.Download.Clients.FreeboxDownload
public TorrentFreeboxDownload(IFreeboxDownloadProxy proxy,
ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient,
ISeedConfigProvider seedConfigProvider,
IConfigService configService,
IDiskProvider diskProvider,
Logger logger)
: base(torrentFileInfoReader, httpClient, configService, diskProvider, logger)
: base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, logger)
{
_proxy = proxy;
}
public override string Name => "Freebox Download";
public override bool SupportsCategories => true;
protected IEnumerable<FreeboxDownloadTask> GetTorrents()
{
return _proxy.GetTasks(Settings).Where(v => v.Type.ToLower() == FreeboxDownloadTaskType.Bt.ToString().ToLower());
}
protected override string AddFromMagnetLink(ReleaseInfo release, string hash, string magnetLink)
protected override string AddFromMagnetLink(TorrentInfo release, string hash, string magnetLink)
{
return _proxy.AddTaskFromUrl(magnetLink,
GetDownloadDirectory(release).EncodeBase64(),
ToBePaused(),
ToBeQueuedFirst(),
GetSeedRatio(release),
Settings);
}
protected override string AddFromTorrentFile(ReleaseInfo release, string hash, string filename, byte[] fileContent)
protected override string AddFromTorrentFile(TorrentInfo release, string hash, string filename, byte[] fileContent)
{
return _proxy.AddTaskFromFile(filename,
fileContent,
GetDownloadDirectory(release).EncodeBase64(),
ToBePaused(),
ToBeQueuedFirst(),
GetSeedRatio(release),
Settings);
}
protected override string AddFromTorrentLink(ReleaseInfo release, string hash, string torrentLink)
protected override string AddFromTorrentLink(TorrentInfo release, string hash, string torrentLink)
{
return _proxy.AddTaskFromUrl(torrentLink,
GetDownloadDirectory(release).EncodeBase64(),
ToBePaused(),
ToBeQueuedFirst(),
GetSeedRatio(release),
Settings);
}
@@ -108,16 +103,21 @@ namespace NzbDrone.Core.Download.Clients.FreeboxDownload
var destDir = _proxy.GetDownloadConfiguration(Settings).DecodedDownloadDirectory.TrimEnd('/');
if (Settings.Category.IsNotNullOrWhiteSpace())
{
var category = GetCategoryForRelease(release) ?? Settings.Category;
var category = GetCategoryForRelease(release) ?? Settings.Category;
if (category.IsNotNullOrWhiteSpace())
{
destDir = $"{destDir}/{category}";
}
return destDir;
}
private bool ToBePaused()
{
return Settings.AddPaused;
}
private bool ToBeQueuedFirst()
{
if (Settings.Priority == (int)FreeboxDownloadPriority.First)
@@ -128,9 +128,14 @@ namespace NzbDrone.Core.Download.Clients.FreeboxDownload
return false;
}
private bool ToBePaused()
private double? GetSeedRatio(TorrentInfo release)
{
return Settings.AddPaused;
if (release.SeedConfiguration == null || release.SeedConfiguration.Ratio == null)
{
return null;
}
return release.SeedConfiguration.Ratio.Value * 100;
}
}
}

View File

@@ -4,8 +4,8 @@ using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Validation;
@@ -17,11 +17,11 @@ namespace NzbDrone.Core.Download.Clients.Hadouken
public Hadouken(IHadoukenProxy proxy,
ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient,
ISeedConfigProvider seedConfigProvider,
IConfigService configService,
IDiskProvider diskProvider,
Logger logger)
: base(torrentFileInfoReader, httpClient, configService, diskProvider, logger)
: base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, logger)
{
_proxy = proxy;
}
@@ -40,14 +40,14 @@ namespace NzbDrone.Core.Download.Clients.Hadouken
failures.AddIfNotNull(TestGetTorrents());
}
protected override string AddFromMagnetLink(ReleaseInfo release, string hash, string magnetLink)
protected override string AddFromMagnetLink(TorrentInfo release, string hash, string magnetLink)
{
_proxy.AddTorrentUri(Settings, magnetLink, GetCategoryForRelease(release) ?? Settings.Category);
return hash.ToUpper();
}
protected override string AddFromTorrentFile(ReleaseInfo release, string hash, string filename, byte[] fileContent)
protected override string AddFromTorrentFile(TorrentInfo release, string hash, string filename, byte[] fileContent)
{
return _proxy.AddTorrentFile(Settings, fileContent, GetCategoryForRelease(release) ?? Settings.Category).ToUpper();
}
@@ -97,7 +97,7 @@ namespace NzbDrone.Core.Download.Clients.Hadouken
return null;
}
protected override string AddFromTorrentLink(ReleaseInfo release, string hash, string torrentLink)
protected override string AddFromTorrentLink(TorrentInfo release, string hash, string torrentLink)
{
throw new NotImplementedException();
}

View File

@@ -6,8 +6,8 @@ using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Validation;
@@ -26,12 +26,12 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
public QBittorrent(IQBittorrentProxySelector proxySelector,
ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient,
ISeedConfigProvider seedConfigProvider,
IConfigService configService,
IDiskProvider diskProvider,
ICacheManager cacheManager,
Logger logger)
: base(torrentFileInfoReader, httpClient, configService, diskProvider, logger)
: base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, logger)
{
_proxySelector = proxySelector;
@@ -41,33 +41,41 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
private IQBittorrentProxy Proxy => _proxySelector.GetProxy(Settings);
private Version ProxyApiVersion => _proxySelector.GetApiVersion(Settings);
protected override string AddFromMagnetLink(ReleaseInfo release, string hash, string magnetLink)
protected override string AddFromMagnetLink(TorrentInfo release, string hash, string magnetLink)
{
if (!Proxy.GetConfig(Settings).DhtEnabled && !magnetLink.Contains("&tr="))
{
throw new NotSupportedException("Magnet Links without trackers not supported if DHT is disabled");
}
//var setShareLimits = release.SeedConfiguration != null && (release.SeedConfiguration.Ratio.HasValue || release.SeedConfiguration.SeedTime.HasValue);
//var addHasSetShareLimits = setShareLimits && ProxyApiVersion >= new Version(2, 8, 1);
var itemToTop = Settings.Priority == (int)QBittorrentPriority.First;
var setShareLimits = release.SeedConfiguration != null && (release.SeedConfiguration.Ratio.HasValue || release.SeedConfiguration.SeedTime.HasValue);
var addHasSetShareLimits = setShareLimits && ProxyApiVersion >= new Version(2, 8, 1);
var moveToTop = Settings.Priority == (int)QBittorrentPriority.First;
var forceStart = (QBittorrentState)Settings.InitialState == QBittorrentState.ForceStart;
var category = GetCategoryForRelease(release) ?? Settings.Category;
Proxy.AddTorrentFromUrl(magnetLink, null, Settings, category);
Proxy.AddTorrentFromUrl(magnetLink, addHasSetShareLimits && setShareLimits ? release.SeedConfiguration : null, Settings, category);
if (itemToTop || forceStart)
if ((!addHasSetShareLimits && setShareLimits) || moveToTop || forceStart)
{
if (!WaitForTorrent(hash))
{
return hash;
}
//if (!addHasSetShareLimits && setShareLimits)
//{
// Proxy.SetTorrentSeedingConfiguration(hash.ToLower(), release.SeedConfiguration, Settings);
//}
if (itemToTop)
if (!addHasSetShareLimits && setShareLimits)
{
try
{
Proxy.SetTorrentSeedingConfiguration(hash.ToLower(), release.SeedConfiguration, Settings);
}
catch (Exception ex)
{
_logger.Warn(ex, "Failed to set the torrent seed criteria for {0}.", hash);
}
}
if (moveToTop)
{
try
{
@@ -95,28 +103,36 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
return hash;
}
protected override string AddFromTorrentFile(ReleaseInfo release, string hash, string filename, byte[] fileContent)
protected override string AddFromTorrentFile(TorrentInfo release, string hash, string filename, byte[] fileContent)
{
//var setShareLimits = release.SeedConfiguration != null && (release.SeedConfiguration.Ratio.HasValue || release.SeedConfiguration.SeedTime.HasValue);
//var addHasSetShareLimits = setShareLimits && ProxyApiVersion >= new Version(2, 8, 1);
var itemToTop = Settings.Priority == (int)QBittorrentPriority.First;
var setShareLimits = release.SeedConfiguration != null && (release.SeedConfiguration.Ratio.HasValue || release.SeedConfiguration.SeedTime.HasValue);
var addHasSetShareLimits = setShareLimits && ProxyApiVersion >= new Version(2, 8, 1);
var moveToTop = Settings.Priority == (int)QBittorrentPriority.First;
var forceStart = (QBittorrentState)Settings.InitialState == QBittorrentState.ForceStart;
var category = GetCategoryForRelease(release) ?? Settings.Category;
Proxy.AddTorrentFromFile(filename, fileContent, null, Settings, category);
Proxy.AddTorrentFromFile(filename, fileContent, addHasSetShareLimits ? release.SeedConfiguration : null, Settings, category);
if (itemToTop || forceStart)
if ((!addHasSetShareLimits && setShareLimits) || moveToTop || forceStart)
{
if (!WaitForTorrent(hash))
{
return hash;
}
//if (!addHasSetShareLimits && setShareLimits)
//{
// Proxy.SetTorrentSeedingConfiguration(hash.ToLower(), release.SeedConfiguration, Settings);
//}
if (itemToTop)
if (!addHasSetShareLimits && setShareLimits)
{
try
{
Proxy.SetTorrentSeedingConfiguration(hash.ToLower(), release.SeedConfiguration, Settings);
}
catch (Exception ex)
{
_logger.Warn(ex, "Failed to set the torrent seed criteria for {0}.", hash);
}
}
if (moveToTop)
{
try
{
@@ -146,14 +162,16 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
protected bool WaitForTorrent(string hash)
{
var count = 5;
var count = 10;
while (count != 0)
{
try
{
Proxy.GetTorrentProperties(hash.ToLower(), Settings);
return true;
if (Proxy.IsTorrentLoaded(hash.ToLower(), Settings))
{
return true;
}
}
catch
{
@@ -235,9 +253,9 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
_logger.Error(ex, "Unable to test qBittorrent");
return new NzbDroneValidationFailure("Host", "Unable to connect to qBittorrent")
{
DetailedDescription = ex.Message
};
{
DetailedDescription = ex.Message
};
}
return null;
@@ -297,11 +315,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
{
var recentPriorityDefault = Settings.Priority == (int)QBittorrentPriority.Last;
if (recentPriorityDefault)
{
return null;
}
try
{
var config = Proxy.GetConfig(Settings);
@@ -450,7 +463,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
torrent.SeedingTime = torrentProperties.SeedingTime;
}
protected override string AddFromTorrentLink(ReleaseInfo release, string hash, string torrentLink)
protected override string AddFromTorrentLink(TorrentInfo release, string hash, string torrentLink)
{
throw new NotImplementedException();
}

View File

@@ -1,4 +1,4 @@
namespace NzbDrone.Core.Download.Clients.QBittorrent
namespace NzbDrone.Core.Download.Clients.QBittorrent
{
public class QBittorrentLabel
{

View File

@@ -1,4 +1,4 @@
namespace NzbDrone.Core.Download.Clients.QBittorrent
namespace NzbDrone.Core.Download.Clients.QBittorrent
{
public enum QBittorrentPriority
{

View File

@@ -1,11 +1,8 @@
using System;
using System.Collections.Generic;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Http;
namespace NzbDrone.Core.Download.Clients.QBittorrent
{
public interface IQBittorrentProxy
@@ -15,6 +12,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
string GetVersion(QBittorrentSettings settings);
QBittorrentPreferences GetConfig(QBittorrentSettings settings);
List<QBittorrentTorrent> GetTorrents(QBittorrentSettings settings);
bool IsTorrentLoaded(string hash, QBittorrentSettings settings);
QBittorrentTorrentProperties GetTorrentProperties(string hash, QBittorrentSettings settings);
List<QBittorrentTorrentFile> GetTorrentFiles(string hash, QBittorrentSettings settings);
@@ -40,7 +38,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
public class QBittorrentProxySelector : IQBittorrentProxySelector
{
private readonly IHttpClient _httpClient;
private readonly ICached<Tuple<IQBittorrentProxy, Version>> _proxyCache;
private readonly Logger _logger;
@@ -49,11 +46,9 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
public QBittorrentProxySelector(QBittorrentProxyV1 proxyV1,
QBittorrentProxyV2 proxyV2,
IHttpClient httpClient,
ICacheManager cacheManager,
Logger logger)
{
_httpClient = httpClient;
_proxyCache = cacheManager.GetCache<Tuple<IQBittorrentProxy, Version>>(GetType());
_logger = logger;

View File

@@ -97,6 +97,23 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
return response;
}
public bool IsTorrentLoaded(string hash, QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource($"/query/propertiesGeneral/{hash}");
request.LogHttpError = false;
try
{
ProcessRequest(request, settings);
return true;
}
catch
{
return false;
}
}
public QBittorrentTorrentProperties GetTorrentProperties(string hash, QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource($"/query/propertiesGeneral/{hash}");
@@ -295,15 +312,14 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
var request = requestBuilder.Build();
request.LogResponseContent = true;
request.SuppressHttpErrorStatusCodes = new[] { HttpStatusCode.Forbidden };
HttpResponse response;
try
{
response = _httpClient.Execute(request);
}
catch (HttpException ex)
{
if (ex.Response.StatusCode == HttpStatusCode.Forbidden)
if (response.StatusCode == HttpStatusCode.Forbidden)
{
_logger.Debug("Authentication required, logging in.");
@@ -313,10 +329,10 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
response = _httpClient.Execute(request);
}
else
{
throw new DownloadClientException("Failed to connect to qBittorrent, check your settings.", ex);
}
}
catch (HttpException ex)
{
throw new DownloadClientException("Failed to connect to qBittorrent, check your settings.", ex);
}
catch (WebException ex)
{
@@ -367,9 +383,9 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
throw new DownloadClientUnavailableException("Failed to connect to qBittorrent, please check your settings.", ex);
}
// returns "Fails." on bad login
if (response.Content != "Ok.")
{
// returns "Fails." on bad login
_logger.Debug("qbitTorrent authentication failed.");
throw new DownloadClientAuthenticationException("Failed to authenticate with qBittorrent.");
}

View File

@@ -106,6 +106,24 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
return response;
}
public bool IsTorrentLoaded(string hash, QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource("/api/v2/torrents/properties")
.AddQueryParam("hash", hash);
request.LogHttpError = false;
try
{
ProcessRequest(request, settings);
return true;
}
catch
{
return false;
}
}
public QBittorrentTorrentProperties GetTorrentProperties(string hash, QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource("/api/v2/torrents/properties")
@@ -129,24 +147,12 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
var request = BuildRequest(settings).Resource("/api/v2/torrents/add")
.Post()
.AddFormParameter("urls", torrentUrl);
if (category.IsNotNullOrWhiteSpace())
{
request.AddFormParameter("category", category);
}
// Note: ForceStart is handled by separate api call
if ((QBittorrentState)settings.InitialState == QBittorrentState.Start)
{
request.AddFormParameter("paused", false);
}
else if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause)
{
request.AddFormParameter("paused", true);
}
AddTorrentDownloadFormParameters(request, settings, category);
if (seedConfiguration != null)
{
AddTorrentSeedingFormParameters(request, seedConfiguration, settings);
AddTorrentSeedingFormParameters(request, seedConfiguration);
}
var result = ProcessRequest(request, settings);
@@ -164,24 +170,11 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
.Post()
.AddFormUpload("torrents", fileName, fileContent);
if (category.IsNotNullOrWhiteSpace())
{
request.AddFormParameter("category", category);
}
// Note: ForceStart is handled by separate api call
if ((QBittorrentState)settings.InitialState == QBittorrentState.Start)
{
request.AddFormParameter("paused", false);
}
else if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause)
{
request.AddFormParameter("paused", true);
}
AddTorrentDownloadFormParameters(request, settings, category);
if (seedConfiguration != null)
{
AddTorrentSeedingFormParameters(request, seedConfiguration, settings);
AddTorrentSeedingFormParameters(request, seedConfiguration);
}
var result = ProcessRequest(request, settings);
@@ -230,29 +223,57 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
return Json.Deserialize<Dictionary<string, QBittorrentLabel>>(ProcessRequest(request, settings));
}
private void AddTorrentSeedingFormParameters(HttpRequestBuilder request, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings)
private void AddTorrentSeedingFormParameters(HttpRequestBuilder request, TorrentSeedConfiguration seedConfiguration, bool always = false)
{
var ratioLimit = seedConfiguration.Ratio.HasValue ? seedConfiguration.Ratio : -2;
var seedingTimeLimit = seedConfiguration.SeedTime.HasValue ? (long)seedConfiguration.SeedTime.Value.TotalMinutes : -2;
if (ratioLimit != -2)
if (ratioLimit != -2 || always)
{
request.AddFormParameter("ratioLimit", ratioLimit);
}
if (seedingTimeLimit != -2)
if (seedingTimeLimit != -2 || always)
{
request.AddFormParameter("seedingTimeLimit", seedingTimeLimit);
}
}
private void AddTorrentDownloadFormParameters(HttpRequestBuilder request, QBittorrentSettings settings, string category)
{
if (category.IsNotNullOrWhiteSpace())
{
request.AddFormParameter("category", category);
}
// Note: ForceStart is handled by separate api call
if ((QBittorrentState)settings.InitialState == QBittorrentState.Start)
{
request.AddFormParameter("paused", false);
}
else if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause)
{
request.AddFormParameter("paused", true);
}
if (settings.SequentialOrder)
{
request.AddFormParameter("sequentialDownload", true);
}
if (settings.FirstAndLast)
{
request.AddFormParameter("firstLastPiecePrio", true);
}
}
public void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource("/api/v2/torrents/setShareLimits")
.Post()
.AddFormParameter("hashes", hash);
AddTorrentSeedingFormParameters(request, seedConfiguration, settings);
AddTorrentSeedingFormParameters(request, seedConfiguration, true);
try
{
@@ -341,15 +362,14 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
var request = requestBuilder.Build();
request.LogResponseContent = true;
request.SuppressHttpErrorStatusCodes = new[] { HttpStatusCode.Forbidden };
HttpResponse response;
try
{
response = _httpClient.Execute(request);
}
catch (HttpException ex)
{
if (ex.Response.StatusCode == HttpStatusCode.Forbidden)
if (response.StatusCode == HttpStatusCode.Forbidden)
{
_logger.Debug("Authentication required, logging in.");
@@ -359,10 +379,10 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
response = _httpClient.Execute(request);
}
else
{
throw new DownloadClientException("Failed to connect to qBittorrent, check your settings.", ex);
}
}
catch (HttpException ex)
{
throw new DownloadClientException("Failed to connect to qBittorrent, check your settings.", ex);
}
catch (WebException ex)
{
@@ -418,9 +438,9 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
throw new DownloadClientUnavailableException("Failed to connect to qBittorrent, please check your settings.", ex);
}
// returns "Fails." on bad login
if (response.Content != "Ok.")
{
// returns "Fails." on bad login
_logger.Debug("qbitTorrent authentication failed.");
throw new DownloadClientAuthenticationException("Failed to authenticate with qBittorrent.");
}

View File

@@ -56,6 +56,12 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
[FieldDefinition(8, Label = "Initial State", Type = FieldType.Select, SelectOptions = typeof(QBittorrentState), HelpText = "Initial state for torrents added to qBittorrent")]
public int InitialState { get; set; }
[FieldDefinition(9, Label = "Sequential Order", Type = FieldType.Checkbox, HelpText = "Download in sequential order (qBittorrent 4.1.0+)")]
public bool SequentialOrder { get; set; }
[FieldDefinition(10, Label = "First and Last First", Type = FieldType.Checkbox, HelpText = "Download first and last pieces first (qBittorrent 4.1.0+)")]
public bool FirstAndLast { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));

View File

@@ -3,8 +3,8 @@ using System.Text.RegularExpressions;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers;
namespace NzbDrone.Core.Download.Clients.Transmission
{
@@ -12,11 +12,11 @@ namespace NzbDrone.Core.Download.Clients.Transmission
{
public Transmission(ITransmissionProxy proxy,
ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient,
ISeedConfigProvider seedConfigProvider,
IConfigService configService,
IDiskProvider diskProvider,
Logger logger)
: base(proxy, torrentFileInfoReader, httpClient, configService, diskProvider, logger)
: base(proxy, torrentFileInfoReader, seedConfigProvider, configService, diskProvider, logger)
{
}

View File

@@ -4,8 +4,8 @@ using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Validation;
@@ -17,60 +17,20 @@ namespace NzbDrone.Core.Download.Clients.Transmission
public TransmissionBase(ITransmissionProxy proxy,
ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient,
ISeedConfigProvider seedConfigProvider,
IConfigService configService,
IDiskProvider diskProvider,
Logger logger)
: base(torrentFileInfoReader, httpClient, configService, diskProvider, logger)
: base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, logger)
{
_proxy = proxy;
}
protected bool HasReachedSeedLimit(TransmissionTorrent torrent, double? ratio, Lazy<TransmissionConfig> config)
{
var isStopped = torrent.Status == TransmissionTorrentStatus.Stopped;
var isSeeding = torrent.Status == TransmissionTorrentStatus.Seeding;
if (torrent.SeedRatioMode == 1)
{
if (isStopped && ratio.HasValue && ratio >= torrent.SeedRatioLimit)
{
return true;
}
}
else if (torrent.SeedRatioMode == 0)
{
if (isStopped && config.Value.SeedRatioLimited && ratio >= config.Value.SeedRatioLimit)
{
return true;
}
}
// Transmission doesn't support SeedTimeLimit, use/abuse seed idle limit, but only if it was set per-torrent.
if (torrent.SeedIdleMode == 1)
{
if ((isStopped || isSeeding) && torrent.SecondsSeeding > torrent.SeedIdleLimit * 60)
{
return true;
}
}
else if (torrent.SeedIdleMode == 0)
{
// The global idle limit is a real idle limit, if it's configured then 'Stopped' is enough.
if (isStopped && config.Value.IdleSeedingLimitEnabled)
{
return true;
}
}
return false;
}
protected override string AddFromMagnetLink(ReleaseInfo release, string hash, string magnetLink)
protected override string AddFromMagnetLink(TorrentInfo release, string hash, string magnetLink)
{
_proxy.AddTorrentFromUrl(magnetLink, GetDownloadDirectory(), Settings);
_proxy.SetTorrentSeedingConfiguration(hash, release.SeedConfiguration, Settings);
//_proxy.SetTorrentSeedingConfiguration(hash, release.SeedConfiguration, Settings);
if (Settings.Priority == (int)TransmissionPriority.First)
{
_proxy.MoveTorrentToTopInQueue(hash, Settings);
@@ -79,11 +39,11 @@ namespace NzbDrone.Core.Download.Clients.Transmission
return hash;
}
protected override string AddFromTorrentFile(ReleaseInfo release, string hash, string filename, byte[] fileContent)
protected override string AddFromTorrentFile(TorrentInfo release, string hash, string filename, byte[] fileContent)
{
_proxy.AddTorrentFromData(fileContent, GetDownloadDirectory(), Settings);
_proxy.SetTorrentSeedingConfiguration(hash, release.SeedConfiguration, Settings);
//_proxy.SetTorrentSeedingConfiguration(hash, release.SeedConfiguration, Settings);
if (Settings.Priority == (int)TransmissionPriority.First)
{
_proxy.MoveTorrentToTopInQueue(hash, Settings);
@@ -92,7 +52,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission
return hash;
}
protected override string AddFromTorrentLink(ReleaseInfo release, string hash, string torrentLink)
protected override string AddFromTorrentLink(TorrentInfo release, string hash, string torrentLink)
{
throw new NotImplementedException();
}

View File

@@ -1,4 +1,4 @@
using Newtonsoft.Json;
using Newtonsoft.Json;
namespace NzbDrone.Core.Download.Clients.Transmission
{

View File

@@ -1,4 +1,4 @@
namespace NzbDrone.Core.Download.Clients.Transmission
namespace NzbDrone.Core.Download.Clients.Transmission
{
public class TransmissionException : DownloadClientException
{

View File

@@ -1,4 +1,4 @@
namespace NzbDrone.Core.Download.Clients.Transmission
namespace NzbDrone.Core.Download.Clients.Transmission
{
public enum TransmissionPriority
{

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Net;
using Newtonsoft.Json.Linq;
@@ -141,7 +141,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission
private TransmissionResponse GetSessionVariables(TransmissionSettings settings)
{
// Retrieve transmission information such as the default download directory, bandwith throttling and seed ratio.
// Retrieve transmission information such as the default download directory, bandwidth throttling and seed ratio.
return ProcessRequest("session-get", null, settings);
}

View File

@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
namespace NzbDrone.Core.Download.Clients.Transmission
{

View File

@@ -1,4 +1,4 @@
namespace NzbDrone.Core.Download.Clients.Transmission
namespace NzbDrone.Core.Download.Clients.Transmission
{
public class TransmissionTorrent
{
@@ -9,7 +9,7 @@
public long TotalSize { get; set; }
public long LeftUntilDone { get; set; }
public bool IsFinished { get; set; }
public int Eta { get; set; }
public long Eta { get; set; }
public TransmissionTorrentStatus Status { get; set; }
public int SecondsDownloading { get; set; }
public int SecondsSeeding { get; set; }

View File

@@ -1,4 +1,4 @@
namespace NzbDrone.Core.Download.Clients.Transmission
namespace NzbDrone.Core.Download.Clients.Transmission
{
public enum TransmissionTorrentStatus
{

View File

@@ -1,9 +1,9 @@
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Download.Clients.Transmission;
using NzbDrone.Core.Indexers;
namespace NzbDrone.Core.Download.Clients.Vuze
{
@@ -13,11 +13,11 @@ namespace NzbDrone.Core.Download.Clients.Vuze
public Vuze(ITransmissionProxy proxy,
ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient,
ISeedConfigProvider seedConfigProvider,
IConfigService configService,
IDiskProvider diskProvider,
Logger logger)
: base(proxy, torrentFileInfoReader, httpClient, configService, diskProvider, logger)
: base(proxy, torrentFileInfoReader, seedConfigProvider, configService, diskProvider, logger)
{
}
@@ -27,7 +27,7 @@ namespace NzbDrone.Core.Download.Clients.Vuze
// - A multi-file torrent is downloaded in a job folder and 'outputPath' points to that directory directly.
// - A single-file torrent is downloaded in the root folder and 'outputPath' poinst to that root folder.
// We have to make sure the return value points to the job folder OR file.
if (outputPath == default || outputPath.FileName == torrent.Name || torrent.FileCount > 1)
if (outputPath.FileName == torrent.Name || torrent.FileCount > 1)
{
_logger.Trace("Vuze output directory: {0}", outputPath);
}

View File

@@ -6,10 +6,10 @@ using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Download.Clients.rTorrent;
using NzbDrone.Core.Exceptions;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
@@ -23,18 +23,18 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
public RTorrent(IRTorrentProxy proxy,
ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient,
ISeedConfigProvider seedConfigProvider,
IConfigService configService,
IDiskProvider diskProvider,
IRTorrentDirectoryValidator rTorrentDirectoryValidator,
Logger logger)
: base(torrentFileInfoReader, httpClient, configService, diskProvider, logger)
: base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, logger)
{
_proxy = proxy;
_rTorrentDirectoryValidator = rTorrentDirectoryValidator;
}
protected override string AddFromMagnetLink(ReleaseInfo release, string hash, string magnetLink)
protected override string AddFromMagnetLink(TorrentInfo release, string hash, string magnetLink)
{
var priority = (RTorrentPriority)Settings.Priority;
@@ -54,7 +54,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
return hash;
}
protected override string AddFromTorrentFile(ReleaseInfo release, string hash, string filename, byte[] fileContent)
protected override string AddFromTorrentFile(TorrentInfo release, string hash, string filename, byte[] fileContent)
{
var priority = (RTorrentPriority)Settings.Priority;
@@ -157,7 +157,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
return false;
}
protected override string AddFromTorrentLink(ReleaseInfo release, string hash, string torrentLink)
protected override string AddFromTorrentLink(TorrentInfo release, string hash, string torrentLink)
{
throw new NotImplementedException();
}

View File

@@ -3,11 +3,10 @@ using System.Collections.Generic;
using System.Net;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Validation;
@@ -16,29 +15,26 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
public class UTorrent : TorrentClientBase<UTorrentSettings>
{
private readonly IUTorrentProxy _proxy;
private readonly ICached<UTorrentTorrentCache> _torrentCache;
public UTorrent(IUTorrentProxy proxy,
ICacheManager cacheManager,
ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient,
ISeedConfigProvider seedConfigProvider,
IConfigService configService,
IDiskProvider diskProvider,
Logger logger)
: base(torrentFileInfoReader, httpClient, configService, diskProvider, logger)
: base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, logger)
{
_proxy = proxy;
_torrentCache = cacheManager.GetCache<UTorrentTorrentCache>(GetType(), "differentialTorrents");
}
protected override string AddFromMagnetLink(ReleaseInfo release, string hash, string magnetLink)
protected override string AddFromMagnetLink(TorrentInfo release, string hash, string magnetLink)
{
_proxy.AddTorrentFromUrl(magnetLink, Settings);
_proxy.SetTorrentSeedingConfiguration(hash, release.SeedConfiguration, Settings);
//_proxy.SetTorrentSeedingConfiguration(hash, release.SeedConfiguration, Settings);
var category = GetCategoryForRelease(release) ?? Settings.Category;
if (GetCategoryForRelease(release).IsNotNullOrWhiteSpace())
if (category.IsNotNullOrWhiteSpace())
{
_proxy.SetTorrentLabel(hash, category, Settings);
}
@@ -53,12 +49,13 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
return hash;
}
protected override string AddFromTorrentFile(ReleaseInfo release, string hash, string filename, byte[] fileContent)
protected override string AddFromTorrentFile(TorrentInfo release, string hash, string filename, byte[] fileContent)
{
_proxy.AddTorrentFromFile(filename, fileContent, Settings);
_proxy.SetTorrentSeedingConfiguration(hash, release.SeedConfiguration, Settings);
//_proxy.SetTorrentSeedingConfiguration(hash, release.SeedConfiguration, Settings);
var category = GetCategoryForRelease(release) ?? Settings.Category;
if (category.IsNotNullOrWhiteSpace())
{
_proxy.SetTorrentLabel(hash, category, Settings);
@@ -125,9 +122,9 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
_logger.Error(ex, "Failed to test uTorrent");
return new NzbDroneValidationFailure("Host", "Unable to connect to uTorrent")
{
DetailedDescription = ex.Message
};
{
DetailedDescription = ex.Message
};
}
return null;
@@ -148,7 +145,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
return null;
}
protected override string AddFromTorrentLink(ReleaseInfo release, string hash, string torrentLink)
protected override string AddFromTorrentLink(TorrentInfo release, string hash, string torrentLink)
{
throw new NotImplementedException();
}

View File

@@ -39,7 +39,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
public object Unknown28 { get; set; }
}
internal class UTorrentTorrentJsonConverter : JsonConverter
public class UTorrentTorrentJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{

View File

@@ -27,7 +27,20 @@ namespace NzbDrone.Core.Download
public virtual ProviderMessage Message => null;
public IEnumerable<ProviderDefinition> DefaultDefinitions => new List<ProviderDefinition>();
public IEnumerable<ProviderDefinition> DefaultDefinitions
{
get
{
var config = (IProviderConfig)new TSettings();
yield return new DownloadClientDefinition
{
Name = GetType().Name,
Implementation = GetType().Name,
Settings = config
};
}
}
public ProviderDefinition Definition { get; set; }

View File

@@ -58,8 +58,6 @@ namespace NzbDrone.Core.Download
throw new DownloadClientUnavailableException($"{release.DownloadProtocol} Download client isn't configured yet");
}
// Get the seed configuration for this release.
// remoteMovie.SeedConfiguration = _seedConfigProvider.GetSeedConfiguration(remoteMovie);
var indexer = _indexerFactory.GetInstance(_indexerFactory.Get(release.IndexerId));
var grabEvent = new IndexerDownloadEvent(release, true, source, host, release.Title, release.DownloadUrl)

View File

@@ -5,7 +5,6 @@ using MonoTorrent;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Exceptions;
using NzbDrone.Core.Indexers;
@@ -18,32 +17,38 @@ namespace NzbDrone.Core.Download
public abstract class TorrentClientBase<TSettings> : DownloadClientBase<TSettings>
where TSettings : IProviderConfig, new()
{
protected readonly IHttpClient _httpClient;
protected readonly ITorrentFileInfoReader _torrentFileInfoReader;
private readonly ITorrentFileInfoReader _torrentFileInfoReader;
private readonly ISeedConfigProvider _seedConfigProvider;
protected TorrentClientBase(ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient,
ISeedConfigProvider seedConfigProvider,
IConfigService configService,
IDiskProvider diskProvider,
Logger logger)
: base(configService, diskProvider, logger)
{
_httpClient = httpClient;
_torrentFileInfoReader = torrentFileInfoReader;
_seedConfigProvider = seedConfigProvider;
}
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
public virtual bool PreferTorrentFile => false;
protected abstract string AddFromMagnetLink(ReleaseInfo release, string hash, string magnetLink);
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 AddFromMagnetLink(TorrentInfo release, string hash, string magnetLink);
protected abstract string AddFromTorrentFile(TorrentInfo release, string hash, string filename, byte[] fileContent);
protected abstract string AddFromTorrentLink(TorrentInfo release, string hash, string torrentLink);
public override async Task<string> Download(ReleaseInfo release, bool redirect, IIndexer indexer)
{
var torrentInfo = release as TorrentInfo;
if (torrentInfo != null)
{
// Get the seed configuration for this release.
torrentInfo.SeedConfiguration = _seedConfigProvider.GetSeedConfiguration(release);
}
string magnetUrl = null;
string torrentUrl = null;
@@ -67,7 +72,7 @@ namespace NzbDrone.Core.Download
{
try
{
return await DownloadFromWebUrl(release, indexer, torrentUrl);
return await DownloadFromWebUrl(torrentInfo, indexer, torrentUrl);
}
catch (Exception ex)
{
@@ -84,7 +89,7 @@ namespace NzbDrone.Core.Download
{
try
{
return DownloadFromMagnetUrl(release, magnetUrl);
return DownloadFromMagnetUrl(torrentInfo, magnetUrl);
}
catch (NotSupportedException ex)
{
@@ -98,7 +103,7 @@ namespace NzbDrone.Core.Download
{
try
{
return DownloadFromMagnetUrl(release, magnetUrl);
return DownloadFromMagnetUrl(torrentInfo, magnetUrl);
}
catch (NotSupportedException ex)
{
@@ -113,14 +118,14 @@ namespace NzbDrone.Core.Download
if (torrentUrl.IsNotNullOrWhiteSpace())
{
return await DownloadFromWebUrl(release, indexer, torrentUrl);
return await DownloadFromWebUrl(torrentInfo, indexer, torrentUrl);
}
}
return null;
}
private async Task<string> DownloadFromWebUrl(ReleaseInfo release, IIndexer indexer, string torrentUrl)
private async Task<string> DownloadFromWebUrl(TorrentInfo release, IIndexer indexer, string torrentUrl)
{
byte[] torrentFile = null;
@@ -155,7 +160,7 @@ namespace NzbDrone.Core.Download
return actualHash;
}
private string DownloadFromMagnetUrl(ReleaseInfo release, string magnetUrl)
private string DownloadFromMagnetUrl(TorrentInfo release, string magnetUrl)
{
string hash = null;
string actualHash = null;

View File

@@ -14,13 +14,11 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
public void Clean()
{
using (var mapper = _database.OpenConnection())
{
mapper.Execute(@"DELETE FROM ""Users""
WHERE ""Id"" NOT IN (
SELECT ""Id"" FROM ""Users""
LIMIT 1)");
}
using var mapper = _database.OpenConnection();
mapper.Execute(@"DELETE FROM ""Users""
WHERE ""Id"" NOT IN (
SELECT ""Id"" FROM ""Users""
LIMIT 1)");
}
}
}

View File

@@ -14,14 +14,13 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
public void Clean()
{
var mapper = _database.OpenConnection();
using var mapper = _database.OpenConnection();
mapper.Execute(@"DELETE FROM ""ApplicationStatus""
WHERE ""Id"" IN (
SELECT ""ApplicationStatus"".""Id"" FROM ""ApplicationStatus""
LEFT OUTER JOIN ""Applications""
ON ""ApplicationStatus"".""ProviderId"" = ""Applications"".""Id""
WHERE ""Applications"".""Id"" IS NULL)");
WHERE ""Id"" IN (
SELECT ""ApplicationStatus"".""Id"" FROM ""ApplicationStatus""
LEFT OUTER JOIN ""Applications""
ON ""ApplicationStatus"".""ProviderId"" = ""Applications"".""Id""
WHERE ""Applications"".""Id"" IS NULL)");
}
}
}

View File

@@ -14,14 +14,13 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
public void Clean()
{
var mapper = _database.OpenConnection();
using var mapper = _database.OpenConnection();
mapper.Execute(@"DELETE FROM ""DownloadClientStatus""
WHERE ""Id"" IN (
SELECT ""DownloadClientStatus"".""Id"" FROM ""DownloadClientStatus""
LEFT OUTER JOIN ""DownloadClients""
ON ""DownloadClientStatus"".""ProviderId"" = ""DownloadClients"".""Id""
WHERE ""DownloadClients"".""Id"" IS NULL)");
WHERE ""Id"" IN (
SELECT ""DownloadClientStatus"".""Id"" FROM ""DownloadClientStatus""
LEFT OUTER JOIN ""DownloadClients""
ON ""DownloadClientStatus"".""ProviderId"" = ""DownloadClients"".""Id""
WHERE ""DownloadClients"".""Id"" IS NULL)");
}
}
}

View File

@@ -19,15 +19,13 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
private void CleanupOrphanedByIndexer()
{
using (var mapper = _database.OpenConnection())
{
mapper.Execute(@"DELETE FROM ""History""
WHERE ""Id"" IN (
SELECT ""History"".""Id"" FROM ""History""
LEFT OUTER JOIN ""Indexers""
ON ""History"".""IndexerId"" = ""Indexers"".""Id""
WHERE ""Indexers"".""Id"" IS NULL)");
}
using var mapper = _database.OpenConnection();
mapper.Execute(@"DELETE FROM ""History""
WHERE ""Id"" IN (
SELECT ""History"".""Id"" FROM ""History""
LEFT OUTER JOIN ""Indexers""
ON ""History"".""IndexerId"" = ""Indexers"".""Id""
WHERE ""Indexers"".""Id"" IS NULL)");
}
}
}

View File

@@ -14,15 +14,13 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
public void Clean()
{
using (var mapper = _database.OpenConnection())
{
mapper.Execute(@"DELETE FROM ""IndexerStatus""
WHERE ""Id"" IN (
SELECT ""IndexerStatus"".""Id"" FROM ""IndexerStatus""
LEFT OUTER JOIN ""Indexers""
ON ""IndexerStatus"".""ProviderId"" = ""Indexers"".""Id""
WHERE ""Indexers"".""Id"" IS NULL)");
}
using var mapper = _database.OpenConnection();
mapper.Execute(@"DELETE FROM ""IndexerStatus""
WHERE ""Id"" IN (
SELECT ""IndexerStatus"".""Id"" FROM ""IndexerStatus""
LEFT OUTER JOIN ""Indexers""
ON ""IndexerStatus"".""ProviderId"" = ""Indexers"".""Id""
WHERE ""Indexers"".""Id"" IS NULL)");
}
}
}

View File

@@ -17,23 +17,21 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
public void Clean()
{
using (var mapper = _database.OpenConnection())
using var mapper = _database.OpenConnection();
var usedTags = new[] { "Notifications", "IndexerProxies", "Indexers", "Applications" }
.SelectMany(v => GetUsedTags(v, mapper))
.Distinct()
.ToArray();
if (usedTags.Length > 0)
{
var usedTags = new[] { "Notifications", "IndexerProxies", "Indexers", "Applications" }
.SelectMany(v => GetUsedTags(v, mapper))
.Distinct()
.ToArray();
var usedTagsList = string.Join(",", usedTags.Select(d => d.ToString()).ToArray());
if (usedTags.Length > 0)
{
var usedTagsList = string.Join(",", usedTags.Select(d => d.ToString()).ToArray());
mapper.Execute($"DELETE FROM \"Tags\" WHERE NOT \"Id\" IN ({usedTagsList})");
}
else
{
mapper.Execute($"DELETE FROM \"Tags\"");
}
mapper.Execute($"DELETE FROM \"Tags\" WHERE NOT \"Id\" IN ({usedTagsList})");
}
else
{
mapper.Execute("DELETE FROM \"Tags\"");
}
}

View File

@@ -24,13 +24,11 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
_logger.Debug("Not running scheduled task last execution cleanup during debug");
}
using (var mapper = _database.OpenConnection())
{
mapper.Execute(@"UPDATE ""ScheduledTasks""
SET ""LastExecution"" = @time
WHERE ""LastExecution"" > @time",
new { time = DateTime.UtcNow });
}
using var mapper = _database.OpenConnection();
mapper.Execute(@"UPDATE ""ScheduledTasks""
SET ""LastExecution"" = @time
WHERE ""LastExecution"" > @time",
new { time = DateTime.UtcNow });
}
}
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NLog;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.ThingiProvider;
@@ -16,5 +17,10 @@ namespace NzbDrone.Core.IndexerProxies
: base(providerRepository, providers, container, eventAggregator, logger)
{
}
protected override List<IndexerProxyDefinition> Active()
{
return All().Where(c => c.Enable && c.Settings.Validate().IsValid).ToList();
}
}
}

View File

@@ -1,13 +1,14 @@
using System.Text.RegularExpressions;
using NzbDrone.Common.Extensions;
namespace NzbDrone.Core.IndexerSearch
{
public class NewznabRequest
{
private static readonly Regex TvRegex = new Regex(@"\{((?:imdbid\:)(?<imdbid>[^{]+)|(?:rid\:)(?<rid>[^{]+)|(?:tvdbid\:)(?<tvdbid>[^{]+)|(?:tmdbid\:)(?<tmdbid>[^{]+)|(?:doubanid\:)(?<doubanid>[^{]+)|(?:season\:)(?<season>[^{]+)|(?:episode\:)(?<episode>[^{]+)|(?:year\:)(?<year>[^{]+)|(?:genre\:)(?<genre>[^{]+))\}", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex MovieRegex = new Regex(@"\{((?:imdbid\:)(?<imdbid>[^{]+)|(?:doubanid\:)(?<doubanid>[^{]+)|(?:tmdbid\:)(?<tmdbid>[^{]+)|(?:traktid\:)(?<traktid>[^{]+)|(?:year\:)(?<year>[^{]+)|(?:genre\:)(?<genre>[^{]+))\}", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex MusicRegex = new Regex(@"\{((?:artist\:)(?<artist>[^{]+)|(?:album\:)(?<album>[^{]+)|(?:track\:)(?<track>[^{]+)|(?:label\:)(?<label>[^{]+)|(?:year\:)(?<year>[^{]+)|(?:genre\:)(?<genre>[^{]+))\}", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex BookRegex = new Regex(@"\{((?:author\:)(?<author>[^{]+)|(?:publisher\:)(?<publisher>[^{]+)|(?:title\:)(?<title>[^{]+)|(?:year\:)(?<year>[^{]+)|(?:genre\:)(?<genre>[^{]+))\}", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex TvRegex = new (@"\{((?:imdbid\:)(?<imdbid>[^{]+)|(?:rid\:)(?<rid>[^{]+)|(?:tvdbid\:)(?<tvdbid>[^{]+)|(?:tmdbid\:)(?<tmdbid>[^{]+)|(?:doubanid\:)(?<doubanid>[^{]+)|(?:season\:)(?<season>[^{]+)|(?:episode\:)(?<episode>[^{]+)|(?:year\:)(?<year>[^{]+)|(?:genre\:)(?<genre>[^{]+))\}", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex MovieRegex = new (@"\{((?:imdbid\:)(?<imdbid>[^{]+)|(?:doubanid\:)(?<doubanid>[^{]+)|(?:tmdbid\:)(?<tmdbid>[^{]+)|(?:traktid\:)(?<traktid>[^{]+)|(?:year\:)(?<year>[^{]+)|(?:genre\:)(?<genre>[^{]+))\}", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex MusicRegex = new (@"\{((?:artist\:)(?<artist>[^{]+)|(?:album\:)(?<album>[^{]+)|(?:track\:)(?<track>[^{]+)|(?:label\:)(?<label>[^{]+)|(?:year\:)(?<year>[^{]+)|(?:genre\:)(?<genre>[^{]+))\}", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex BookRegex = new (@"\{((?:author\:)(?<author>[^{]+)|(?:publisher\:)(?<publisher>[^{]+)|(?:title\:)(?<title>[^{]+)|(?:year\:)(?<year>[^{]+)|(?:genre\:)(?<genre>[^{]+))\}", RegexOptions.Compiled | RegexOptions.IgnoreCase);
public string t { get; set; }
public string q { get; set; }
@@ -40,6 +41,11 @@ namespace NzbDrone.Core.IndexerSearch
public void QueryToParams()
{
if (q.IsNullOrWhiteSpace())
{
return;
}
if (t == "tvsearch")
{
var matches = TvRegex.Matches(q);

View File

@@ -76,8 +76,12 @@ namespace NzbDrone.Core.IndexerSearch
select new XElement("item",
new XElement("title", RemoveInvalidXMLChars(r.Title)),
new XElement("description", RemoveInvalidXMLChars(r.Description)),
new XElement("guid", r.Guid), // GUID and (Link or Magnet) are mandatory
new XElement("prowlarrindexer", new XAttribute("id", r.IndexerId), r.Indexer),
new XElement("guid", r.Guid), // GUID and (Link or Magnet) are mandatory
new XElement(
"prowlarrindexer",
new XAttribute("id", r.IndexerId),
new XAttribute("type", r.IndexerPrivacy switch { IndexerPrivacy.Private => "private", IndexerPrivacy.Public => "public", _ => "semi-private" }),
r.Indexer),
r.InfoUrl == null ? null : new XElement("comments", r.InfoUrl),
r.PublishDate == DateTime.MinValue ? new XElement("pubDate", XmlDateFormat(DateTime.Now)) : new XElement("pubDate", XmlDateFormat(r.PublishDate)),
new XElement("size", r.Size),

View File

@@ -4,6 +4,7 @@ using System.Linq;
using System.Threading.Tasks;
using NLog;
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Core.Exceptions;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.Events;
using NzbDrone.Core.IndexerSearch.Definitions;
@@ -157,15 +158,22 @@ namespace NzbDrone.Core.IndexerSearch
{
var indexers = _indexerFactory.Enabled();
if (criteriaBase.IndexerIds != null && criteriaBase.IndexerIds.Count > 0)
if (criteriaBase.IndexerIds is { Count: > 0 })
{
indexers = indexers.Where(i => criteriaBase.IndexerIds.Contains(i.Definition.Id) ||
(criteriaBase.IndexerIds.Contains(-1) && i.Protocol == DownloadProtocol.Usenet) ||
(criteriaBase.IndexerIds.Contains(-2) && i.Protocol == DownloadProtocol.Torrent))
.ToList();
if (indexers.Count == 0)
{
_logger.Debug("Search failed due to all selected indexers being unavailable: {0}", string.Join(", ", criteriaBase.IndexerIds));
throw new SearchFailedException("Search failed due to all selected indexers being unavailable");
}
}
if (criteriaBase.Categories != null && criteriaBase.Categories.Length > 0)
if (criteriaBase.Categories is { Length: > 0 })
{
//Only query supported indexers
indexers = indexers.Where(i => ((IndexerDefinition)i.Definition).Capabilities.Categories.SupportedCategories(criteriaBase.Categories).Any()).ToList();
@@ -173,6 +181,7 @@ namespace NzbDrone.Core.IndexerSearch
if (indexers.Count == 0)
{
_logger.Debug("All provided categories are unsupported by selected indexers: {0}", string.Join(", ", criteriaBase.Categories));
return Array.Empty<ReleaseInfo>();
}
}

View File

@@ -27,7 +27,6 @@ namespace NzbDrone.Core.Indexers.Definitions
public override string Description => "Anidex is a Public torrent tracker and indexer, primarily for English fansub groups of anime";
public override string Language => "en-US";
public override Encoding Encoding => Encoding.UTF8;
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
public override IndexerPrivacy Privacy => IndexerPrivacy.Public;
public override IndexerCapabilities Capabilities => SetCapabilities();

View File

@@ -25,10 +25,9 @@ namespace NzbDrone.Core.Indexers.Definitions
{
public override string Name => "Anidub";
public override string[] IndexerUrls => new[] { "https://tr.anidub.com/" };
public override string Description => "Anidub is russian anime voiceover group and eponymous anime tracker.";
public override string Description => "Anidub is RUSSIAN anime voiceover group and eponymous anime tracker.";
public override string Language => "ru-RU";
public override Encoding Encoding => Encoding.UTF8;
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
public override IndexerPrivacy Privacy => IndexerPrivacy.SemiPrivate;
public override IndexerCapabilities Capabilities => SetCapabilities();

View File

@@ -35,7 +35,6 @@ namespace NzbDrone.Core.Indexers.Definitions
public override string Description => "AnimeBytes (AB) is the largest private torrent tracker that specialises in anime and anime-related content.";
public override string Language => "en-US";
public override Encoding Encoding => Encoding.UTF8;
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public override IndexerCapabilities Capabilities => SetCapabilities();
public override TimeSpan RateLimit => TimeSpan.FromSeconds(4);

View File

@@ -24,7 +24,6 @@ namespace NzbDrone.Core.Indexers.Definitions
public override string Name => "AnimeTorrents";
public override string[] IndexerUrls => new[] { "https://animetorrents.me/" };
public override string Description => "Definitive source for anime and manga";
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public override bool SupportsPagination => true;
public override TimeSpan RateLimit => TimeSpan.FromSeconds(4);

View File

@@ -24,7 +24,6 @@ namespace NzbDrone.Core.Indexers.Definitions
public override string Description => "Animedia is RUSSIAN anime voiceover group and eponymous anime tracker.";
public override string Language => "ru-RU";
public override Encoding Encoding => Encoding.UTF8;
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
public override IndexerPrivacy Privacy => IndexerPrivacy.Public;
public override IndexerCapabilities Capabilities => SetCapabilities();

View File

@@ -28,7 +28,6 @@ namespace NzbDrone.Core.Indexers.Definitions
public override string Description => "A movies tracker";
public override string Language => "en-US";
public override Encoding Encoding => Encoding.UTF8;
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public override IndexerCapabilities Capabilities => SetCapabilities();

View File

@@ -59,7 +59,6 @@ public class AudioBookBay : TorrentIndexerBase<NoAuthTorrentBaseSettings>
};
public override string Description => "AudioBook Bay (ABB) is a public Torrent Tracker for AUDIOBOOKS";
public override string Language => "en-US";
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
public override IndexerPrivacy Privacy => IndexerPrivacy.Public;
public override int PageSize => 15;
public override IndexerCapabilities Capabilities => SetCapabilities();

View File

@@ -12,7 +12,6 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
{
public abstract class AvistazBase : TorrentIndexerBase<AvistazSettings>
{
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
public override bool SupportsRss => true;
public override bool SupportsSearch => true;
public override bool SupportsPagination => true;
@@ -93,12 +92,10 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
var jsonResponse = new HttpResponse<AvistazErrorResponse>(ex.Response);
return new ValidationFailure(string.Empty, jsonResponse.Resource?.Message ?? "Unauthorized request to indexer");
}
else
{
_logger.Warn(ex, "Unable to connect to indexer");
return new ValidationFailure(string.Empty, "Unable to connect to indexer, check the log above the ValidationFailure for more details. " + ex.Message);
}
_logger.Warn(ex, "Unable to connect to indexer");
return new ValidationFailure(string.Empty, "Unable to connect to indexer, check the log above the ValidationFailure for more details. " + ex.Message);
}
catch (Exception ex)
{
@@ -107,7 +104,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
return new ValidationFailure(string.Empty, "Unable to connect to indexer, check the log above the ValidationFailure for more details");
}
return null;
return await base.TestConnection();
}
private async Task<string> GetToken()

View File

@@ -28,7 +28,6 @@ namespace NzbDrone.Core.Indexers.Definitions
public override string Description => "BB is a Private Torrent Tracker for 0DAY / GENERAL";
public override string Language => "en-US";
public override Encoding Encoding => Encoding.UTF8;
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public override IndexerCapabilities Capabilities => SetCapabilities();

View File

@@ -28,7 +28,6 @@ namespace NzbDrone.Core.Indexers.Definitions
public override string[] IndexerUrls => new[] { "https://bakabt.me/" };
public override string Description => "Anime Community";
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public override IndexerCapabilities Capabilities => SetCapabilities();
@@ -413,7 +412,7 @@ namespace NzbDrone.Core.Indexers.Definitions
FreeleechOnly = false;
}
[FieldDefinition(4, Label = "Freeleech Only", Type = FieldType.Checkbox, HelpText = "Search freeleech torrents only")]
[FieldDefinition(4, Label = "Freeleech Only", Type = FieldType.Checkbox, HelpText = "Show freeleech torrents only")]
public bool FreeleechOnly { get; set; }
[FieldDefinition(5, Label = "Add Romaji Title", Type = FieldType.Checkbox, HelpText = "Add releases for Romaji Title")]

View File

@@ -28,7 +28,6 @@ namespace NzbDrone.Core.Indexers.Definitions
public override string[] IndexerUrls => new string[] { "https://beyond-hd.me/" };
public override string Description => "BeyondHD (BHD) is a Private Torrent Tracker for HD MOVIES / TV";
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public override IndexerCapabilities Capabilities => SetCapabilities();

View File

@@ -26,7 +26,6 @@ namespace NzbDrone.Core.Indexers.Definitions
public override string Description => "The binary Usenet search engine";
public override string Language => "en-US";
public override Encoding Encoding => Encoding.UTF8;
public override DownloadProtocol Protocol => DownloadProtocol.Usenet;
public override IndexerPrivacy Privacy => IndexerPrivacy.Public;
public override bool SupportsRss => false;
public override bool SupportsPagination => true;

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