mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2026-04-16 21:35:04 -04:00
Compare commits
134 Commits
v1.9.3.401
...
v1.11.2.41
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7890ef6f9d | ||
|
|
df8e4e5acb | ||
|
|
1b36951879 | ||
|
|
d8d5170ab8 | ||
|
|
a443a87603 | ||
|
|
df18ee77e7 | ||
|
|
426159b452 | ||
|
|
8704bef69a | ||
|
|
0f1b01adab | ||
|
|
4dbf5aa9f4 | ||
|
|
e1264d7cda | ||
|
|
d4bbb2e14a | ||
|
|
a2395dccb5 | ||
|
|
df89450428 | ||
|
|
7b5e1f40ba | ||
|
|
fe142b8a1c | ||
|
|
8c7f73ed16 | ||
|
|
a4ae800603 | ||
|
|
e5d7a21714 | ||
|
|
67355ec07b | ||
|
|
43d56f6817 | ||
|
|
515f4dff8b | ||
|
|
c3aa377ed5 | ||
|
|
fbc3c09094 | ||
|
|
b88f8e5fde | ||
|
|
aaf4c73c83 | ||
|
|
7c38ec2baa | ||
|
|
dcfdd49119 | ||
|
|
badb9dee61 | ||
|
|
12ca705556 | ||
|
|
a0d0e3e232 | ||
|
|
e12cd68772 | ||
|
|
9dc117191e | ||
|
|
fdaca64d60 | ||
|
|
6d134750ff | ||
|
|
bbf9945b49 | ||
|
|
b66a85269f | ||
|
|
81e9c29d54 | ||
|
|
3df33e1a86 | ||
|
|
c19802c471 | ||
|
|
36c8806f88 | ||
|
|
0d69a42ae0 | ||
|
|
fc482d4808 | ||
|
|
fab4bd5ead | ||
|
|
9c243c7e0d | ||
|
|
c107011659 | ||
|
|
c690e9a50a | ||
|
|
c4b0ecdafe | ||
|
|
f2a709f725 | ||
|
|
01a9799ce8 | ||
|
|
8ac68e8946 | ||
|
|
6cb787c17b | ||
|
|
51259ceb30 | ||
|
|
92cf329174 | ||
|
|
257c9ab248 | ||
|
|
cad42fd005 | ||
|
|
d7927f60fe | ||
|
|
26a657fa77 | ||
|
|
7fa4daae9b | ||
|
|
e1b063eaa5 | ||
|
|
75f00436ec | ||
|
|
343d7088c9 | ||
|
|
709dfe453b | ||
|
|
3130fac106 | ||
|
|
28004dfae1 | ||
|
|
9b34c89bc8 | ||
|
|
28e90acd0d | ||
|
|
9d11d7e17f | ||
|
|
2cbdb5bcba | ||
|
|
118bfb8c28 | ||
|
|
942477ecf9 | ||
|
|
4b4589ed27 | ||
|
|
bd0609639e | ||
|
|
ccdad3a44c | ||
|
|
d99da0481b | ||
|
|
da1965b18e | ||
|
|
493114f4e8 | ||
|
|
6969326092 | ||
|
|
95f899131d | ||
|
|
0ba4f3e692 | ||
|
|
a7c00a0fd7 | ||
|
|
c84ff60ec9 | ||
|
|
b3f6f54e6e | ||
|
|
ed272aaf74 | ||
|
|
c0b10f889b | ||
|
|
bbfb92bbd8 | ||
|
|
793de05e3d | ||
|
|
1b1f9d16be | ||
|
|
051dea30c2 | ||
|
|
75d8a3d1d0 | ||
|
|
edf41e2ead | ||
|
|
c15c71386d | ||
|
|
71a19efd9a | ||
|
|
2c6c0fcc81 | ||
|
|
203e2dbb10 | ||
|
|
6169fc2fa3 | ||
|
|
768ce14afb | ||
|
|
31d32e8c30 | ||
|
|
7a61761b2b | ||
|
|
3963807c96 | ||
|
|
e0f6726a3d | ||
|
|
dd25bff3d6 | ||
|
|
d834c4292e | ||
|
|
7e8272ec2b | ||
|
|
62548f32fe | ||
|
|
db9f061564 | ||
|
|
b37d8799a0 | ||
|
|
4366530409 | ||
|
|
c7959f735e | ||
|
|
be3ee00e1f | ||
|
|
dace1982d6 | ||
|
|
980bd35f95 | ||
|
|
4b2f81bee8 | ||
|
|
30eb481c65 | ||
|
|
29f1c36f54 | ||
|
|
f1c01343bf | ||
|
|
bae79b22ad | ||
|
|
229d879f86 | ||
|
|
d1cee950a4 | ||
|
|
7e32b54547 | ||
|
|
b1f7d30021 | ||
|
|
c41a7e0ccc | ||
|
|
42c533386b | ||
|
|
bdae7a2cdc | ||
|
|
5e8d3542f4 | ||
|
|
d9d2aa8493 | ||
|
|
09bf1500d6 | ||
|
|
34464160cb | ||
|
|
bada5fe309 | ||
|
|
b088febbc4 | ||
|
|
1a307b8e21 | ||
|
|
32db2af0ea | ||
|
|
e602862102 | ||
|
|
bd5336e4c4 |
@@ -2,7 +2,7 @@
|
||||
|
||||
[](https://dev.azure.com/Prowlarr/Prowlarr/_build/latest?definitionId=1&branchName=develop)
|
||||
[](https://translate.servarr.com/engage/prowlarr/?utm_source=widget)
|
||||
[](https://wiki.servarr.com/prowlarr/installation#docker)
|
||||
[](https://wiki.servarr.com/prowlarr/installation/docker)
|
||||

|
||||
[](#backers)
|
||||
[](#sponsors)
|
||||
|
||||
@@ -9,13 +9,13 @@ variables:
|
||||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '1.9.3'
|
||||
majorVersion: '1.11.2'
|
||||
minorVersion: $[counter('minorVersion', 1)]
|
||||
prowlarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
|
||||
sentryOrg: 'servarr'
|
||||
sentryUrl: 'https://sentry.servarr.com'
|
||||
dotnetVersion: '6.0.413'
|
||||
dotnetVersion: '6.0.417'
|
||||
nodeVersion: '16.X'
|
||||
innoVersion: '6.2.0'
|
||||
windowsImage: 'windows-2022'
|
||||
|
||||
@@ -65,12 +65,12 @@ function AppUpdatedModalContent(props) {
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{translate('AppUpdated', { appName: 'Prowlarr' })}
|
||||
{translate('AppUpdated')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div>
|
||||
<InlineMarkdown data={translate('AppUpdatedVersion', { appName: 'Prowlarr', version })} blockClassName={styles.version} />
|
||||
<InlineMarkdown data={translate('AppUpdatedVersion', { version })} blockClassName={styles.version} />
|
||||
</div>
|
||||
|
||||
{
|
||||
|
||||
@@ -28,11 +28,11 @@ function ConnectionLostModal(props) {
|
||||
|
||||
<ModalBody>
|
||||
<div>
|
||||
{translate('ConnectionLostToBackend', { appName: 'Prowlarr' })}
|
||||
{translate('ConnectionLostToBackend')}
|
||||
</div>
|
||||
|
||||
<div className={styles.automatic}>
|
||||
{translate('ConnectionLostReconnect', { appName: 'Prowlarr' })}
|
||||
{translate('ConnectionLostReconnect')}
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-right: $formLabelRightMarginWidth;
|
||||
padding-top: 8px;
|
||||
min-height: 35px;
|
||||
text-align: end;
|
||||
font-weight: bold;
|
||||
line-height: 35px;
|
||||
}
|
||||
|
||||
.hasError {
|
||||
|
||||
13
frontend/src/Components/Form/InfoInput.css
Normal file
13
frontend/src/Components/Form/InfoInput.css
Normal file
@@ -0,0 +1,13 @@
|
||||
.message {
|
||||
composes: alert from '~Components/Alert.css';
|
||||
|
||||
a {
|
||||
color: var(--linkColor);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
color: var(--linkHoverColor);
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
7
frontend/src/Components/Form/InfoInput.css.d.ts
vendored
Normal file
7
frontend/src/Components/Form/InfoInput.css.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'message': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
@@ -1,5 +1,8 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import styles from './InfoInput.css';
|
||||
|
||||
class InfoInput extends Component {
|
||||
|
||||
@@ -7,12 +10,15 @@ class InfoInput extends Component {
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
value
|
||||
} = this.props;
|
||||
const { value } = this.props;
|
||||
|
||||
return (
|
||||
<span dangerouslySetInnerHTML={{ __html: value }} />
|
||||
<Alert
|
||||
kind={kinds.INFO}
|
||||
className={styles.message}
|
||||
>
|
||||
<span dangerouslySetInnerHTML={{ __html: value }} />
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './ModalContent.css';
|
||||
|
||||
function ModalContent(props) {
|
||||
@@ -28,6 +29,7 @@ function ModalContent(props) {
|
||||
<Icon
|
||||
name={icons.CLOSE}
|
||||
size={18}
|
||||
title={translate('Close')}
|
||||
/>
|
||||
</Link>
|
||||
}
|
||||
|
||||
@@ -78,6 +78,7 @@ class PageHeader extends Component {
|
||||
aria-label="Donate"
|
||||
to="https://prowlarr.com/donate"
|
||||
size={14}
|
||||
title={translate('Donate')}
|
||||
/>
|
||||
<IconButton
|
||||
className={styles.translate}
|
||||
|
||||
@@ -24,6 +24,7 @@ function PageHeaderActionsMenu(props) {
|
||||
<MenuButton className={styles.menuButton} aria-label="Menu Button">
|
||||
<Icon
|
||||
name={icons.INTERACTIVE}
|
||||
title={translate('Menu')}
|
||||
/>
|
||||
</MenuButton>
|
||||
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
"start_url": "../../../../",
|
||||
"theme_color": "#3a3f51",
|
||||
"background_color": "#3a3f51",
|
||||
"display": "standalone"
|
||||
"display": "minimal-ui"
|
||||
}
|
||||
|
||||
@@ -34,7 +34,8 @@ function AuthenticationRequiredModalContent(props) {
|
||||
authenticationMethod,
|
||||
authenticationRequired,
|
||||
username,
|
||||
password
|
||||
password,
|
||||
passwordConfirmation
|
||||
} = settings;
|
||||
|
||||
const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none';
|
||||
@@ -63,7 +64,7 @@ function AuthenticationRequiredModalContent(props) {
|
||||
className={styles.authRequiredAlert}
|
||||
kind={kinds.WARNING}
|
||||
>
|
||||
{translate('AuthenticationRequiredWarning', { appName: 'Prowlarr' })}
|
||||
{translate('AuthenticationRequiredWarning')}
|
||||
</Alert>
|
||||
|
||||
{
|
||||
@@ -76,7 +77,7 @@ function AuthenticationRequiredModalContent(props) {
|
||||
type={inputTypes.SELECT}
|
||||
name="authenticationMethod"
|
||||
values={authenticationMethodOptions}
|
||||
helpText={translate('AuthenticationMethodHelpText', { appName: 'Prowlarr' })}
|
||||
helpText={translate('AuthenticationMethodHelpText')}
|
||||
helpTextWarning={authenticationMethod.value === 'none' ? translate('AuthenticationMethodHelpTextWarning') : undefined}
|
||||
helpLink="https://wiki.servarr.com/prowlarr/faq#forced-authentication"
|
||||
onChange={onInputChange}
|
||||
@@ -120,6 +121,18 @@ function AuthenticationRequiredModalContent(props) {
|
||||
{...password}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('PasswordConfirmation')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.PASSWORD}
|
||||
name="passwordConfirmation"
|
||||
onChange={onInputChange}
|
||||
helpTextWarning={passwordConfirmation?.value ? undefined : translate('AuthenticationRequiredPasswordConfirmationHelpTextWarning')}
|
||||
{...passwordConfirmation}
|
||||
/>
|
||||
</FormGroup>
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import AddIndexerModalContentConnector from './AddIndexerModalContentConnector';
|
||||
import styles from './AddIndexerModal.css';
|
||||
|
||||
@@ -8,6 +9,7 @@ function AddIndexerModal({ isOpen, onModalClose, onSelectIndexer, ...otherProps
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.EXTRA_LARGE}
|
||||
onModalClose={onModalClose}
|
||||
className={styles.modal}
|
||||
>
|
||||
|
||||
@@ -16,7 +16,7 @@ import TableBody from 'Components/Table/TableBody';
|
||||
import { kinds, scrollDirections } from 'Helpers/Props';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import SelectIndexerRowConnector from './SelectIndexerRowConnector';
|
||||
import SelectIndexerRow from './SelectIndexerRow';
|
||||
import styles from './AddIndexerModalContent.css';
|
||||
|
||||
const columns = [
|
||||
@@ -49,6 +49,12 @@ const columns = [
|
||||
label: () => translate('Privacy'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'categories',
|
||||
label: () => translate('Categories'),
|
||||
isSortable: false,
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
|
||||
@@ -260,7 +266,7 @@ class AddIndexerModalContent extends Component {
|
||||
<TableBody>
|
||||
{
|
||||
filteredIndexers.map((indexer) => (
|
||||
<SelectIndexerRowConnector
|
||||
<SelectIndexerRow
|
||||
key={`${indexer.implementation}-${indexer.name}`}
|
||||
implementation={indexer.implementation}
|
||||
implementationName={indexer.implementationName}
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import { some } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchIndexerSchema, selectIndexerSchema, setIndexerSchemaSort } from 'Store/Actions/indexerActions';
|
||||
import createAllIndexersSelector from 'Store/Selectors/createAllIndexersSelector';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import AddIndexerModalContent from './AddIndexerModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createClientSideCollectionSelector('indexers.schema'),
|
||||
(indexers) => {
|
||||
createAllIndexersSelector(),
|
||||
(indexers, allIndexers) => {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
@@ -19,11 +22,19 @@ function createMapStateToProps() {
|
||||
sortKey
|
||||
} = indexers;
|
||||
|
||||
const indexerList = items.map((item) => {
|
||||
const { definitionName } = item;
|
||||
return {
|
||||
...item,
|
||||
isExistingIndexer: some(allIndexers, { definitionName })
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
indexers: items,
|
||||
indexers: indexerList,
|
||||
sortKey,
|
||||
sortDirection
|
||||
};
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRowButton from 'Components/Table/TableRowButton';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import ProtocolLabel from 'Indexer/Index/Table/ProtocolLabel';
|
||||
import firstCharToUpper from 'Utilities/String/firstCharToUpper';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './SelectIndexerRow.css';
|
||||
|
||||
class SelectIndexerRow extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onPress = () => {
|
||||
const {
|
||||
implementation,
|
||||
implementationName,
|
||||
name
|
||||
} = this.props;
|
||||
|
||||
this.props.onIndexerSelect({ implementation, implementationName, name });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
protocol,
|
||||
privacy,
|
||||
name,
|
||||
language,
|
||||
description,
|
||||
isExistingIndexer
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<TableRowButton onPress={this.onPress}>
|
||||
<TableRowCell className={styles.protocol}>
|
||||
<ProtocolLabel
|
||||
protocol={protocol}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
{name}
|
||||
{
|
||||
isExistingIndexer ?
|
||||
<Icon
|
||||
className={styles.alreadyExistsIcon}
|
||||
name={icons.CHECK_CIRCLE}
|
||||
size={15}
|
||||
title={translate('IndexerAlreadySetup')}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
{language}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
{description}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
{translate(firstCharToUpper(privacy))}
|
||||
</TableRowCell>
|
||||
</TableRowButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectIndexerRow.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
protocol: PropTypes.string.isRequired,
|
||||
privacy: PropTypes.string.isRequired,
|
||||
language: PropTypes.string.isRequired,
|
||||
description: PropTypes.string.isRequired,
|
||||
implementation: PropTypes.string.isRequired,
|
||||
implementationName: PropTypes.string.isRequired,
|
||||
onIndexerSelect: PropTypes.func.isRequired,
|
||||
isExistingIndexer: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default SelectIndexerRow;
|
||||
75
frontend/src/Indexer/Add/SelectIndexerRow.tsx
Normal file
75
frontend/src/Indexer/Add/SelectIndexerRow.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRowButton from 'Components/Table/TableRowButton';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import CapabilitiesLabel from 'Indexer/Index/Table/CapabilitiesLabel';
|
||||
import ProtocolLabel from 'Indexer/Index/Table/ProtocolLabel';
|
||||
import { IndexerCapabilities } from 'Indexer/Indexer';
|
||||
import firstCharToUpper from 'Utilities/String/firstCharToUpper';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './SelectIndexerRow.css';
|
||||
|
||||
interface SelectIndexerRowProps {
|
||||
name: string;
|
||||
protocol: string;
|
||||
privacy: string;
|
||||
language: string;
|
||||
description: string;
|
||||
capabilities: IndexerCapabilities;
|
||||
implementation: string;
|
||||
implementationName: string;
|
||||
isExistingIndexer: boolean;
|
||||
onIndexerSelect(...args: unknown[]): void;
|
||||
}
|
||||
|
||||
function SelectIndexerRow(props: SelectIndexerRowProps) {
|
||||
const {
|
||||
name,
|
||||
protocol,
|
||||
privacy,
|
||||
language,
|
||||
description,
|
||||
capabilities,
|
||||
implementation,
|
||||
implementationName,
|
||||
isExistingIndexer,
|
||||
onIndexerSelect,
|
||||
} = props;
|
||||
|
||||
const onPress = useCallback(() => {
|
||||
onIndexerSelect({ implementation, implementationName, name });
|
||||
}, [implementation, implementationName, name, onIndexerSelect]);
|
||||
|
||||
return (
|
||||
<TableRowButton onPress={onPress}>
|
||||
<TableRowCell className={styles.protocol}>
|
||||
<ProtocolLabel protocol={protocol} />
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
{name}
|
||||
{isExistingIndexer ? (
|
||||
<Icon
|
||||
className={styles.alreadyExistsIcon}
|
||||
name={icons.CHECK_CIRCLE}
|
||||
size={15}
|
||||
title={translate('IndexerAlreadySetup')}
|
||||
/>
|
||||
) : null}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>{language}</TableRowCell>
|
||||
|
||||
<TableRowCell>{description}</TableRowCell>
|
||||
|
||||
<TableRowCell>{translate(firstCharToUpper(privacy))}</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<CapabilitiesLabel capabilities={capabilities} />
|
||||
</TableRowCell>
|
||||
</TableRowButton>
|
||||
);
|
||||
}
|
||||
|
||||
export default SelectIndexerRow;
|
||||
@@ -1,18 +0,0 @@
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createExistingIndexerSelector from 'Store/Selectors/createExistingIndexerSelector';
|
||||
import SelectIndexerRow from './SelectIndexerRow';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createExistingIndexerSelector(),
|
||||
(isExistingIndexer, dimensions) => {
|
||||
return {
|
||||
isExistingIndexer
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(SelectIndexerRow);
|
||||
@@ -1,3 +1,4 @@
|
||||
import { uniqBy } from 'lodash';
|
||||
import React from 'react';
|
||||
import Label from 'Components/Label';
|
||||
import { IndexerCapabilities } from 'Indexer/Indexer';
|
||||
@@ -23,14 +24,18 @@ function CapabilitiesLabel(props: CapabilitiesLabelProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const nameList = Array.from(
|
||||
new Set(filteredList.map((item) => item.name).sort())
|
||||
const indexerCategories = uniqBy(filteredList, 'id').sort(
|
||||
(a, b) => a.id - b.id
|
||||
);
|
||||
|
||||
return (
|
||||
<span>
|
||||
{nameList.map((category) => {
|
||||
return <Label key={category}>{category}</Label>;
|
||||
{indexerCategories.map((category) => {
|
||||
return (
|
||||
<Label key={category.id} title={`${category.id}`}>
|
||||
{category.name}
|
||||
</Label>
|
||||
);
|
||||
})}
|
||||
|
||||
{filteredList.length === 0 ? <Label>{'None'}</Label> : null}
|
||||
|
||||
@@ -3,6 +3,7 @@ import React, { useCallback, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
|
||||
import { createSelector } from 'reselect';
|
||||
import Alert from 'Components/Alert';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
import DescriptionListItemDescription from 'Components/DescriptionList/DescriptionListItemDescription';
|
||||
@@ -23,7 +24,7 @@ import TagListConnector from 'Components/TagListConnector';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import DeleteIndexerModal from 'Indexer/Delete/DeleteIndexerModal';
|
||||
import EditIndexerModalConnector from 'Indexer/Edit/EditIndexerModalConnector';
|
||||
import Indexer from 'Indexer/Indexer';
|
||||
import Indexer, { IndexerCapabilities } from 'Indexer/Indexer';
|
||||
import { createIndexerSelectorForHook } from 'Store/Selectors/createIndexerSelector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import IndexerHistory from './History/IndexerHistory';
|
||||
@@ -63,7 +64,7 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
|
||||
fields,
|
||||
tags,
|
||||
protocol,
|
||||
capabilities,
|
||||
capabilities = {} as IndexerCapabilities,
|
||||
} = indexer as Indexer;
|
||||
|
||||
const { onModalClose } = props;
|
||||
@@ -207,7 +208,7 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('RawSearchSupported')}
|
||||
data={
|
||||
capabilities.supportsRawSearch
|
||||
capabilities?.supportsRawSearch
|
||||
? translate('Yes')
|
||||
: translate('No')
|
||||
}
|
||||
@@ -216,12 +217,12 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('SearchTypes')}
|
||||
data={
|
||||
capabilities.searchParams.length === 0 ? (
|
||||
translate('NotSupported')
|
||||
) : (
|
||||
capabilities?.searchParams?.length > 0 ? (
|
||||
<Label kind={kinds.PRIMARY}>
|
||||
{capabilities.searchParams[0]}
|
||||
</Label>
|
||||
) : (
|
||||
translate('NotSupported')
|
||||
)
|
||||
}
|
||||
/>
|
||||
@@ -229,60 +230,60 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('TVSearchTypes')}
|
||||
data={
|
||||
capabilities.tvSearchParams.length === 0
|
||||
? translate('NotSupported')
|
||||
: capabilities.tvSearchParams.map((p) => {
|
||||
capabilities?.tvSearchParams?.length > 0
|
||||
? capabilities.tvSearchParams.map((p) => {
|
||||
return (
|
||||
<Label key={p} kind={kinds.PRIMARY}>
|
||||
{p}
|
||||
</Label>
|
||||
);
|
||||
})
|
||||
: translate('NotSupported')
|
||||
}
|
||||
/>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('MovieSearchTypes')}
|
||||
data={
|
||||
capabilities.movieSearchParams.length === 0
|
||||
? translate('NotSupported')
|
||||
: capabilities.movieSearchParams.map((p) => {
|
||||
capabilities?.movieSearchParams?.length > 0
|
||||
? capabilities.movieSearchParams.map((p) => {
|
||||
return (
|
||||
<Label key={p} kind={kinds.PRIMARY}>
|
||||
{p}
|
||||
</Label>
|
||||
);
|
||||
})
|
||||
: translate('NotSupported')
|
||||
}
|
||||
/>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('BookSearchTypes')}
|
||||
data={
|
||||
capabilities.bookSearchParams.length === 0
|
||||
? translate('NotSupported')
|
||||
: capabilities.bookSearchParams.map((p) => {
|
||||
capabilities?.bookSearchParams?.length > 0
|
||||
? capabilities.bookSearchParams.map((p) => {
|
||||
return (
|
||||
<Label key={p} kind={kinds.PRIMARY}>
|
||||
{p}
|
||||
</Label>
|
||||
);
|
||||
})
|
||||
: translate('NotSupported')
|
||||
}
|
||||
/>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('MusicSearchTypes')}
|
||||
data={
|
||||
capabilities.musicSearchParams.length === 0
|
||||
? translate('NotSupported')
|
||||
: capabilities.musicSearchParams.map((p) => {
|
||||
capabilities?.musicSearchParams?.length > 0
|
||||
? capabilities.musicSearchParams.map((p) => {
|
||||
return (
|
||||
<Label key={p} kind={kinds.PRIMARY}>
|
||||
{p}
|
||||
</Label>
|
||||
);
|
||||
})
|
||||
: translate('NotSupported')
|
||||
}
|
||||
/>
|
||||
</DescriptionList>
|
||||
@@ -338,7 +339,11 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
|
||||
})}
|
||||
</Table>
|
||||
</FieldSet>
|
||||
) : null}
|
||||
) : (
|
||||
<Alert kind={kinds.INFO}>
|
||||
{translate('NoIndexerCategories')}
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
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';
|
||||
|
||||
function CategoryLabel({ categories }) {
|
||||
const sortedCategories = categories.filter((cat) => cat.name !== undefined).sort((c) => c.id);
|
||||
|
||||
if (categories?.length === 0) {
|
||||
return (
|
||||
<Tooltip
|
||||
anchor={<Label kind={kinds.DANGER}>Unknown</Label>}
|
||||
tooltip="Please report this issue to the GitHub as this shouldn't be happening"
|
||||
position={tooltipPositions.LEFT}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span>
|
||||
{
|
||||
sortedCategories.map((category) => {
|
||||
return (
|
||||
<Label key={category.name}>
|
||||
{category.name}
|
||||
</Label>
|
||||
);
|
||||
})
|
||||
}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
CategoryLabel.defaultProps = {
|
||||
categories: []
|
||||
};
|
||||
|
||||
CategoryLabel.propTypes = {
|
||||
categories: PropTypes.arrayOf(PropTypes.object).isRequired
|
||||
};
|
||||
|
||||
export default CategoryLabel;
|
||||
36
frontend/src/Search/Table/CategoryLabel.tsx
Normal file
36
frontend/src/Search/Table/CategoryLabel.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
import Label from 'Components/Label';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import { kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import { IndexerCategory } from 'Indexer/Indexer';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
interface CategoryLabelProps {
|
||||
categories: IndexerCategory[];
|
||||
}
|
||||
|
||||
function CategoryLabel({ categories = [] }: CategoryLabelProps) {
|
||||
if (categories?.length === 0) {
|
||||
return (
|
||||
<Tooltip
|
||||
anchor={<Label kind={kinds.DANGER}>{translate('Unknown')}</Label>}
|
||||
tooltip="Please report this issue to the GitHub as this shouldn't be happening"
|
||||
position={tooltipPositions.LEFT}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const sortedCategories = categories
|
||||
.filter((cat) => cat.name !== undefined)
|
||||
.sort((a, b) => a.id - b.id);
|
||||
|
||||
return (
|
||||
<span>
|
||||
{sortedCategories.map((category) => {
|
||||
return <Label key={category.id}>{category.name}</Label>;
|
||||
})}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export default CategoryLabel;
|
||||
13
frontend/src/Search/Table/ReleaseLinks.css
Normal file
13
frontend/src/Search/Table/ReleaseLinks.css
Normal file
@@ -0,0 +1,13 @@
|
||||
.links {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.link {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.linkLabel {
|
||||
composes: label from '~Components/Label.css';
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
9
frontend/src/Search/Table/ReleaseLinks.css.d.ts
vendored
Normal file
9
frontend/src/Search/Table/ReleaseLinks.css.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'link': string;
|
||||
'linkLabel': string;
|
||||
'links': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
90
frontend/src/Search/Table/ReleaseLinks.tsx
Normal file
90
frontend/src/Search/Table/ReleaseLinks.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import React from 'react';
|
||||
import Label from 'Components/Label';
|
||||
import Link from 'Components/Link/Link';
|
||||
import { kinds, sizes } from 'Helpers/Props';
|
||||
import { IndexerCategory } from 'Indexer/Indexer';
|
||||
import styles from './ReleaseLinks.css';
|
||||
|
||||
interface ReleaseLinksProps {
|
||||
categories: IndexerCategory[];
|
||||
imdbId?: string;
|
||||
tmdbId?: number;
|
||||
tvdbId?: number;
|
||||
tvMazeId?: number;
|
||||
}
|
||||
|
||||
function ReleaseLinks(props: ReleaseLinksProps) {
|
||||
const { categories = [], imdbId, tmdbId, tvdbId, tvMazeId } = props;
|
||||
|
||||
const categoryNames = categories
|
||||
.filter((item) => item.id < 100000)
|
||||
.map((c) => c.name);
|
||||
|
||||
return (
|
||||
<div className={styles.links}>
|
||||
{imdbId ? (
|
||||
<Link
|
||||
className={styles.link}
|
||||
to={`https://imdb.com/title/tt${imdbId.toString().padStart(7, '0')}/`}
|
||||
>
|
||||
<Label
|
||||
className={styles.linkLabel}
|
||||
kind={kinds.INFO}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
IMDb
|
||||
</Label>
|
||||
</Link>
|
||||
) : null}
|
||||
|
||||
{tmdbId ? (
|
||||
<Link
|
||||
className={styles.link}
|
||||
to={`https://www.themoviedb.org/${
|
||||
categoryNames.includes('Movies') ? 'movie' : 'tv'
|
||||
}/${tmdbId}`}
|
||||
>
|
||||
<Label
|
||||
className={styles.linkLabel}
|
||||
kind={kinds.INFO}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
TMDb
|
||||
</Label>
|
||||
</Link>
|
||||
) : null}
|
||||
|
||||
{tvdbId ? (
|
||||
<Link
|
||||
className={styles.link}
|
||||
to={`https://www.thetvdb.com/?tab=series&id=${tvdbId}`}
|
||||
>
|
||||
<Label
|
||||
className={styles.linkLabel}
|
||||
kind={kinds.INFO}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
TVDb
|
||||
</Label>
|
||||
</Link>
|
||||
) : null}
|
||||
|
||||
{tvMazeId ? (
|
||||
<Link
|
||||
className={styles.link}
|
||||
to={`https://www.tvmaze.com/shows/${tvMazeId}/_`}
|
||||
>
|
||||
<Label
|
||||
className={styles.linkLabel}
|
||||
kind={kinds.INFO}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
TV Maze
|
||||
</Label>
|
||||
</Link>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ReleaseLinks;
|
||||
@@ -63,7 +63,7 @@
|
||||
}
|
||||
|
||||
.externalLinks {
|
||||
margin: 0 2px;
|
||||
width: 22px;
|
||||
text-align: center;
|
||||
composes: button from '~Components/Link/IconButton.css';
|
||||
|
||||
color: var(--textColor);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import titleCase from 'Utilities/String/titleCase';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import CategoryLabel from './CategoryLabel';
|
||||
import Peers from './Peers';
|
||||
import ReleaseLinks from './ReleaseLinks';
|
||||
import styles from './SearchIndexRow.css';
|
||||
|
||||
function getDownloadIcon(isGrabbing, isGrabbed, grabError) {
|
||||
@@ -118,6 +119,10 @@ class SearchIndexRow extends Component {
|
||||
grabs,
|
||||
seeders,
|
||||
leechers,
|
||||
imdbId,
|
||||
tmdbId,
|
||||
tvdbId,
|
||||
tvMazeId,
|
||||
indexerFlags,
|
||||
columns,
|
||||
isGrabbing,
|
||||
@@ -343,6 +348,32 @@ class SearchIndexRow extends Component {
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
imdbId || tmdbId || tvdbId || tvMazeId ? (
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon
|
||||
className={styles.externalLinks}
|
||||
name={icons.EXTERNAL_LINK}
|
||||
size={12}
|
||||
/>
|
||||
}
|
||||
title={translate('Links')}
|
||||
body={
|
||||
<ReleaseLinks
|
||||
categories={categories}
|
||||
imdbId={imdbId}
|
||||
tmdbId={tmdbId}
|
||||
tvdbId={tvdbId}
|
||||
tvMazeId={tvMazeId}
|
||||
/>
|
||||
}
|
||||
kind={kinds.INVERSE}
|
||||
position={tooltipPositions.TOP}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
@@ -375,6 +406,10 @@ SearchIndexRow.propTypes = {
|
||||
grabs: PropTypes.number,
|
||||
seeders: PropTypes.number,
|
||||
leechers: PropTypes.number,
|
||||
imdbId: PropTypes.number,
|
||||
tmdbId: PropTypes.number,
|
||||
tvdbId: PropTypes.number,
|
||||
tvMazeId: PropTypes.number,
|
||||
indexerFlags: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onGrabPress: PropTypes.func.isRequired,
|
||||
|
||||
@@ -124,6 +124,7 @@ class SecuritySettings extends Component {
|
||||
authenticationRequired,
|
||||
username,
|
||||
password,
|
||||
passwordConfirmation,
|
||||
apiKey,
|
||||
certificateValidation
|
||||
} = settings;
|
||||
@@ -139,8 +140,8 @@ class SecuritySettings extends Component {
|
||||
type={inputTypes.SELECT}
|
||||
name="authenticationMethod"
|
||||
values={authenticationMethodOptions}
|
||||
helpText={translate('AuthenticationMethodHelpText', { appName: 'Prowlarr' })}
|
||||
helpTextWarning={translate('AuthenticationRequiredWarning', { appName: 'Prowlarr' })}
|
||||
helpText={translate('AuthenticationMethodHelpText')}
|
||||
helpTextWarning={translate('AuthenticationRequiredWarning')}
|
||||
onChange={onInputChange}
|
||||
{...authenticationMethod}
|
||||
/>
|
||||
@@ -193,6 +194,21 @@ class SecuritySettings extends Component {
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
authenticationEnabled ?
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('PasswordConfirmation')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.PASSWORD}
|
||||
name="passwordConfirmation"
|
||||
onChange={onInputChange}
|
||||
{...passwordConfirmation}
|
||||
/>
|
||||
</FormGroup> :
|
||||
null
|
||||
}
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ApiKey')}</FormLabel>
|
||||
|
||||
|
||||
@@ -176,6 +176,13 @@ class UISettings extends Component {
|
||||
helpTextWarning={translate('UILanguageHelpTextWarning')}
|
||||
onChange={onInputChange}
|
||||
{...settings.uiLanguage}
|
||||
errors={
|
||||
languages.some((language) => language.key === settings.uiLanguage.value) ?
|
||||
settings.uiLanguage.errors :
|
||||
[
|
||||
...settings.uiLanguage.errors,
|
||||
{ message: translate('InvalidUILanguage') }
|
||||
]}
|
||||
/>
|
||||
</FormGroup>
|
||||
</FieldSet>
|
||||
|
||||
@@ -3,7 +3,7 @@ import React from 'react';
|
||||
import { createAction } from 'redux-actions';
|
||||
import { batchActions } from 'redux-batched-actions';
|
||||
import Icon from 'Components/Icon';
|
||||
import { filterBuilderTypes, filterBuilderValueTypes, icons, sortDirections } from 'Helpers/Props';
|
||||
import { filterBuilderTypes, filterBuilderValueTypes, filterTypePredicates, icons, sortDirections } from 'Helpers/Props';
|
||||
import { createThunk, handleThunks } from 'Store/thunks';
|
||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
import getSectionState from 'Utilities/State/getSectionState';
|
||||
@@ -169,6 +169,18 @@ export const defaultState = {
|
||||
}
|
||||
],
|
||||
|
||||
filterPredicates: {
|
||||
peers: function(item, filterValue, type) {
|
||||
const predicate = filterTypePredicates[type];
|
||||
|
||||
const seeders = item.seeders || 0;
|
||||
const leechers = item.leechers || 0;
|
||||
const peers = seeders + leechers;
|
||||
|
||||
return predicate(peers, filterValue);
|
||||
}
|
||||
},
|
||||
|
||||
filterBuilderProps: [
|
||||
{
|
||||
name: 'title',
|
||||
|
||||
@@ -114,7 +114,7 @@ class Updates extends Component {
|
||||
/>
|
||||
|
||||
<div className={styles.message}>
|
||||
{translate('TheLatestVersionIsAlreadyInstalled', { appName: 'Prowlarr' })}
|
||||
{translate('TheLatestVersionIsAlreadyInstalled')}
|
||||
</div>
|
||||
|
||||
{
|
||||
|
||||
@@ -25,20 +25,18 @@ export async function fetchTranslations(): Promise<boolean> {
|
||||
|
||||
export default function translate(
|
||||
key: string,
|
||||
tokens?: Record<string, string | number | boolean>
|
||||
tokens: Record<string, string | number | boolean> = {}
|
||||
) {
|
||||
const translation = translations[key] || key;
|
||||
|
||||
if (tokens) {
|
||||
// Fallback to the old behaviour for translations not yet updated to use named tokens
|
||||
Object.values(tokens).forEach((value, index) => {
|
||||
tokens[index] = value;
|
||||
});
|
||||
tokens.appName = 'Prowlarr';
|
||||
|
||||
return translation.replace(/\{([a-z0-9]+?)\}/gi, (match, tokenMatch) =>
|
||||
String(tokens[tokenMatch] ?? match)
|
||||
);
|
||||
}
|
||||
// Fallback to the old behaviour for translations not yet updated to use named tokens
|
||||
Object.values(tokens).forEach((value, index) => {
|
||||
tokens[index] = value;
|
||||
});
|
||||
|
||||
return translation;
|
||||
return translation.replace(/\{([a-z0-9]+?)\}/gi, (match, tokenMatch) =>
|
||||
String(tokens[tokenMatch] ?? match)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"@fortawesome/free-solid-svg-icons": "6.4.0",
|
||||
"@fortawesome/react-fontawesome": "0.2.0",
|
||||
"@juggle/resize-observer": "3.4.0",
|
||||
"@microsoft/signalr": "6.0.21",
|
||||
"@microsoft/signalr": "6.0.25",
|
||||
"@sentry/browser": "7.51.2",
|
||||
"@sentry/integrations": "7.51.2",
|
||||
"@types/node": "18.15.11",
|
||||
@@ -105,7 +105,7 @@
|
||||
"babel-loader": "9.1.3",
|
||||
"babel-plugin-inline-classnames": "2.0.1",
|
||||
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
|
||||
"core-js": "3.32.1",
|
||||
"core-js": "3.33.0",
|
||||
"css-loader": "6.7.3",
|
||||
"css-modules-typescript-loader": "4.0.1",
|
||||
"eslint": "8.45.0",
|
||||
@@ -122,7 +122,7 @@
|
||||
"html-webpack-plugin": "5.5.1",
|
||||
"loader-utils": "^3.2.1",
|
||||
"mini-css-extract-plugin": "2.7.5",
|
||||
"postcss": "8.4.23",
|
||||
"postcss": "8.4.31",
|
||||
"postcss-color-function": "4.1.0",
|
||||
"postcss-loader": "7.3.0",
|
||||
"postcss-mixins": "9.0.4",
|
||||
@@ -141,7 +141,7 @@
|
||||
"ts-loader": "9.4.2",
|
||||
"typescript-plugin-css-modules": "5.0.1",
|
||||
"url-loader": "4.1.1",
|
||||
"webpack": "5.88.1",
|
||||
"webpack": "5.89.0",
|
||||
"webpack-cli": "5.1.4",
|
||||
"webpack-livereload-plugin": "3.0.2"
|
||||
}
|
||||
|
||||
@@ -128,6 +128,16 @@ namespace NzbDrone.Common.Test.Http
|
||||
response.Content.Should().NotBeNullOrWhiteSpace();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_throw_timeout_request()
|
||||
{
|
||||
var request = new HttpRequest($"https://{_httpBinHost}/delay/10");
|
||||
|
||||
request.RequestTimeout = new TimeSpan(0, 0, 5);
|
||||
|
||||
Assert.ThrowsAsync<WebException>(async () => await Subject.ExecuteAsync(request));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_execute_https_get()
|
||||
{
|
||||
|
||||
@@ -89,8 +89,16 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
||||
// Download Station
|
||||
[TestCase(@"webapi/entry.cgi?api=(removed)&version=2&method=login&account=01233210&passwd=mySecret&format=sid&session=DownloadStation")]
|
||||
|
||||
// Tracker Responses
|
||||
[TestCase(@"tracker"":""http://xxx.yyy/announce.php?passkey=9pr04sg601233210imaveql2tyu8xyui"",""info"":""http://xxx.yyy/info?a=b""")]
|
||||
// Announce URLs (passkeys) Magnet & Tracker
|
||||
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2f9pr04sg601233210IMAveQL2tyu8xyui%2fannounce""}")]
|
||||
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2ftracker.php%2f9pr04sg601233210IMAveQL2tyu8xyui%2fannounce""}")]
|
||||
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2fannounce%2f9pr04sg601233210IMAveQL2tyu8xyui""}")]
|
||||
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2fannounce.php%3fpasskey%3d9pr04sg601233210IMAveQL2tyu8xyui""}")]
|
||||
[TestCase(@"tracker"":""https://xxx.yyy/9pr04sg601233210IMAveQL2tyu8xyui/announce""}")]
|
||||
[TestCase(@"tracker"":""https://xxx.yyy/tracker.php/9pr04sg601233210IMAveQL2tyu8xyui/announce""}")]
|
||||
[TestCase(@"tracker"":""https://xxx.yyy/announce/9pr04sg601233210IMAveQL2tyu8xyui""}")]
|
||||
[TestCase(@"tracker"":""https://xxx.yyy/announce.php?passkey=9pr04sg601233210IMAveQL2tyu8xyui""}")]
|
||||
[TestCase(@"tracker"":""http://xxx.yyy/announce.php?passkey=9pr04sg601233210IMAveQL2tyu8xyui"",""info"":""http://xxx.yyy/info?a=b""")]
|
||||
|
||||
// BroadcastheNet
|
||||
[TestCase(@"method: ""getTorrents"", ""params"": [ ""mySecret"",")]
|
||||
|
||||
@@ -107,52 +107,59 @@ namespace NzbDrone.Common.Http.Dispatchers
|
||||
|
||||
sw.Start();
|
||||
|
||||
using var responseMessage = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cts.Token);
|
||||
try
|
||||
{
|
||||
byte[] data = null;
|
||||
|
||||
try
|
||||
using var responseMessage = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cts.Token);
|
||||
{
|
||||
if (request.ResponseStream != null && responseMessage.StatusCode == HttpStatusCode.OK)
|
||||
byte[] data = null;
|
||||
|
||||
try
|
||||
{
|
||||
await responseMessage.Content.CopyToAsync(request.ResponseStream, null, cts.Token);
|
||||
}
|
||||
else
|
||||
{
|
||||
data = await responseMessage.Content.ReadAsByteArrayAsync(cts.Token);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new WebException("Failed to read complete http response", ex, WebExceptionStatus.ReceiveFailure, null);
|
||||
}
|
||||
|
||||
var headers = responseMessage.Headers.ToNameValueCollection();
|
||||
|
||||
headers.Add(responseMessage.Content.Headers.ToNameValueCollection());
|
||||
|
||||
var responseCookies = new CookieContainer();
|
||||
|
||||
if (responseMessage.Headers.TryGetValues("Set-Cookie", out var cookieHeaders))
|
||||
{
|
||||
foreach (var responseCookieHeader in cookieHeaders)
|
||||
{
|
||||
try
|
||||
if (request.ResponseStream != null && responseMessage.StatusCode == HttpStatusCode.OK)
|
||||
{
|
||||
cookies.SetCookies(responseMessage.RequestMessage.RequestUri, responseCookieHeader);
|
||||
await responseMessage.Content.CopyToAsync(request.ResponseStream, null, cts.Token);
|
||||
}
|
||||
catch
|
||||
else
|
||||
{
|
||||
// Ignore invalid cookies
|
||||
data = await responseMessage.Content.ReadAsByteArrayAsync(cts.Token);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new WebException("Failed to read complete http response", ex, WebExceptionStatus.ReceiveFailure, null);
|
||||
}
|
||||
|
||||
var headers = responseMessage.Headers.ToNameValueCollection();
|
||||
|
||||
headers.Add(responseMessage.Content.Headers.ToNameValueCollection());
|
||||
|
||||
var responseCookies = new CookieContainer();
|
||||
|
||||
if (responseMessage.Headers.TryGetValues("Set-Cookie", out var cookieHeaders))
|
||||
{
|
||||
foreach (var responseCookieHeader in cookieHeaders)
|
||||
{
|
||||
try
|
||||
{
|
||||
cookies.SetCookies(responseMessage.RequestMessage.RequestUri, responseCookieHeader);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore invalid cookies
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var cookieCollection = cookies.GetCookies(responseMessage.RequestMessage.RequestUri);
|
||||
|
||||
sw.Stop();
|
||||
|
||||
return new HttpResponse(request, new HttpHeader(headers), cookieCollection, data, sw.ElapsedMilliseconds, responseMessage.StatusCode, responseMessage.Version);
|
||||
}
|
||||
|
||||
var cookieCollection = cookies.GetCookies(responseMessage.RequestMessage.RequestUri);
|
||||
|
||||
sw.Stop();
|
||||
|
||||
return new HttpResponse(request, new HttpHeader(headers), cookieCollection, data, sw.ElapsedMilliseconds, responseMessage.StatusCode, responseMessage.Version);
|
||||
}
|
||||
catch (OperationCanceledException ex) when (cts.IsCancellationRequested)
|
||||
{
|
||||
throw new WebException("Http request timed out", ex.InnerException, WebExceptionStatus.Timeout, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ namespace NzbDrone.Common.Instrumentation
|
||||
new (@"""/(home|Users)/(?<secret>[^/""]+?)(/|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
// 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"),
|
||||
new (@"announce(\.php)?(/|%2f|%3fpasskey%3d)(?<secret>[a-z0-9]{16,})|(?<secret>[a-z0-9]{16,})(/|%2f)announce", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
// NzbGet
|
||||
new (@"""Name""\s*:\s*""[^""]*(username|password)""\s*,\s*""Value""\s*:\s*""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
<DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DryIoc.dll" Version="5.4.1" />
|
||||
<PackageReference Include="DryIoc.dll" Version="5.4.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.2" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="NLog" Version="5.2.0" />
|
||||
<PackageReference Include="NLog.Extensions.Logging" Version="5.3.0" />
|
||||
<PackageReference Include="Npgsql" Version="7.0.4" />
|
||||
<PackageReference Include="Npgsql" Version="7.0.6" />
|
||||
<PackageReference Include="Sentry" Version="3.29.1" />
|
||||
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
|
||||
<PackageReference Include="SharpZipLib" Version="1.4.2" />
|
||||
|
||||
17
src/NzbDrone.Common/TPL/DebounceManager.cs
Normal file
17
src/NzbDrone.Common/TPL/DebounceManager.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
|
||||
namespace NzbDrone.Common.TPL
|
||||
{
|
||||
public interface IDebounceManager
|
||||
{
|
||||
Debouncer CreateDebouncer(Action action, TimeSpan debounceDuration);
|
||||
}
|
||||
|
||||
public class DebounceManager : IDebounceManager
|
||||
{
|
||||
public Debouncer CreateDebouncer(Action action, TimeSpan debounceDuration)
|
||||
{
|
||||
return new Debouncer(action, debounceDuration);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,11 @@ namespace NzbDrone.Common.TPL
|
||||
{
|
||||
public class Debouncer
|
||||
{
|
||||
private readonly Action _action;
|
||||
private readonly System.Timers.Timer _timer;
|
||||
protected readonly Action _action;
|
||||
protected readonly System.Timers.Timer _timer;
|
||||
|
||||
private volatile int _paused;
|
||||
private volatile bool _triggered;
|
||||
protected volatile int _paused;
|
||||
protected volatile bool _triggered;
|
||||
|
||||
public Debouncer(Action action, TimeSpan debounceDuration)
|
||||
{
|
||||
@@ -27,7 +27,7 @@ namespace NzbDrone.Common.TPL
|
||||
}
|
||||
}
|
||||
|
||||
public void Execute()
|
||||
public virtual void Execute()
|
||||
{
|
||||
lock (_timer)
|
||||
{
|
||||
@@ -39,7 +39,7 @@ namespace NzbDrone.Common.TPL
|
||||
}
|
||||
}
|
||||
|
||||
public void Pause()
|
||||
public virtual void Pause()
|
||||
{
|
||||
lock (_timer)
|
||||
{
|
||||
@@ -48,7 +48,7 @@ namespace NzbDrone.Common.TPL
|
||||
}
|
||||
}
|
||||
|
||||
public void Resume()
|
||||
public virtual void Resume()
|
||||
{
|
||||
lock (_timer)
|
||||
{
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common.Messaging;
|
||||
using NzbDrone.Common.TPL;
|
||||
using NzbDrone.Core.HealthCheck;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Test.Common;
|
||||
|
||||
namespace NzbDrone.Core.Test.HealthCheck
|
||||
{
|
||||
@@ -19,10 +23,10 @@ namespace NzbDrone.Core.Test.HealthCheck
|
||||
|
||||
Mocker.SetConstant<IEnumerable<IProvideHealthCheck>>(new[] { _healthCheck });
|
||||
Mocker.SetConstant<ICacheManager>(Mocker.Resolve<CacheManager>());
|
||||
Mocker.SetConstant<IDebounceManager>(Mocker.Resolve<DebounceManager>());
|
||||
|
||||
Mocker.GetMock<IServerSideNotificationService>()
|
||||
.Setup(v => v.GetServerChecks())
|
||||
.Returns(new List<Core.HealthCheck.HealthCheck>());
|
||||
Mocker.GetMock<IDebounceManager>().Setup(s => s.CreateDebouncer(It.IsAny<Action>(), It.IsAny<TimeSpan>()))
|
||||
.Returns<Action, TimeSpan>((a, t) => new MockDebouncer(a, t));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -43,7 +43,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AnimeBytesTests
|
||||
.Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get), Subject.Definition))
|
||||
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed)));
|
||||
|
||||
var releases = (await Subject.Fetch(new BasicSearchCriteria { Categories = new[] { 2000, 5000 } })).Releases;
|
||||
var releases = (await Subject.Fetch(new BasicSearchCriteria { SearchTerm = "test", Categories = new[] { 2000, 5000 } })).Releases;
|
||||
|
||||
releases.Should().HaveCount(33);
|
||||
releases.First().Should().BeOfType<TorrentInfo>();
|
||||
|
||||
@@ -51,7 +51,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
|
||||
torrentInfo.InfoUrl.Should().Be("https://avistaz.to/torrent/187240-japan-sinks-people-of-hope-2021-s01e05-720p-nf-web-dl-ddp20-x264-seikel");
|
||||
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-11-14 21:26:21"));
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-11-14 22:26:21"));
|
||||
torrentInfo.Size.Should().Be(935127615);
|
||||
torrentInfo.InfoHash.Should().Be("a879261d4e6e792402f92401141a21de70d51bf2");
|
||||
torrentInfo.MagnetUrl.Should().Be(null);
|
||||
|
||||
@@ -51,7 +51,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
|
||||
torrentInfo.InfoUrl.Should().Be("https://exoticaz.to/torrent/64040-ssis-419-my-first-experience-is-yua-mikami-from-the-day-i-lost-my-virginity-i-was-devoted-to-sex");
|
||||
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-06-11 15:04:50"));
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-06-11 10:04:50"));
|
||||
torrentInfo.Size.Should().Be(7085405541);
|
||||
torrentInfo.InfoHash.Should().Be("asdjfiasdf54asd7f4a2sdf544asdf");
|
||||
torrentInfo.MagnetUrl.Should().Be(null);
|
||||
|
||||
@@ -51,7 +51,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
|
||||
torrentInfo.InfoUrl.Should().Be("https://privatehd.to/torrent/78506-godzilla-2014-2160p-uhd-bluray-remux-hdr-hevc-atmos-triton");
|
||||
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-03-21 04:24:49"));
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-03-21 05:24:49"));
|
||||
torrentInfo.Size.Should().Be(69914591044);
|
||||
torrentInfo.InfoHash.Should().Be("a879261d4e6e792402f92401141a21de70d51bf2");
|
||||
torrentInfo.MagnetUrl.Should().Be(null);
|
||||
|
||||
@@ -55,7 +55,7 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
|
||||
torrentInfo.InfoUrl.Should().Be("https://filelist.io/details.php?id=665873");
|
||||
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2020-01-25 19:20:19"));
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2020-01-25 20:20:19"));
|
||||
torrentInfo.Size.Should().Be(8300512414);
|
||||
torrentInfo.InfoHash.Should().Be(null);
|
||||
torrentInfo.MagnetUrl.Should().Be(null);
|
||||
|
||||
@@ -57,7 +57,7 @@ namespace NzbDrone.Core.Test.IndexerTests.HDBitsTests
|
||||
var first = torrents.First() as TorrentInfo;
|
||||
|
||||
first.Guid.Should().Be("HDBits-257142");
|
||||
first.Title.Should().Be("Supernatural S10E17 1080p WEB-DL DD5.1 H.264-ECI");
|
||||
first.Title.Should().Be("Supernatural.S10E17.1080p.WEB-DL.DD5.1.H.264-ECI");
|
||||
first.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
||||
first.DownloadUrl.Should().Be("https://hdbits.org/download.php?id=257142&passkey=fakekey");
|
||||
first.InfoUrl.Should().Be("https://hdbits.org/details.php?id=257142");
|
||||
|
||||
@@ -68,5 +68,16 @@ namespace NzbDrone.Core.Test.IndexerTests
|
||||
|
||||
VerifyNoUpdate();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_record_failure_for_unknown_provider()
|
||||
{
|
||||
Subject.RecordFailure(0);
|
||||
|
||||
Mocker.GetMock<IIndexerStatusRepository>()
|
||||
.Verify(v => v.FindByProviderId(1), Times.Never);
|
||||
|
||||
VerifyNoUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,11 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
Subject.Definition = new IndexerDefinition
|
||||
{
|
||||
Name = "Newznab"
|
||||
};
|
||||
|
||||
Subject.Settings = new NewznabSettings()
|
||||
{
|
||||
BaseUrl = "http://127.0.0.1:1234/",
|
||||
|
||||
@@ -39,12 +39,12 @@ namespace NzbDrone.Core.Test.IndexerTests.OrpheusTests
|
||||
|
||||
var releases = (await Subject.Fetch(new BasicSearchCriteria { Categories = new[] { 3000 } })).Releases;
|
||||
|
||||
releases.Should().HaveCount(65);
|
||||
releases.Should().HaveCount(50);
|
||||
releases.First().Should().BeOfType<TorrentInfo>();
|
||||
|
||||
var torrentInfo = releases.First() as TorrentInfo;
|
||||
|
||||
torrentInfo.Title.Should().Be("The Beatles - Abbey Road [1969] [Album] [2.0 Mix 2019] [MP3 V2 (VBR)] [BD]");
|
||||
torrentInfo.Title.Should().Be("The Beatles - Abbey Road (1969) [Album] [2.0 Mix 2019] [MP3 V2 (VBR) / BD]");
|
||||
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
||||
torrentInfo.DownloadUrl.Should().Be("https://orpheus.network/ajax.php?action=download&id=1902448");
|
||||
torrentInfo.InfoUrl.Should().Be("https://orpheus.network/torrents.php?id=466&torrentid=1902448");
|
||||
|
||||
@@ -44,7 +44,7 @@ namespace NzbDrone.Core.Test.IndexerTests.RedactedTests
|
||||
|
||||
var torrentInfo = releases.First() as TorrentInfo;
|
||||
|
||||
torrentInfo.Title.Should().Be("Red Hot Chili Peppers - Californication [1999] [Album] [US / Reissue 2020] [FLAC 24bit Lossless] [Vinyl]");
|
||||
torrentInfo.Title.Should().Be("Red Hot Chili Peppers - Californication (1999) [Album] [US / Reissue 2020] [FLAC 24bit Lossless / Vinyl]");
|
||||
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
||||
torrentInfo.DownloadUrl.Should().Be("https://redacted.ch/ajax.php?action=download&id=3892313");
|
||||
torrentInfo.InfoUrl.Should().Be("https://redacted.ch/torrents.php?id=16720&torrentid=3892313");
|
||||
|
||||
@@ -5,7 +5,6 @@ using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Localization;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Test.Common;
|
||||
|
||||
namespace NzbDrone.Core.Test.Localization
|
||||
{
|
||||
@@ -29,19 +28,20 @@ namespace NzbDrone.Core.Test.Localization
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_get_string_in_default_dictionary_if_no_lang_exists_and_string_exists()
|
||||
public void should_get_string_in_french()
|
||||
{
|
||||
var localizedString = Subject.GetLocalizedString("BackupNow", "an");
|
||||
Mocker.GetMock<IConfigService>().Setup(m => m.UILanguage).Returns("fr");
|
||||
|
||||
localizedString.Should().Be("Backup Now");
|
||||
var localizedString = Subject.GetLocalizedString("BackupNow");
|
||||
|
||||
ExceptionVerification.ExpectedErrors(1);
|
||||
localizedString.Should().Be("Sauvegarder maintenant");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_get_string_in_default_dictionary_if_lang_empty_and_string_exists()
|
||||
public void should_get_string_in_default_dictionary_if_unknown_language_and_string_exists()
|
||||
{
|
||||
var localizedString = Subject.GetLocalizedString("BackupNow", "");
|
||||
Mocker.GetMock<IConfigService>().Setup(m => m.UILanguage).Returns("");
|
||||
var localizedString = Subject.GetLocalizedString("BackupNow");
|
||||
|
||||
localizedString.Should().Be("Backup Now");
|
||||
}
|
||||
@@ -49,7 +49,7 @@ namespace NzbDrone.Core.Test.Localization
|
||||
[Test]
|
||||
public void should_return_argument_if_string_doesnt_exists()
|
||||
{
|
||||
var localizedString = Subject.GetLocalizedString("BadString", "en");
|
||||
var localizedString = Subject.GetLocalizedString("BadString");
|
||||
|
||||
localizedString.Should().Be("BadString");
|
||||
}
|
||||
|
||||
@@ -325,6 +325,20 @@ namespace NzbDrone.Core.Configuration
|
||||
}
|
||||
}
|
||||
|
||||
public void MigrateConfigFile()
|
||||
{
|
||||
if (!File.Exists(_configFile))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// If SSL is enabled and a cert hash is still in the config file disable SSL
|
||||
if (EnableSsl && GetValue("SslCertHash", null).IsNotNullOrWhiteSpace())
|
||||
{
|
||||
SetValue("EnableSsl", false);
|
||||
}
|
||||
}
|
||||
|
||||
private void DeleteOldValues()
|
||||
{
|
||||
var xDoc = LoadConfigFile();
|
||||
@@ -396,6 +410,7 @@ namespace NzbDrone.Core.Configuration
|
||||
|
||||
public void HandleAsync(ApplicationStartedEvent message)
|
||||
{
|
||||
MigrateConfigFile();
|
||||
EnsureDefaultConfigFile();
|
||||
DeleteOldValues();
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
public interface IConnectionStringFactory
|
||||
{
|
||||
string MainDbConnectionString { get; }
|
||||
string LogDbConnectionString { get; }
|
||||
DatabaseConnectionInfo MainDbConnection { get; }
|
||||
DatabaseConnectionInfo LogDbConnection { get; }
|
||||
string GetDatabasePath(string connectionString);
|
||||
}
|
||||
|
||||
@@ -22,15 +22,15 @@ namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
_configFileProvider = configFileProvider;
|
||||
|
||||
MainDbConnectionString = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresMainDb) :
|
||||
MainDbConnection = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresMainDb) :
|
||||
GetConnectionString(appFolderInfo.GetDatabase());
|
||||
|
||||
LogDbConnectionString = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresLogDb) :
|
||||
LogDbConnection = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresLogDb) :
|
||||
GetConnectionString(appFolderInfo.GetLogDatabase());
|
||||
}
|
||||
|
||||
public string MainDbConnectionString { get; private set; }
|
||||
public string LogDbConnectionString { get; private set; }
|
||||
public DatabaseConnectionInfo MainDbConnection { get; private set; }
|
||||
public DatabaseConnectionInfo LogDbConnection { get; private set; }
|
||||
|
||||
public string GetDatabasePath(string connectionString)
|
||||
{
|
||||
@@ -39,37 +39,40 @@ namespace NzbDrone.Core.Datastore
|
||||
return connectionBuilder.DataSource;
|
||||
}
|
||||
|
||||
private static string GetConnectionString(string dbPath)
|
||||
private static DatabaseConnectionInfo GetConnectionString(string dbPath)
|
||||
{
|
||||
var connectionBuilder = new SQLiteConnectionStringBuilder();
|
||||
|
||||
connectionBuilder.DataSource = dbPath;
|
||||
connectionBuilder.CacheSize = (int)-20000;
|
||||
connectionBuilder.DateTimeKind = DateTimeKind.Utc;
|
||||
connectionBuilder.JournalMode = OsInfo.IsOsx ? SQLiteJournalModeEnum.Truncate : SQLiteJournalModeEnum.Wal;
|
||||
connectionBuilder.Pooling = true;
|
||||
connectionBuilder.Version = 3;
|
||||
var connectionBuilder = new SQLiteConnectionStringBuilder
|
||||
{
|
||||
DataSource = dbPath,
|
||||
CacheSize = (int)-20000,
|
||||
DateTimeKind = DateTimeKind.Utc,
|
||||
JournalMode = OsInfo.IsOsx ? SQLiteJournalModeEnum.Truncate : SQLiteJournalModeEnum.Wal,
|
||||
Pooling = true,
|
||||
Version = 3,
|
||||
BusyTimeout = 100
|
||||
};
|
||||
|
||||
if (OsInfo.IsOsx)
|
||||
{
|
||||
connectionBuilder.Add("Full FSync", true);
|
||||
}
|
||||
|
||||
return connectionBuilder.ConnectionString;
|
||||
return new DatabaseConnectionInfo(DatabaseType.SQLite, connectionBuilder.ConnectionString);
|
||||
}
|
||||
|
||||
private string GetPostgresConnectionString(string dbName)
|
||||
private DatabaseConnectionInfo GetPostgresConnectionString(string dbName)
|
||||
{
|
||||
var connectionBuilder = new NpgsqlConnectionStringBuilder();
|
||||
var connectionBuilder = new NpgsqlConnectionStringBuilder
|
||||
{
|
||||
Database = dbName,
|
||||
Host = _configFileProvider.PostgresHost,
|
||||
Username = _configFileProvider.PostgresUser,
|
||||
Password = _configFileProvider.PostgresPassword,
|
||||
Port = _configFileProvider.PostgresPort,
|
||||
Enlist = false
|
||||
};
|
||||
|
||||
connectionBuilder.Database = dbName;
|
||||
connectionBuilder.Host = _configFileProvider.PostgresHost;
|
||||
connectionBuilder.Username = _configFileProvider.PostgresUser;
|
||||
connectionBuilder.Password = _configFileProvider.PostgresPassword;
|
||||
connectionBuilder.Port = _configFileProvider.PostgresPort;
|
||||
connectionBuilder.Enlist = false;
|
||||
|
||||
return connectionBuilder.ConnectionString;
|
||||
return new DatabaseConnectionInfo(DatabaseType.PostgreSQL, connectionBuilder.ConnectionString);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
14
src/NzbDrone.Core/Datastore/DatabaseConnectionInfo.cs
Normal file
14
src/NzbDrone.Core/Datastore/DatabaseConnectionInfo.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
public class DatabaseConnectionInfo
|
||||
{
|
||||
public DatabaseConnectionInfo(DatabaseType databaseType, string connectionString)
|
||||
{
|
||||
DatabaseType = databaseType;
|
||||
ConnectionString = connectionString;
|
||||
}
|
||||
|
||||
public DatabaseType DatabaseType { get; internal set; }
|
||||
public string ConnectionString { get; internal set; }
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Data.Common;
|
||||
using System.Data.SQLite;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using NLog;
|
||||
using Npgsql;
|
||||
using NzbDrone.Common.Disk;
|
||||
@@ -60,22 +61,22 @@ namespace NzbDrone.Core.Datastore
|
||||
|
||||
public IDatabase Create(MigrationContext migrationContext)
|
||||
{
|
||||
string connectionString;
|
||||
DatabaseConnectionInfo connectionInfo;
|
||||
|
||||
switch (migrationContext.MigrationType)
|
||||
{
|
||||
case MigrationType.Main:
|
||||
{
|
||||
connectionString = _connectionStringFactory.MainDbConnectionString;
|
||||
CreateMain(connectionString, migrationContext);
|
||||
connectionInfo = _connectionStringFactory.MainDbConnection;
|
||||
CreateMain(connectionInfo.ConnectionString, migrationContext, connectionInfo.DatabaseType);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case MigrationType.Log:
|
||||
{
|
||||
connectionString = _connectionStringFactory.LogDbConnectionString;
|
||||
CreateLog(connectionString, migrationContext);
|
||||
connectionInfo = _connectionStringFactory.LogDbConnection;
|
||||
CreateLog(connectionInfo.ConnectionString, migrationContext, connectionInfo.DatabaseType);
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -90,14 +91,14 @@ namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
DbConnection conn;
|
||||
|
||||
if (connectionString.Contains(".db"))
|
||||
if (connectionInfo.DatabaseType == DatabaseType.SQLite)
|
||||
{
|
||||
conn = SQLiteFactory.Instance.CreateConnection();
|
||||
conn.ConnectionString = connectionString;
|
||||
conn.ConnectionString = connectionInfo.ConnectionString;
|
||||
}
|
||||
else
|
||||
{
|
||||
conn = new NpgsqlConnection(connectionString);
|
||||
conn = new NpgsqlConnection(connectionInfo.ConnectionString);
|
||||
}
|
||||
|
||||
conn.Open();
|
||||
@@ -107,12 +108,12 @@ namespace NzbDrone.Core.Datastore
|
||||
return db;
|
||||
}
|
||||
|
||||
private void CreateMain(string connectionString, MigrationContext migrationContext)
|
||||
private void CreateMain(string connectionString, MigrationContext migrationContext, DatabaseType databaseType)
|
||||
{
|
||||
try
|
||||
{
|
||||
_restoreDatabaseService.Restore();
|
||||
_migrationController.Migrate(connectionString, migrationContext);
|
||||
_migrationController.Migrate(connectionString, migrationContext, databaseType);
|
||||
}
|
||||
catch (SQLiteException e)
|
||||
{
|
||||
@@ -135,15 +136,17 @@ namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
Logger.Error(e, "Failure to connect to Postgres DB, {0} retries remaining", retryCount);
|
||||
|
||||
Thread.Sleep(5000);
|
||||
|
||||
try
|
||||
{
|
||||
_migrationController.Migrate(connectionString, migrationContext);
|
||||
_migrationController.Migrate(connectionString, migrationContext, databaseType);
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (--retryCount > 0)
|
||||
{
|
||||
System.Threading.Thread.Sleep(5000);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -162,11 +165,11 @@ namespace NzbDrone.Core.Datastore
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateLog(string connectionString, MigrationContext migrationContext)
|
||||
private void CreateLog(string connectionString, MigrationContext migrationContext, DatabaseType databaseType)
|
||||
{
|
||||
try
|
||||
{
|
||||
_migrationController.Migrate(connectionString, migrationContext);
|
||||
_migrationController.Migrate(connectionString, migrationContext, databaseType);
|
||||
}
|
||||
catch (SQLiteException e)
|
||||
{
|
||||
@@ -186,7 +189,7 @@ namespace NzbDrone.Core.Datastore
|
||||
Logger.Error("Unable to recreate logging database automatically. It will need to be removed manually.");
|
||||
}
|
||||
|
||||
_migrationController.Migrate(connectionString, migrationContext);
|
||||
_migrationController.Migrate(connectionString, migrationContext, databaseType);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(038)]
|
||||
public class indexers_freeleech_only_config_contract : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Update.Table("Indexers").Set(new { ConfigContract = "HDSpaceSettings" }).Where(new { Implementation = "HDSpace" });
|
||||
Update.Table("Indexers").Set(new { ConfigContract = "ImmortalSeedSettings" }).Where(new { Implementation = "ImmortalSeed" });
|
||||
Update.Table("Indexers").Set(new { ConfigContract = "XSpeedsSettings" }).Where(new { Implementation = "XSpeeds" });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
||||
{
|
||||
public interface IMigrationController
|
||||
{
|
||||
void Migrate(string connectionString, MigrationContext migrationContext);
|
||||
void Migrate(string connectionString, MigrationContext migrationContext, DatabaseType databaseType);
|
||||
}
|
||||
|
||||
public class MigrationController : IMigrationController
|
||||
@@ -29,7 +29,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
||||
_migrationLoggerProvider = migrationLoggerProvider;
|
||||
}
|
||||
|
||||
public void Migrate(string connectionString, MigrationContext migrationContext)
|
||||
public void Migrate(string connectionString, MigrationContext migrationContext, DatabaseType databaseType)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
||||
|
||||
ServiceProvider serviceProvider;
|
||||
|
||||
var db = connectionString.Contains(".db") ? "sqlite" : "postgres";
|
||||
var db = databaseType == DatabaseType.SQLite ? "sqlite" : "postgres";
|
||||
|
||||
serviceProvider = new ServiceCollection()
|
||||
.AddLogging(b => b.AddNLog())
|
||||
|
||||
@@ -6,6 +6,7 @@ using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Messaging;
|
||||
using NzbDrone.Common.Reflection;
|
||||
using NzbDrone.Common.TPL;
|
||||
using NzbDrone.Core.Lifecycle;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
@@ -27,35 +28,35 @@ namespace NzbDrone.Core.HealthCheck
|
||||
private readonly IProvideHealthCheck[] _startupHealthChecks;
|
||||
private readonly IProvideHealthCheck[] _scheduledHealthChecks;
|
||||
private readonly Dictionary<Type, IEventDrivenHealthCheck[]> _eventDrivenHealthChecks;
|
||||
private readonly IServerSideNotificationService _serverSideNotificationService;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly ICacheManager _cacheManager;
|
||||
private readonly Logger _logger;
|
||||
|
||||
private readonly ICached<HealthCheck> _healthCheckResults;
|
||||
private readonly HashSet<IProvideHealthCheck> _pendingHealthChecks;
|
||||
private readonly Debouncer _debounce;
|
||||
|
||||
private bool _hasRunHealthChecksAfterGracePeriod;
|
||||
private bool _isRunningHealthChecksAfterGracePeriod;
|
||||
|
||||
public HealthCheckService(IEnumerable<IProvideHealthCheck> healthChecks,
|
||||
IServerSideNotificationService serverSideNotificationService,
|
||||
IEventAggregator eventAggregator,
|
||||
ICacheManager cacheManager,
|
||||
IDebounceManager debounceManager,
|
||||
IRuntimeInfo runtimeInfo,
|
||||
Logger logger)
|
||||
{
|
||||
_healthChecks = healthChecks.ToArray();
|
||||
_serverSideNotificationService = serverSideNotificationService;
|
||||
_eventAggregator = eventAggregator;
|
||||
_cacheManager = cacheManager;
|
||||
_logger = logger;
|
||||
|
||||
_healthCheckResults = _cacheManager.GetCache<HealthCheck>(GetType());
|
||||
_healthCheckResults = cacheManager.GetCache<HealthCheck>(GetType());
|
||||
_pendingHealthChecks = new HashSet<IProvideHealthCheck>();
|
||||
_debounce = debounceManager.CreateDebouncer(ProcessHealthChecks, TimeSpan.FromSeconds(5));
|
||||
|
||||
_startupHealthChecks = _healthChecks.Where(v => v.CheckOnStartup).ToArray();
|
||||
_scheduledHealthChecks = _healthChecks.Where(v => v.CheckOnSchedule).ToArray();
|
||||
_eventDrivenHealthChecks = GetEventDrivenHealthChecks();
|
||||
_startupGracePeriodEndTime = runtimeInfo.StartTime.AddMinutes(15);
|
||||
_startupGracePeriodEndTime = runtimeInfo.StartTime + TimeSpan.FromMinutes(15);
|
||||
}
|
||||
|
||||
public List<HealthCheck> Results()
|
||||
@@ -77,63 +78,93 @@ namespace NzbDrone.Core.HealthCheck
|
||||
.ToDictionary(g => g.Key, g => g.ToArray());
|
||||
}
|
||||
|
||||
private void PerformHealthCheck(IProvideHealthCheck[] healthChecks, bool performServerChecks = false)
|
||||
private void ProcessHealthChecks()
|
||||
{
|
||||
var results = healthChecks.Select(c => c.Check())
|
||||
.ToList();
|
||||
List<IProvideHealthCheck> healthChecks;
|
||||
|
||||
if (performServerChecks)
|
||||
lock (_pendingHealthChecks)
|
||||
{
|
||||
results.AddRange(_serverSideNotificationService.GetServerChecks());
|
||||
healthChecks = _pendingHealthChecks.ToList();
|
||||
_pendingHealthChecks.Clear();
|
||||
}
|
||||
|
||||
foreach (var result in results)
|
||||
_debounce.Pause();
|
||||
|
||||
try
|
||||
{
|
||||
if (result.Type == HealthCheckResult.Ok)
|
||||
{
|
||||
var previous = _healthCheckResults.Find(result.Source.Name);
|
||||
|
||||
if (previous != null)
|
||||
var results = healthChecks.Select(c =>
|
||||
{
|
||||
_eventAggregator.PublishEvent(new HealthCheckRestoredEvent(previous, !_hasRunHealthChecksAfterGracePeriod));
|
||||
}
|
||||
_logger.Trace("Check health -> {0}", c.GetType().Name);
|
||||
var result = c.Check();
|
||||
_logger.Trace("Check health <- {0}", c.GetType().Name);
|
||||
|
||||
_healthCheckResults.Remove(result.Source.Name);
|
||||
}
|
||||
else
|
||||
return result;
|
||||
})
|
||||
.ToList();
|
||||
|
||||
foreach (var result in results)
|
||||
{
|
||||
if (_healthCheckResults.Find(result.Source.Name) == null)
|
||||
if (result.Type == HealthCheckResult.Ok)
|
||||
{
|
||||
_eventAggregator.PublishEvent(new HealthCheckFailedEvent(result, !_hasRunHealthChecksAfterGracePeriod));
|
||||
}
|
||||
var previous = _healthCheckResults.Find(result.Source.Name);
|
||||
|
||||
_healthCheckResults.Set(result.Source.Name, result);
|
||||
if (previous != null)
|
||||
{
|
||||
_eventAggregator.PublishEvent(new HealthCheckRestoredEvent(previous, !_hasRunHealthChecksAfterGracePeriod));
|
||||
}
|
||||
|
||||
_healthCheckResults.Remove(result.Source.Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_healthCheckResults.Find(result.Source.Name) == null)
|
||||
{
|
||||
_eventAggregator.PublishEvent(new HealthCheckFailedEvent(result, !_hasRunHealthChecksAfterGracePeriod));
|
||||
}
|
||||
|
||||
_healthCheckResults.Set(result.Source.Name, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_debounce.Resume();
|
||||
}
|
||||
|
||||
_eventAggregator.PublishEvent(new HealthCheckCompleteEvent());
|
||||
}
|
||||
|
||||
public void Execute(CheckHealthCommand message)
|
||||
{
|
||||
if (message.Trigger == CommandTrigger.Manual)
|
||||
var healthChecks = message.Trigger == CommandTrigger.Manual ? _healthChecks : _scheduledHealthChecks;
|
||||
|
||||
lock (_pendingHealthChecks)
|
||||
{
|
||||
PerformHealthCheck(_healthChecks, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
PerformHealthCheck(_scheduledHealthChecks, true);
|
||||
foreach (var healthCheck in healthChecks)
|
||||
{
|
||||
_pendingHealthChecks.Add(healthCheck);
|
||||
}
|
||||
}
|
||||
|
||||
ProcessHealthChecks();
|
||||
}
|
||||
|
||||
public void HandleAsync(ApplicationStartedEvent message)
|
||||
{
|
||||
PerformHealthCheck(_startupHealthChecks, true);
|
||||
lock (_pendingHealthChecks)
|
||||
{
|
||||
foreach (var healthCheck in _startupHealthChecks)
|
||||
{
|
||||
_pendingHealthChecks.Add(healthCheck);
|
||||
}
|
||||
}
|
||||
|
||||
ProcessHealthChecks();
|
||||
}
|
||||
|
||||
public void HandleAsync(IEvent message)
|
||||
{
|
||||
if (message is HealthCheckCompleteEvent)
|
||||
if (message is HealthCheckCompleteEvent || message is ApplicationStartedEvent)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -144,7 +175,16 @@ namespace NzbDrone.Core.HealthCheck
|
||||
{
|
||||
_isRunningHealthChecksAfterGracePeriod = true;
|
||||
|
||||
PerformHealthCheck(_startupHealthChecks);
|
||||
lock (_pendingHealthChecks)
|
||||
{
|
||||
foreach (var healthCheck in _startupHealthChecks)
|
||||
{
|
||||
_pendingHealthChecks.Add(healthCheck);
|
||||
}
|
||||
}
|
||||
|
||||
// Call it directly so it's not debounced and any alerts can be sent.
|
||||
ProcessHealthChecks();
|
||||
|
||||
// Update after running health checks so new failure notifications aren't sent 2x.
|
||||
_hasRunHealthChecksAfterGracePeriod = true;
|
||||
@@ -176,11 +216,16 @@ namespace NzbDrone.Core.HealthCheck
|
||||
if (eventDrivenHealthCheck.ShouldExecute(message, previouslyFailed))
|
||||
{
|
||||
filteredChecks.Add(eventDrivenHealthCheck.HealthCheck);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Add debounce
|
||||
PerformHealthCheck(filteredChecks.ToArray());
|
||||
lock (_pendingHealthChecks)
|
||||
{
|
||||
filteredChecks.ForEach(h => _pendingHealthChecks.Add(h));
|
||||
}
|
||||
|
||||
_debounce.Execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,50 +9,43 @@ using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Localization;
|
||||
|
||||
namespace NzbDrone.Core.HealthCheck
|
||||
{
|
||||
public interface IServerSideNotificationService
|
||||
{
|
||||
public List<HealthCheck> GetServerChecks();
|
||||
}
|
||||
|
||||
public class ServerSideNotificationService : IServerSideNotificationService
|
||||
public class ServerSideNotificationService : HealthCheckBase
|
||||
{
|
||||
private readonly IHttpClient _client;
|
||||
private readonly IProwlarrCloudRequestBuilder _cloudRequestBuilder;
|
||||
private readonly IConfigFileProvider _configFileProvider;
|
||||
private readonly IHttpRequestBuilderFactory _cloudRequestBuilder;
|
||||
private readonly Logger _logger;
|
||||
|
||||
private readonly ICached<List<HealthCheck>> _cache;
|
||||
private readonly ICached<HealthCheck> _cache;
|
||||
|
||||
public ServerSideNotificationService(IHttpClient client,
|
||||
IConfigFileProvider configFileProvider,
|
||||
IProwlarrCloudRequestBuilder cloudRequestBuilder,
|
||||
ICacheManager cacheManager,
|
||||
Logger logger)
|
||||
public ServerSideNotificationService(IHttpClient client, IProwlarrCloudRequestBuilder cloudRequestBuilder, IConfigFileProvider configFileProvider, ICacheManager cacheManager, ILocalizationService localizationService, Logger logger)
|
||||
: base(localizationService)
|
||||
{
|
||||
_client = client;
|
||||
_configFileProvider = configFileProvider;
|
||||
_cloudRequestBuilder = cloudRequestBuilder.Services;
|
||||
_cloudRequestBuilder = cloudRequestBuilder;
|
||||
_logger = logger;
|
||||
|
||||
_cache = cacheManager.GetCache<List<HealthCheck>>(GetType());
|
||||
_cache = cacheManager.GetCache<HealthCheck>(GetType());
|
||||
}
|
||||
|
||||
public List<HealthCheck> GetServerChecks()
|
||||
public override HealthCheck Check()
|
||||
{
|
||||
return _cache.Get("ServerChecks", RetrieveServerChecks, TimeSpan.FromHours(2));
|
||||
}
|
||||
|
||||
private List<HealthCheck> RetrieveServerChecks()
|
||||
private HealthCheck RetrieveServerChecks()
|
||||
{
|
||||
if (BuildInfo.IsDebug)
|
||||
{
|
||||
return new List<HealthCheck>();
|
||||
return new HealthCheck(GetType());
|
||||
}
|
||||
|
||||
var request = _cloudRequestBuilder.Create()
|
||||
var request = _cloudRequestBuilder.Services.Create()
|
||||
.Resource("/notification")
|
||||
.AddQueryParam("version", BuildInfo.Version)
|
||||
.AddQueryParam("os", OsInfo.Os.ToString().ToLowerInvariant())
|
||||
@@ -63,17 +56,22 @@ namespace NzbDrone.Core.HealthCheck
|
||||
|
||||
try
|
||||
{
|
||||
_logger.Trace("Getting server side health notifications");
|
||||
_logger.Trace("Getting notifications");
|
||||
|
||||
var response = _client.Execute(request);
|
||||
var result = Json.Deserialize<List<ServerNotificationResponse>>(response.Content);
|
||||
return result.Select(x => new HealthCheck(GetType(), x.Type, x.Message, x.WikiUrl)).ToList();
|
||||
|
||||
var checks = result.Select(x => new HealthCheck(GetType(), x.Type, x.Message, x.WikiUrl)).ToList();
|
||||
|
||||
// Only one health check is supported, services returns an ordered list, so use the first one
|
||||
return checks.FirstOrDefault() ?? new HealthCheck(GetType());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Failed to retrieve server notifications");
|
||||
}
|
||||
_logger.Error(ex, "Failed to retrieve notifications");
|
||||
|
||||
return new List<HealthCheck>();
|
||||
return new HealthCheck(GetType());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,19 +39,14 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
|
||||
public Task<NewznabResults> Search(NewznabRequest request, List<int> indexerIds, bool interactiveSearch)
|
||||
{
|
||||
switch (request.t)
|
||||
return request.t switch
|
||||
{
|
||||
case "movie":
|
||||
return MovieSearch(request, indexerIds, interactiveSearch);
|
||||
case "music":
|
||||
return MusicSearch(request, indexerIds, interactiveSearch);
|
||||
case "tvsearch":
|
||||
return TvSearch(request, indexerIds, interactiveSearch);
|
||||
case "book":
|
||||
return BookSearch(request, indexerIds, interactiveSearch);
|
||||
default:
|
||||
return BasicSearch(request, indexerIds, interactiveSearch);
|
||||
}
|
||||
"movie" => MovieSearch(request, indexerIds, interactiveSearch),
|
||||
"music" => MusicSearch(request, indexerIds, interactiveSearch),
|
||||
"tvsearch" => TvSearch(request, indexerIds, interactiveSearch),
|
||||
"book" => BookSearch(request, indexerIds, interactiveSearch),
|
||||
_ => BasicSearch(request, indexerIds, interactiveSearch)
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<NewznabResults> MovieSearch(NewznabRequest request, List<int> indexerIds, bool interactiveSearch)
|
||||
|
||||
@@ -91,6 +91,11 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
var cleanReleases = base.CleanupReleases(releases, searchCriteria);
|
||||
|
||||
if (searchCriteria.IsRssSearch)
|
||||
{
|
||||
cleanReleases = cleanReleases.Where(r => r.PublishDate > DateTime.Now.AddDays(-1)).ToList();
|
||||
}
|
||||
|
||||
return cleanReleases.Select(r => (ReleaseInfo)r.Clone()).ToList();
|
||||
}
|
||||
|
||||
@@ -555,6 +560,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
MinimumRatio = 1,
|
||||
MinimumSeedTime = minimumSeedTime,
|
||||
Title = fileName,
|
||||
Year = year.GetValueOrDefault(),
|
||||
InfoUrl = details.AbsoluteUri,
|
||||
Guid = guid.AbsoluteUri,
|
||||
DownloadUrl = link.AbsoluteUri,
|
||||
@@ -587,6 +593,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
MinimumRatio = 1,
|
||||
MinimumSeedTime = minimumSeedTime,
|
||||
Title = releaseTitle.Trim(),
|
||||
Year = year.GetValueOrDefault(),
|
||||
InfoUrl = details.AbsoluteUri,
|
||||
Guid = guid.AbsoluteUri,
|
||||
DownloadUrl = link.AbsoluteUri,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using AngleSharp.Html.Parser;
|
||||
using NLog;
|
||||
@@ -10,6 +11,7 @@ using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions;
|
||||
|
||||
[Obsolete("Site has shutdown")]
|
||||
public class AroLol : GazelleBase<AroLolSettings>
|
||||
{
|
||||
public override string Name => "aro.lol";
|
||||
|
||||
@@ -84,6 +84,6 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
public class AvistaZParser : AvistazParserBase
|
||||
{
|
||||
protected override string TimezoneOffset => "+02:00";
|
||||
protected override string TimezoneOffset => "+01:00";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using NLog;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Indexers.Exceptions;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
||||
@@ -15,9 +16,9 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
||||
{
|
||||
public override bool SupportsRss => true;
|
||||
public override bool SupportsSearch => true;
|
||||
public override bool SupportsPagination => true;
|
||||
public override bool SupportsPagination => false;
|
||||
public override int PageSize => 50;
|
||||
public override TimeSpan RateLimit => TimeSpan.FromSeconds(4);
|
||||
public override TimeSpan RateLimit => TimeSpan.FromSeconds(6);
|
||||
public override IndexerCapabilities Capabilities => SetCapabilities();
|
||||
protected virtual string LoginUrl => Settings.BaseUrl + "api/v1/jackett/auth";
|
||||
private IIndexerRepository _indexerRepository;
|
||||
@@ -50,21 +51,28 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
||||
return new AvistazParserBase();
|
||||
}
|
||||
|
||||
protected virtual IndexerCapabilities SetCapabilities()
|
||||
{
|
||||
return new IndexerCapabilities();
|
||||
}
|
||||
protected abstract IndexerCapabilities SetCapabilities();
|
||||
|
||||
protected override async Task DoLogin()
|
||||
{
|
||||
Settings.Token = await GetToken();
|
||||
|
||||
if (Definition.Id > 0)
|
||||
try
|
||||
{
|
||||
_indexerRepository.UpdateSettings((IndexerDefinition)Definition);
|
||||
}
|
||||
Settings.Token = await GetToken();
|
||||
|
||||
_logger.Debug("Avistaz authentication succeeded.");
|
||||
if (Definition.Id > 0)
|
||||
{
|
||||
_indexerRepository.UpdateSettings((IndexerDefinition)Definition);
|
||||
}
|
||||
|
||||
_logger.Debug("Avistaz authentication succeeded.");
|
||||
}
|
||||
catch (HttpException ex) when (ex.Response.StatusCode == HttpStatusCode.Unauthorized)
|
||||
{
|
||||
_logger.Warn(ex, "Failed to authenticate with Avistaz");
|
||||
|
||||
var jsonResponse = STJson.Deserialize<AvistazErrorResponse>(ex.Response.Content);
|
||||
throw new IndexerAuthException(jsonResponse?.Message ?? "Unauthorized request to indexer");
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
|
||||
@@ -116,11 +124,12 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
||||
Method = HttpMethod.Post
|
||||
};
|
||||
|
||||
// TODO: Change to HttpAccept.Json after they fix the issue with missing headers
|
||||
var authLoginRequest = requestBuilder
|
||||
.AddFormParameter("username", Settings.Username)
|
||||
.AddFormParameter("password", Settings.Password)
|
||||
.AddFormParameter("pid", Settings.Pid.Trim())
|
||||
.Accept(HttpAccept.Json)
|
||||
.Accept(HttpAccept.Html)
|
||||
.Build();
|
||||
|
||||
var response = await ExecuteAuth(authLoginRequest);
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
||||
{
|
||||
public class AvistazParserBase : IParseIndexerResponse
|
||||
{
|
||||
protected virtual string TimezoneOffset => "-04:00"; // Avistaz does not specify a timezone & returns server time
|
||||
protected virtual string TimezoneOffset => "-05:00"; // Avistaz does not specify a timezone & returns server time
|
||||
private readonly HashSet<string> _hdResolutions = new () { "1080p", "1080i", "720p" };
|
||||
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
@@ -69,11 +69,22 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
||||
DownloadVolumeFactor = row.DownloadMultiply,
|
||||
UploadVolumeFactor = row.UploadMultiply,
|
||||
MinimumRatio = 1,
|
||||
MinimumSeedTime = 172800, // 48 hours
|
||||
MinimumSeedTime = 259200, // 72 hours
|
||||
Languages = row.Audio?.Select(x => x.Language).ToList() ?? new List<string>(),
|
||||
Subs = row.Subtitle?.Select(x => x.Language).ToList() ?? new List<string>()
|
||||
};
|
||||
|
||||
if (row.FileSize is > 0)
|
||||
{
|
||||
var sizeGigabytes = row.FileSize.Value / Math.Pow(1024, 3);
|
||||
|
||||
release.MinimumSeedTime = sizeGigabytes switch
|
||||
{
|
||||
> 50.0 => (long)((100 * Math.Log(sizeGigabytes)) - 219.2023) * 3600,
|
||||
_ => 259200 + (long)(sizeGigabytes * 7200)
|
||||
};
|
||||
}
|
||||
|
||||
if (row.MovieTvinfo != null)
|
||||
{
|
||||
release.ImdbId = ParseUtil.GetImdbId(row.MovieTvinfo.Imdb).GetValueOrDefault();
|
||||
|
||||
@@ -20,6 +20,7 @@ using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
[Obsolete("Site has shutdown")]
|
||||
public class BB : TorrentIndexerBase<UserPassTorrentBaseSettings>
|
||||
{
|
||||
public override string Name => "BB";
|
||||
|
||||
@@ -48,6 +48,13 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
return new BeyondHDParser(Capabilities.Categories);
|
||||
}
|
||||
|
||||
protected override IList<ReleaseInfo> CleanupReleases(IEnumerable<ReleaseInfo> releases, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
var cleanReleases = base.CleanupReleases(releases, searchCriteria);
|
||||
|
||||
return FilterReleasesByQuery(cleanReleases, searchCriteria).ToList();
|
||||
}
|
||||
|
||||
private IndexerCapabilities SetCapabilities()
|
||||
{
|
||||
var caps = new IndexerCapabilities
|
||||
@@ -130,7 +137,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
if (cats.Count > 0)
|
||||
{
|
||||
body.Add("categories", string.Join(",", cats));
|
||||
body.Add("categories", cats.Select(int.Parse).ToArray());
|
||||
}
|
||||
|
||||
if (_settings.SearchTypes.Any())
|
||||
@@ -143,7 +150,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
if (searchTypes.Any())
|
||||
{
|
||||
body.Add("types", string.Join(",", searchTypes));
|
||||
body.Add("types", searchTypes.ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,9 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
|
||||
public class BroadcastheNet : TorrentIndexerBase<BroadcastheNetSettings>
|
||||
{
|
||||
public override string Name => "BroadcasTheNet";
|
||||
|
||||
public override string[] IndexerUrls => new[] { "https://api.broadcasthe.net/" };
|
||||
public override string[] LegacyUrls => new[] { "http://api.broadcasthe.net/" };
|
||||
public override string Description => "BroadcasTheNet (BTN) is an invite-only torrent tracker focused on TV shows";
|
||||
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
|
||||
public override bool SupportsRss => true;
|
||||
public override bool SupportsSearch => true;
|
||||
@@ -18,11 +20,6 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
|
||||
public override IndexerCapabilities Capabilities => SetCapabilities();
|
||||
public override TimeSpan RateLimit => TimeSpan.FromSeconds(5);
|
||||
|
||||
public override string[] IndexerUrls => new string[] { "https://api.broadcasthe.net/" };
|
||||
public override string[] LegacyUrls => new string[] { "http://api.broadcasthe.net/" };
|
||||
|
||||
public override string Description => "BroadcasTheNet (BTN) is an invite-only torrent tracker focused on TV shows";
|
||||
|
||||
public BroadcastheNet(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
|
||||
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
|
||||
{
|
||||
@@ -30,18 +27,7 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
|
||||
|
||||
public override IIndexerRequestGenerator GetRequestGenerator()
|
||||
{
|
||||
var requestGenerator = new BroadcastheNetRequestGenerator() { Settings = Settings, PageSize = PageSize, Capabilities = Capabilities };
|
||||
|
||||
var releaseInfo = _indexerStatusService.GetLastRssSyncReleaseInfo(Definition.Id);
|
||||
if (releaseInfo != null)
|
||||
{
|
||||
if (int.TryParse(releaseInfo.Guid.Replace("BTN-", string.Empty), out var torrentId))
|
||||
{
|
||||
requestGenerator.LastRecentTorrentID = torrentId;
|
||||
}
|
||||
}
|
||||
|
||||
return requestGenerator;
|
||||
return new BroadcastheNetRequestGenerator { Settings = Settings, Capabilities = Capabilities, PageSize = PageSize };
|
||||
}
|
||||
|
||||
public override IParseIndexerResponse GetParser()
|
||||
@@ -65,7 +51,7 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
|
||||
caps.Categories.AddCategoryMapping("720p", NewznabStandardCategory.TVHD, "720p");
|
||||
caps.Categories.AddCategoryMapping("1080p", NewznabStandardCategory.TVHD, "1080p");
|
||||
caps.Categories.AddCategoryMapping("1080i", NewznabStandardCategory.TVHD, "1080i");
|
||||
caps.Categories.AddCategoryMapping("2160p", NewznabStandardCategory.TVHD, "2160p");
|
||||
caps.Categories.AddCategoryMapping("2160p", NewznabStandardCategory.TVUHD, "2160p");
|
||||
caps.Categories.AddCategoryMapping("Portable Device", NewznabStandardCategory.TVSD, "Portable Device");
|
||||
|
||||
return caps;
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
|
||||
|
||||
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
||||
{
|
||||
var results = new List<ReleaseInfo>();
|
||||
var releaseInfos = new List<ReleaseInfo>();
|
||||
var indexerHttpResponse = indexerResponse.HttpResponse;
|
||||
|
||||
switch (indexerHttpResponse.StatusCode)
|
||||
@@ -69,67 +69,76 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
|
||||
|
||||
if (jsonResponse.Result.Results == 0 || jsonResponse.Result?.Torrents?.Values == null)
|
||||
{
|
||||
return results;
|
||||
return releaseInfos;
|
||||
}
|
||||
|
||||
var protocol = indexerResponse.HttpRequest.Url.Scheme + ":";
|
||||
|
||||
foreach (var torrent in jsonResponse.Result.Torrents.Values)
|
||||
{
|
||||
var torrentInfo = new TorrentInfo();
|
||||
var flags = new HashSet<IndexerFlag>();
|
||||
|
||||
torrentInfo.Guid = string.Format("BTN-{0}", torrent.TorrentID);
|
||||
torrentInfo.Title = CleanReleaseName(torrent.ReleaseName);
|
||||
torrentInfo.Size = torrent.Size;
|
||||
torrentInfo.DownloadUrl = RegexProtocol.Replace(torrent.DownloadURL, protocol);
|
||||
torrentInfo.InfoUrl = string.Format("{0}//broadcasthe.net/torrents.php?id={1}&torrentid={2}", protocol, torrent.GroupID, torrent.TorrentID);
|
||||
|
||||
//torrentInfo.CommentUrl =
|
||||
if (torrent.TvdbID.HasValue)
|
||||
if (torrent.Origin.ToUpperInvariant() == "INTERNAL")
|
||||
{
|
||||
torrentInfo.TvdbId = torrent.TvdbID.Value;
|
||||
flags.Add(IndexerFlag.Internal);
|
||||
}
|
||||
|
||||
if (torrent.TvrageID.HasValue)
|
||||
var releaseInfo = new TorrentInfo
|
||||
{
|
||||
torrentInfo.TvRageId = torrent.TvrageID.Value;
|
||||
Guid = $"BTN-{torrent.TorrentID}",
|
||||
InfoUrl = $"{protocol}//broadcasthe.net/torrents.php?id={torrent.GroupID}&torrentid={torrent.TorrentID}",
|
||||
DownloadUrl = RegexProtocol.Replace(torrent.DownloadURL, protocol),
|
||||
Title = CleanReleaseName(torrent.ReleaseName),
|
||||
Categories = _categories.MapTrackerCatToNewznab(torrent.Resolution),
|
||||
InfoHash = torrent.InfoHash,
|
||||
Size = torrent.Size,
|
||||
Grabs = torrent.Snatched,
|
||||
Seeders = torrent.Seeders,
|
||||
Peers = torrent.Leechers + torrent.Seeders,
|
||||
PublishDate = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc).ToUniversalTime().AddSeconds(torrent.Time),
|
||||
Origin = torrent.Origin,
|
||||
Source = torrent.Source,
|
||||
Container = torrent.Container,
|
||||
Codec = torrent.Codec,
|
||||
Resolution = torrent.Resolution,
|
||||
Scene = torrent.Origin.ToUpperInvariant() == "SCENE",
|
||||
IndexerFlags = flags,
|
||||
DownloadVolumeFactor = 0,
|
||||
UploadVolumeFactor = 1,
|
||||
MinimumRatio = 1,
|
||||
MinimumSeedTime = torrent.Category.ToUpperInvariant() == "SEASON" ? 432000 : 86400, // 120 hours for seasons and 24 hours for episodes
|
||||
};
|
||||
|
||||
if (torrent.TvdbID is > 0)
|
||||
{
|
||||
releaseInfo.TvdbId = torrent.TvdbID.Value;
|
||||
}
|
||||
|
||||
torrentInfo.PublishDate = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc).ToUniversalTime().AddSeconds(torrent.Time);
|
||||
if (torrent.TvrageID is > 0)
|
||||
{
|
||||
releaseInfo.TvRageId = torrent.TvrageID.Value;
|
||||
}
|
||||
|
||||
//torrentInfo.MagnetUrl =
|
||||
torrentInfo.InfoHash = torrent.InfoHash;
|
||||
torrentInfo.Seeders = torrent.Seeders;
|
||||
torrentInfo.Peers = torrent.Leechers + torrent.Seeders;
|
||||
|
||||
torrentInfo.Origin = torrent.Origin;
|
||||
torrentInfo.Source = torrent.Source;
|
||||
torrentInfo.Container = torrent.Container;
|
||||
torrentInfo.Codec = torrent.Codec;
|
||||
torrentInfo.Resolution = torrent.Resolution;
|
||||
torrentInfo.UploadVolumeFactor = 1;
|
||||
torrentInfo.DownloadVolumeFactor = 0;
|
||||
torrentInfo.MinimumRatio = 1;
|
||||
|
||||
torrentInfo.Categories = _categories.MapTrackerCatToNewznab(torrent.Resolution);
|
||||
if (torrent.ImdbID.IsNotNullOrWhiteSpace() && int.TryParse(torrent.ImdbID, out var imdbId))
|
||||
{
|
||||
releaseInfo.ImdbId = imdbId;
|
||||
}
|
||||
|
||||
// Default to TV if category could not be mapped
|
||||
if (torrentInfo.Categories == null || !torrentInfo.Categories.Any())
|
||||
if (releaseInfo.Categories == null || !releaseInfo.Categories.Any())
|
||||
{
|
||||
torrentInfo.Categories = new List<IndexerCategory> { NewznabStandardCategory.TV };
|
||||
releaseInfo.Categories = new List<IndexerCategory> { NewznabStandardCategory.TV };
|
||||
}
|
||||
|
||||
results.Add(torrentInfo);
|
||||
releaseInfos.Add(releaseInfo);
|
||||
}
|
||||
|
||||
return results;
|
||||
return releaseInfos;
|
||||
}
|
||||
|
||||
private string CleanReleaseName(string releaseName)
|
||||
{
|
||||
releaseName = releaseName.Replace("\\", "");
|
||||
|
||||
return releaseName;
|
||||
return releaseName.Replace("\\", "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,31 +9,13 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
|
||||
{
|
||||
public class BroadcastheNetRequestGenerator : IIndexerRequestGenerator
|
||||
{
|
||||
public int MaxPages { get; set; }
|
||||
public int PageSize { get; set; }
|
||||
public BroadcastheNetSettings Settings { get; set; }
|
||||
public IndexerCapabilities Capabilities { get; set; }
|
||||
|
||||
public int? LastRecentTorrentID { get; set; }
|
||||
public int PageSize { get; set; }
|
||||
|
||||
public Func<IDictionary<string, string>> GetCookies { get; set; }
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
|
||||
public BroadcastheNetRequestGenerator()
|
||||
{
|
||||
MaxPages = 10;
|
||||
PageSize = 100;
|
||||
}
|
||||
|
||||
private IEnumerable<IndexerRequest> GetPagedRequests(BroadcastheNetTorrentQuery parameters, int results, int offset)
|
||||
{
|
||||
var builder = new JsonRpcRequestBuilder(Settings.BaseUrl)
|
||||
.Call("getTorrents", Settings.ApiKey, parameters, results, offset);
|
||||
builder.SuppressHttpError = true;
|
||||
|
||||
yield return new IndexerRequest(builder.Build());
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
{
|
||||
return new IndexerPageableRequestChain();
|
||||
@@ -132,11 +114,24 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
|
||||
|
||||
var btnOffset = searchCriteria.Offset.GetValueOrDefault(0);
|
||||
|
||||
parameters.Search = searchString.Replace(" ", "%");
|
||||
if (searchString.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
parameters.Search = searchString.Replace(" ", "%");
|
||||
}
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(parameters, btnResults, btnOffset));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
private IEnumerable<IndexerRequest> GetPagedRequests(BroadcastheNetTorrentQuery parameters, int results, int offset)
|
||||
{
|
||||
var builder = new JsonRpcRequestBuilder(Settings.BaseUrl)
|
||||
.Call("getTorrents", Settings.ApiKey, parameters, results, offset);
|
||||
|
||||
builder.SuppressHttpError = true;
|
||||
|
||||
yield return new IndexerRequest(builder.Build());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Newtonsoft.Json;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.BroadcastheNet
|
||||
{
|
||||
@@ -13,15 +14,15 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public string Search { get; set; }
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public string Codec { get; set; }
|
||||
public IEnumerable<string> Codec { get; set; }
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public string Container { get; set; }
|
||||
public IEnumerable<string> Container { get; set; }
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public string Source { get; set; }
|
||||
public IEnumerable<string> Source { get; set; }
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public string Resolution { get; set; }
|
||||
public IEnumerable<string> Resolution { get; set; }
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public string Origin { get; set; }
|
||||
public IEnumerable<string> Origin { get; set; }
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public string Hash { get; set; }
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
|
||||
@@ -292,56 +292,60 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann
|
||||
foreach (var setting in _definition.Settings)
|
||||
{
|
||||
var name = ".Config." + setting.Name;
|
||||
var value = Settings.ExtraFieldData.GetValueOrDefault(setting.Name, setting.Default);
|
||||
|
||||
if ((setting.Type != "password" && setting.Name != "apikey" && setting.Name != "rsskey") && indexerLogging)
|
||||
object defaultValue = setting.Type switch
|
||||
{
|
||||
"select" => setting.Options.OrderBy(x => x.Key).Select(x => x.Key).ToList().IndexOf(setting.Default).ToString().ParseInt64() ?? 0,
|
||||
_ => setting.Default
|
||||
};
|
||||
|
||||
var value = Settings.ExtraFieldData.GetValueOrDefault(setting.Name, defaultValue);
|
||||
|
||||
if (indexerLogging && setting.Type != "password" && setting.Name != "apikey" && setting.Name != "rsskey")
|
||||
{
|
||||
_logger.Trace($"{name} got value {value.ToJson()}");
|
||||
}
|
||||
|
||||
if (setting.Type == "text" || setting.Type == "password")
|
||||
switch (setting.Type)
|
||||
{
|
||||
variables[name] = value;
|
||||
}
|
||||
else if (setting.Type == "checkbox")
|
||||
{
|
||||
if (value is string stringValue && bool.TryParse(stringValue, out var result))
|
||||
{
|
||||
value = result;
|
||||
}
|
||||
case "text":
|
||||
case "password":
|
||||
variables[name] = value;
|
||||
break;
|
||||
case "checkbox":
|
||||
if (value is string stringValue && bool.TryParse(stringValue, out var result))
|
||||
{
|
||||
value = result;
|
||||
}
|
||||
|
||||
variables[name] = (bool)value ? ".True" : null;
|
||||
}
|
||||
else if (setting.Type == "select")
|
||||
{
|
||||
if (indexerLogging)
|
||||
{
|
||||
_logger.Trace($"Setting options: {setting.Options.ToJson()}");
|
||||
}
|
||||
variables[name] = (bool)value ? ".True" : null;
|
||||
break;
|
||||
case "select":
|
||||
if (indexerLogging)
|
||||
{
|
||||
_logger.Trace($"Setting options: {setting.Options.ToJson()}");
|
||||
}
|
||||
|
||||
var sorted = setting.Options.OrderBy(x => x.Key).ToList();
|
||||
var selected = sorted[(int)(long)value];
|
||||
var sorted = setting.Options.OrderBy(x => x.Key).ToList();
|
||||
var selected = sorted[(int)(long)value];
|
||||
|
||||
if (indexerLogging)
|
||||
{
|
||||
_logger.Debug($"Selected option: {selected.ToJson()}");
|
||||
}
|
||||
if (indexerLogging)
|
||||
{
|
||||
_logger.Debug($"Selected option: {selected.ToJson()}");
|
||||
}
|
||||
|
||||
variables[name] = selected.Key;
|
||||
}
|
||||
else if (setting.Type == "info")
|
||||
{
|
||||
variables[name] = value;
|
||||
}
|
||||
else if (setting.Type == "cardigannCaptcha")
|
||||
{
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException($"Type {setting.Type} is not supported.");
|
||||
variables[name] = selected.Key;
|
||||
break;
|
||||
case "info":
|
||||
variables[name] = value;
|
||||
break;
|
||||
case "cardigannCaptcha":
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException($"Type {setting.Type} is not supported.");
|
||||
}
|
||||
|
||||
if (setting.Type != "password" && setting.Name != "apikey" && setting.Name != "rsskey" && indexerLogging && variables.ContainsKey(name))
|
||||
if (indexerLogging && setting.Type != "password" && setting.Name != "apikey" && setting.Name != "rsskey" && variables.ContainsKey(name))
|
||||
{
|
||||
_logger.Debug($"Setting {setting.Name} to {variables[name].ToJson()}");
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ namespace NzbDrone.Core.Indexers.Definitions;
|
||||
public class DICMusic : GazelleBase<DICMusicSettings>
|
||||
{
|
||||
public override string Name => "DICMusic";
|
||||
public override string[] IndexerUrls => new[] { "https://dicmusic.club/" };
|
||||
public override string[] IndexerUrls => new[] { "https://dicmusic.com/" };
|
||||
public override string[] LegacyUrls => new[] { "https://dicmusic.club/" };
|
||||
public override string Description => "DICMusic is a CHINESE Private Torrent Tracker for MUSIC";
|
||||
public override string Language => "zh-CN";
|
||||
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
|
||||
|
||||
@@ -54,6 +54,8 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
private readonly IndexerCapabilitiesCategories _categories;
|
||||
|
||||
protected override string TimezoneOffset => "+01:00";
|
||||
|
||||
public ExoticaZParser(IndexerCapabilitiesCategories categories)
|
||||
{
|
||||
_categories = categories;
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions.FileList;
|
||||
|
||||
public class FileList : TorrentIndexerBase<FileListSettings>
|
||||
{
|
||||
public override string Name => "FileList.io";
|
||||
public override string[] IndexerUrls => new[]
|
||||
public override string[] IndexerUrls => new[] { "https://filelist.io/" };
|
||||
public override string[] LegacyUrls => new[]
|
||||
{
|
||||
"https://filelist.io/",
|
||||
"https://filelist.io",
|
||||
"https://flro.org/"
|
||||
};
|
||||
public override string[] LegacyUrls => new[] { "https://filelist.io" };
|
||||
public override string Description => "FileList (FL) is a ROMANIAN Private Torrent Tracker for 0DAY / GENERAL";
|
||||
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
|
||||
public override bool SupportsRss => true;
|
||||
@@ -40,6 +43,13 @@ public class FileList : TorrentIndexerBase<FileListSettings>
|
||||
return new FileListParser(Settings, Capabilities.Categories);
|
||||
}
|
||||
|
||||
protected override IList<ReleaseInfo> CleanupReleases(IEnumerable<ReleaseInfo> releases, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
var cleanReleases = base.CleanupReleases(releases, searchCriteria);
|
||||
|
||||
return FilterReleasesByQuery(cleanReleases, searchCriteria).ToList();
|
||||
}
|
||||
|
||||
private IndexerCapabilities SetCapabilities()
|
||||
{
|
||||
var caps = new IndexerCapabilities
|
||||
|
||||
@@ -72,7 +72,7 @@ public class FileListParser : IParseIndexerResponse
|
||||
InfoUrl = GetInfoUrl(id),
|
||||
Seeders = row.Seeders,
|
||||
Peers = row.Leechers + row.Seeders,
|
||||
PublishDate = DateTime.Parse(row.UploadDate + " +0300", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal),
|
||||
PublishDate = DateTime.Parse(row.UploadDate + " +0200", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal),
|
||||
Description = row.SmallDescription,
|
||||
Genres = row.SmallDescription.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).ToList(),
|
||||
ImdbId = imdbId,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
@@ -25,6 +26,31 @@ public class FileListRequestGenerator : IIndexerRequestGenerator
|
||||
{
|
||||
parameters.Set("action", "search-torrents");
|
||||
|
||||
var searchQuery = searchCriteria.SanitizedSearchTerm.Trim();
|
||||
|
||||
if (DateTime.TryParseExact($"{searchCriteria.Season} {searchCriteria.Episode}", "yyyy MM/dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var showDate))
|
||||
{
|
||||
if (searchCriteria.ImdbId.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
// Skip ID searches for daily episodes
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
searchQuery = $"{searchQuery} {showDate:yyyy.MM.dd}".Trim();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (searchCriteria.Season.HasValue)
|
||||
{
|
||||
parameters.Set("season", searchCriteria.Season.ToString());
|
||||
}
|
||||
|
||||
if (searchCriteria.Episode.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
parameters.Set("episode", searchCriteria.Episode);
|
||||
}
|
||||
}
|
||||
|
||||
if (searchCriteria.ImdbId.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
parameters.Set("type", "imdb");
|
||||
@@ -33,17 +59,7 @@ public class FileListRequestGenerator : IIndexerRequestGenerator
|
||||
else if (searchCriteria.SearchTerm.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
parameters.Set("type", "name");
|
||||
parameters.Set("query", searchCriteria.SanitizedSearchTerm.Trim());
|
||||
}
|
||||
|
||||
if (searchCriteria.Season.HasValue)
|
||||
{
|
||||
parameters.Set("season", searchCriteria.Season.ToString());
|
||||
}
|
||||
|
||||
if (searchCriteria.Episode.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
parameters.Set("episode", searchCriteria.Episode);
|
||||
parameters.Set("query", searchQuery);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ public class GazelleTorrent
|
||||
public string Category { get; set; }
|
||||
public bool IsFreeLeech { get; set; }
|
||||
public bool IsNeutralLeech { get; set; }
|
||||
public bool IsFreeload { get; set; }
|
||||
public bool IsPersonalFreeLeech { get; set; }
|
||||
public bool CanUseToken { get; set; }
|
||||
}
|
||||
@@ -63,6 +64,7 @@ public class GazelleRelease
|
||||
public List<GazelleTorrent> Torrents { get; set; }
|
||||
public bool IsFreeLeech { get; set; }
|
||||
public bool IsNeutralLeech { get; set; }
|
||||
public bool IsFreeload { get; set; }
|
||||
public bool IsPersonalFreeLeech { get; set; }
|
||||
public bool CanUseToken { get; set; }
|
||||
}
|
||||
|
||||
@@ -12,9 +12,10 @@ namespace NzbDrone.Core.Indexers.Definitions.HDBits
|
||||
public override string[] LegacyUrls => new[] { "https://hdbits.org" };
|
||||
public override string Description => "Best HD Tracker";
|
||||
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
|
||||
public override IndexerCapabilities Capabilities => SetCapabilities();
|
||||
public override bool SupportsRedirect => true;
|
||||
public override int PageSize => 30;
|
||||
public override bool SupportsPagination => true;
|
||||
public override int PageSize => 100;
|
||||
public override IndexerCapabilities Capabilities => SetCapabilities();
|
||||
|
||||
public HDBits(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
|
||||
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
|
||||
@@ -45,14 +46,14 @@ namespace NzbDrone.Core.Indexers.Definitions.HDBits
|
||||
}
|
||||
};
|
||||
|
||||
caps.Categories.AddCategoryMapping(6, NewznabStandardCategory.Audio, "Audio Track");
|
||||
caps.Categories.AddCategoryMapping(3, NewznabStandardCategory.TVDocumentary, "Documentary");
|
||||
caps.Categories.AddCategoryMapping(8, NewznabStandardCategory.Other, "Misc/Demo");
|
||||
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.Movies, "Movie");
|
||||
caps.Categories.AddCategoryMapping(2, NewznabStandardCategory.TV, "TV");
|
||||
caps.Categories.AddCategoryMapping(3, NewznabStandardCategory.TVDocumentary, "Documentary");
|
||||
caps.Categories.AddCategoryMapping(4, NewznabStandardCategory.Audio, "Music");
|
||||
caps.Categories.AddCategoryMapping(5, NewznabStandardCategory.TVSport, "Sport");
|
||||
caps.Categories.AddCategoryMapping(2, NewznabStandardCategory.TV, "TV");
|
||||
caps.Categories.AddCategoryMapping(6, NewznabStandardCategory.Audio, "Audio Track");
|
||||
caps.Categories.AddCategoryMapping(7, NewznabStandardCategory.XXX, "XXX");
|
||||
caps.Categories.AddCategoryMapping(8, NewznabStandardCategory.Other, "Misc/Demo");
|
||||
|
||||
return caps;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions.HDBits
|
||||
@@ -7,19 +8,15 @@ namespace NzbDrone.Core.Indexers.Definitions.HDBits
|
||||
{
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
public string Username { get; set; }
|
||||
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
public string Passkey { get; set; }
|
||||
|
||||
public string Hash { get; set; }
|
||||
|
||||
public string Search { get; set; }
|
||||
|
||||
public int[] Category { get; set; }
|
||||
|
||||
public int[] Codec { get; set; }
|
||||
|
||||
public int[] Medium { get; set; }
|
||||
|
||||
public IEnumerable<int> Category { get; set; }
|
||||
public IEnumerable<int> Codec { get; set; }
|
||||
public IEnumerable<int> Medium { get; set; }
|
||||
public int? Origin { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "imdb")]
|
||||
@@ -33,13 +30,9 @@ namespace NzbDrone.Core.Indexers.Definitions.HDBits
|
||||
|
||||
[JsonProperty(PropertyName = "snatched_only")]
|
||||
public bool? SnatchedOnly { get; set; }
|
||||
|
||||
public int? Limit { get; set; }
|
||||
public int? Page { get; set; }
|
||||
|
||||
public TorrentQuery Clone()
|
||||
{
|
||||
return MemberwiseClone() as TorrentQuery;
|
||||
}
|
||||
}
|
||||
|
||||
public class HDBitsResponse
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Indexers.Exceptions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
@@ -14,6 +15,8 @@ namespace NzbDrone.Core.Indexers.Definitions.HDBits
|
||||
private readonly HDBitsSettings _settings;
|
||||
private readonly IndexerCapabilitiesCategories _categories;
|
||||
|
||||
private readonly List<int> _halfLeechMediums = new () { (int)HdBitsMedium.Bluray, (int)HdBitsMedium.Remux, (int)HdBitsMedium.Capture };
|
||||
|
||||
public HDBitsParser(HDBitsSettings settings, IndexerCapabilitiesCategories categories)
|
||||
{
|
||||
_settings = settings;
|
||||
@@ -22,7 +25,6 @@ namespace NzbDrone.Core.Indexers.Definitions.HDBits
|
||||
|
||||
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
||||
{
|
||||
var torrentInfos = new List<ReleaseInfo>();
|
||||
var indexerHttpResponse = indexerResponse.HttpResponse;
|
||||
|
||||
if (indexerHttpResponse.StatusCode == HttpStatusCode.Forbidden)
|
||||
@@ -42,18 +44,25 @@ namespace NzbDrone.Core.Indexers.Definitions.HDBits
|
||||
throw new IndexerException(indexerResponse, "HDBits API request returned status code {0}: {1}", jsonResponse.Status, jsonResponse.Message ?? string.Empty);
|
||||
}
|
||||
|
||||
var responseData = jsonResponse.Data as JArray;
|
||||
if (responseData == null)
|
||||
if (jsonResponse.Data is not JArray responseData)
|
||||
{
|
||||
throw new IndexerException(indexerResponse, "Indexer API call response missing result data");
|
||||
}
|
||||
|
||||
var releaseInfos = new List<ReleaseInfo>();
|
||||
|
||||
var queryResults = responseData.ToObject<TorrentQueryResponse[]>();
|
||||
|
||||
foreach (var result in queryResults)
|
||||
{
|
||||
// Skip non-freeleech results when freeleech only is set
|
||||
if (_settings.FreeleechOnly && result.FreeLeech != "yes")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var id = result.Id;
|
||||
var internalRelease = result.TypeOrigin == 1 ? true : false;
|
||||
var internalRelease = result.TypeOrigin == 1;
|
||||
|
||||
var flags = new HashSet<IndexerFlag>();
|
||||
|
||||
@@ -62,10 +71,10 @@ namespace NzbDrone.Core.Indexers.Definitions.HDBits
|
||||
flags.Add(IndexerFlag.Internal);
|
||||
}
|
||||
|
||||
torrentInfos.Add(new HDBitsInfo()
|
||||
releaseInfos.Add(new HDBitsInfo
|
||||
{
|
||||
Guid = string.Format("HDBits-{0}", id),
|
||||
Title = result.Name,
|
||||
Guid = $"HDBits-{id}",
|
||||
Title = GetTitle(result),
|
||||
Size = result.Size,
|
||||
Categories = _categories.MapTrackerCatToNewznab(result.TypeCategory.ToString()),
|
||||
InfoHash = result.Hash,
|
||||
@@ -77,19 +86,55 @@ namespace NzbDrone.Core.Indexers.Definitions.HDBits
|
||||
Peers = result.Leechers + result.Seeders,
|
||||
PublishDate = result.Added.ToUniversalTime(),
|
||||
Internal = internalRelease,
|
||||
Year = result.ImdbInfo?.Year ?? 0,
|
||||
ImdbId = result.ImdbInfo?.Id ?? 0,
|
||||
TvdbId = result.TvdbInfo?.Id ?? 0,
|
||||
DownloadVolumeFactor = result.FreeLeech == "yes" ? 0 : 1,
|
||||
UploadVolumeFactor = 1,
|
||||
DownloadVolumeFactor = GetDownloadVolumeFactor(result),
|
||||
UploadVolumeFactor = GetUploadVolumeFactor(result),
|
||||
IndexerFlags = flags
|
||||
});
|
||||
}
|
||||
|
||||
return torrentInfos.ToArray();
|
||||
return releaseInfos.ToArray();
|
||||
}
|
||||
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
|
||||
private string GetTitle(TorrentQueryResponse item)
|
||||
{
|
||||
return _settings.UseFilenames && item.FileName.IsNotNullOrWhiteSpace()
|
||||
? item.FileName.Replace(".torrent", "", StringComparison.InvariantCultureIgnoreCase)
|
||||
: item.Name;
|
||||
}
|
||||
|
||||
private double GetDownloadVolumeFactor(TorrentQueryResponse item)
|
||||
{
|
||||
if (item.FreeLeech == "yes")
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 100% Neutral Leech: all XXX content.
|
||||
if (item.TypeCategory == 7)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 50% Free Leech: all full discs, remuxes, captures and all internal encodes, also all TV and Documentary content.
|
||||
if (_halfLeechMediums.Contains(item.TypeMedium) || item.TypeOrigin == 1 || item.TypeCategory is 2 or 3)
|
||||
{
|
||||
return 0.5;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static double GetUploadVolumeFactor(TorrentQueryResponse item)
|
||||
{
|
||||
// 100% Neutral Leech: all XXX content.
|
||||
return item.TypeCategory == 7 ? 0 : 1;
|
||||
}
|
||||
|
||||
private string GetDownloadUrl(string torrentId)
|
||||
{
|
||||
var url = new HttpUri(_settings.BaseUrl)
|
||||
|
||||
@@ -21,12 +21,8 @@ namespace NzbDrone.Core.Indexers.Definitions.HDBits
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
var query = new TorrentQuery();
|
||||
var imdbId = ParseUtil.GetImdbId(searchCriteria.ImdbId).GetValueOrDefault(0);
|
||||
|
||||
if (searchCriteria.Categories?.Length > 0)
|
||||
{
|
||||
query.Category = Capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories).Select(int.Parse).ToArray();
|
||||
}
|
||||
var imdbId = ParseUtil.GetImdbId(searchCriteria.ImdbId).GetValueOrDefault(0);
|
||||
|
||||
if (imdbId == 0 && searchCriteria.SearchTerm.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
@@ -39,37 +35,11 @@ namespace NzbDrone.Core.Indexers.Definitions.HDBits
|
||||
query.ImdbInfo.Id = imdbId;
|
||||
}
|
||||
|
||||
pageableRequests.Add(GetRequest(query));
|
||||
pageableRequests.Add(GetRequest(query, searchCriteria));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public Func<IDictionary<string, string>> GetCookies { get; set; }
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
|
||||
private IEnumerable<IndexerRequest> GetRequest(TorrentQuery query)
|
||||
{
|
||||
var request = new HttpRequestBuilder(Settings.BaseUrl)
|
||||
.Resource("/api/torrents")
|
||||
.Build();
|
||||
|
||||
request.Method = HttpMethod.Post;
|
||||
const string appJson = "application/json";
|
||||
request.Headers.Accept = appJson;
|
||||
request.Headers.ContentType = appJson;
|
||||
|
||||
query.Username = Settings.Username;
|
||||
query.Passkey = Settings.ApiKey;
|
||||
|
||||
query.Codec = Settings.Codecs.ToArray();
|
||||
query.Medium = Settings.Mediums.ToArray();
|
||||
|
||||
request.SetContent(query.ToJson());
|
||||
request.ContentSummary = query.ToJson(Formatting.None);
|
||||
|
||||
yield return new IndexerRequest(request);
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
{
|
||||
return new IndexerPageableRequestChain();
|
||||
@@ -79,14 +49,10 @@ namespace NzbDrone.Core.Indexers.Definitions.HDBits
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
var query = new TorrentQuery();
|
||||
|
||||
var tvdbId = searchCriteria.TvdbId.GetValueOrDefault(0);
|
||||
var imdbId = ParseUtil.GetImdbId(searchCriteria.ImdbId).GetValueOrDefault(0);
|
||||
|
||||
if (searchCriteria.Categories?.Length > 0)
|
||||
{
|
||||
query.Category = Capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories).Select(int.Parse).ToArray();
|
||||
}
|
||||
|
||||
if (tvdbId == 0 && imdbId == 0 && searchCriteria.SearchTerm.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
query.Search = searchCriteria.SanitizedTvSearchString;
|
||||
@@ -114,7 +80,7 @@ namespace NzbDrone.Core.Indexers.Definitions.HDBits
|
||||
query.ImdbInfo.Id = imdbId;
|
||||
}
|
||||
|
||||
pageableRequests.Add(GetRequest(query));
|
||||
pageableRequests.Add(GetRequest(query, searchCriteria));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
@@ -129,19 +95,56 @@ namespace NzbDrone.Core.Indexers.Definitions.HDBits
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
var query = new TorrentQuery();
|
||||
|
||||
if (searchCriteria.Categories?.Length > 0)
|
||||
{
|
||||
query.Category = Capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories).Select(int.Parse).ToArray();
|
||||
}
|
||||
|
||||
if (searchCriteria.SearchTerm.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
query.Search = searchCriteria.SanitizedSearchTerm;
|
||||
}
|
||||
|
||||
pageableRequests.Add(GetRequest(query));
|
||||
pageableRequests.Add(GetRequest(query, searchCriteria));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public Func<IDictionary<string, string>> GetCookies { get; set; }
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
|
||||
private IEnumerable<IndexerRequest> GetRequest(TorrentQuery query, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
var request = new HttpRequestBuilder(Settings.BaseUrl)
|
||||
.Resource("/api/torrents")
|
||||
.Build();
|
||||
|
||||
request.Method = HttpMethod.Post;
|
||||
const string appJson = "application/json";
|
||||
request.Headers.Accept = appJson;
|
||||
request.Headers.ContentType = appJson;
|
||||
|
||||
query.Username = Settings.Username;
|
||||
query.Passkey = Settings.ApiKey;
|
||||
|
||||
query.Codec = Settings.Codecs.ToArray();
|
||||
query.Medium = Settings.Mediums.ToArray();
|
||||
|
||||
if (searchCriteria.Categories?.Length > 0)
|
||||
{
|
||||
query.Category = Capabilities.Categories
|
||||
.MapTorznabCapsToTrackers(searchCriteria.Categories)
|
||||
.Distinct()
|
||||
.Select(int.Parse)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
query.Limit = 100;
|
||||
|
||||
if (searchCriteria.Limit is > 0 && searchCriteria.Offset is > 0)
|
||||
{
|
||||
query.Page = (int)(searchCriteria.Offset / searchCriteria.Limit);
|
||||
}
|
||||
|
||||
request.SetContent(query.ToJson());
|
||||
request.ContentSummary = query.ToJson(Formatting.None);
|
||||
|
||||
yield return new IndexerRequest(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using FluentValidation;
|
||||
using NzbDrone.Core.Annotations;
|
||||
@@ -10,6 +11,7 @@ namespace NzbDrone.Core.Indexers.Definitions.HDBits
|
||||
{
|
||||
public HDBitsSettingsValidator()
|
||||
{
|
||||
RuleFor(c => c.Username).NotEmpty();
|
||||
RuleFor(c => c.ApiKey).NotEmpty();
|
||||
}
|
||||
}
|
||||
@@ -20,8 +22,10 @@ namespace NzbDrone.Core.Indexers.Definitions.HDBits
|
||||
|
||||
public HDBitsSettings()
|
||||
{
|
||||
Codecs = System.Array.Empty<int>();
|
||||
Mediums = System.Array.Empty<int>();
|
||||
Codecs = Array.Empty<int>();
|
||||
Mediums = Array.Empty<int>();
|
||||
FreeleechOnly = false;
|
||||
UseFilenames = true;
|
||||
}
|
||||
|
||||
[FieldDefinition(2, Label = "Username", HelpText = "Site Username", Privacy = PrivacyLevel.UserName)]
|
||||
@@ -30,45 +34,49 @@ namespace NzbDrone.Core.Indexers.Definitions.HDBits
|
||||
[FieldDefinition(3, Label = "API Key", HelpText = "Site API Key", Privacy = PrivacyLevel.ApiKey)]
|
||||
public string ApiKey { get; set; }
|
||||
|
||||
[FieldDefinition(4, Label = "Codecs", Type = FieldType.TagSelect, SelectOptions = typeof(HdBitsCodec), Advanced = true, HelpText = "Options: h264, Mpeg2, VC1, Xvid. If unspecified, all options are used.")]
|
||||
[FieldDefinition(4, Label = "Codecs", Type = FieldType.Select, SelectOptions = typeof(HdBitsCodec), Advanced = true, HelpText = "If unspecified, all options are used.")]
|
||||
public IEnumerable<int> Codecs { get; set; }
|
||||
|
||||
[FieldDefinition(5, Label = "Mediums", Type = FieldType.TagSelect, SelectOptions = typeof(HdBitsMedium), Advanced = true, HelpText = "Options: BluRay, Encode, Capture, Remux, WebDL. If unspecified, all options are used.")]
|
||||
[FieldDefinition(5, Label = "Mediums", Type = FieldType.Select, SelectOptions = typeof(HdBitsMedium), Advanced = true, HelpText = "If unspecified, all options are used.")]
|
||||
public IEnumerable<int> Mediums { get; set; }
|
||||
|
||||
[FieldDefinition(6, Label = "Freeleech Only", Type = FieldType.Checkbox, Advanced = true, HelpText = "Show freeleech releases only")]
|
||||
public bool FreeleechOnly { get; set; }
|
||||
|
||||
[FieldDefinition(7, Label = "Use Filenames", Type = FieldType.Checkbox, HelpText = "Check this option if you want to use torrent filenames as release titles")]
|
||||
public bool UseFilenames { get; set; }
|
||||
|
||||
public override NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
|
||||
public enum HdBitsCategory
|
||||
{
|
||||
Movie = 1,
|
||||
Tv = 2,
|
||||
Documentary = 3,
|
||||
Music = 4,
|
||||
Sport = 5,
|
||||
Audio = 6,
|
||||
Xxx = 7,
|
||||
MiscDemo = 8
|
||||
}
|
||||
|
||||
public enum HdBitsCodec
|
||||
{
|
||||
[FieldOption("H.264")]
|
||||
H264 = 1,
|
||||
[FieldOption("MPEG-2")]
|
||||
Mpeg2 = 2,
|
||||
[FieldOption("VC-1")]
|
||||
Vc1 = 3,
|
||||
[FieldOption("XviD")]
|
||||
Xvid = 4,
|
||||
[FieldOption("HEVC")]
|
||||
HEVC = 5
|
||||
}
|
||||
|
||||
public enum HdBitsMedium
|
||||
{
|
||||
[FieldOption("Blu-ray/HD DVD")]
|
||||
Bluray = 1,
|
||||
[FieldOption("Encode")]
|
||||
Encode = 3,
|
||||
[FieldOption("Capture")]
|
||||
Capture = 4,
|
||||
[FieldOption("Remux")]
|
||||
Remux = 5,
|
||||
[FieldOption("WEB-DL")]
|
||||
WebDl = 6
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ using AngleSharp.Html.Parser;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Indexers.Exceptions;
|
||||
using NzbDrone.Core.Indexers.Settings;
|
||||
@@ -19,7 +20,7 @@ using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
public class HDSpace : TorrentIndexerBase<UserPassTorrentBaseSettings>
|
||||
public class HDSpace : TorrentIndexerBase<HDSpaceSettings>
|
||||
{
|
||||
public override string Name => "HD-Space";
|
||||
public override string[] IndexerUrls => new[] { "https://hd-space.org/" };
|
||||
@@ -225,10 +226,10 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
public class HDSpaceParser : IParseIndexerResponse
|
||||
{
|
||||
private readonly UserPassTorrentBaseSettings _settings;
|
||||
private readonly HDSpaceSettings _settings;
|
||||
private readonly IndexerCapabilitiesCategories _categories;
|
||||
|
||||
public HDSpaceParser(UserPassTorrentBaseSettings settings, IndexerCapabilitiesCategories categories)
|
||||
public HDSpaceParser(HDSpaceSettings settings, IndexerCapabilitiesCategories categories)
|
||||
{
|
||||
_settings = settings;
|
||||
_categories = categories;
|
||||
@@ -251,9 +252,34 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
continue;
|
||||
}
|
||||
|
||||
var release = new TorrentInfo();
|
||||
release.MinimumRatio = 1;
|
||||
release.MinimumSeedTime = 86400; // 24 hours
|
||||
var downloadVolumeFactor = 1.0;
|
||||
|
||||
if (row.QuerySelector("img[title=\"FreeLeech\"]") != null)
|
||||
{
|
||||
downloadVolumeFactor = 0;
|
||||
}
|
||||
else if (row.QuerySelector("img[src=\"images/sf.png\"]") != null)
|
||||
{
|
||||
downloadVolumeFactor = 0;
|
||||
}
|
||||
else if (row.QuerySelector("img[title=\"Half FreeLeech\"]") != null)
|
||||
{
|
||||
downloadVolumeFactor = 0.5;
|
||||
}
|
||||
|
||||
// Skip non-freeleech results when freeleech only is set
|
||||
if (_settings.FreeleechOnly && downloadVolumeFactor != 0.0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var release = new TorrentInfo
|
||||
{
|
||||
DownloadVolumeFactor = downloadVolumeFactor,
|
||||
UploadVolumeFactor = 1,
|
||||
MinimumRatio = 1,
|
||||
MinimumSeedTime = 86400 // 24 hours
|
||||
};
|
||||
|
||||
var qLink = row.QuerySelector("td:nth-child(2) a[href^=\"index.php?page=torrent-details&id=\"]");
|
||||
release.Title = qLink?.TextContent.Trim();
|
||||
@@ -292,25 +318,6 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
var grabs = row.QuerySelector("td:nth-child(10)")?.TextContent.Trim().Replace("---", "0");
|
||||
release.Grabs = ParseUtil.CoerceInt(grabs);
|
||||
|
||||
if (row.QuerySelector("img[title=\"FreeLeech\"]") != null)
|
||||
{
|
||||
release.DownloadVolumeFactor = 0;
|
||||
}
|
||||
else if (row.QuerySelector("img[src=\"images/sf.png\"]") != null)
|
||||
{
|
||||
release.DownloadVolumeFactor = 0;
|
||||
}
|
||||
else if (row.QuerySelector("img[title=\"Half FreeLeech\"]") != null)
|
||||
{
|
||||
release.DownloadVolumeFactor = 0.5;
|
||||
}
|
||||
else
|
||||
{
|
||||
release.DownloadVolumeFactor = 1;
|
||||
}
|
||||
|
||||
release.UploadVolumeFactor = 1;
|
||||
|
||||
var categoryLink = row.QuerySelector("a[href^=\"index.php?page=torrents&category=\"]").GetAttribute("href");
|
||||
var cat = ParseUtil.GetArgumentFromQueryString(categoryLink, "category");
|
||||
release.Categories = _categories.MapTrackerCatToNewznab(cat);
|
||||
@@ -323,4 +330,10 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
}
|
||||
|
||||
public class HDSpaceSettings : UserPassTorrentBaseSettings
|
||||
{
|
||||
[FieldDefinition(4, Label = "Freeleech Only", Type = FieldType.Checkbox, HelpText = "Show freeleech releases only")]
|
||||
public bool FreeleechOnly { get; set; } = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +73,13 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
return CookieUtil.CookieHeaderToDictionary(Settings.Cookie);
|
||||
}
|
||||
|
||||
protected override IList<ReleaseInfo> CleanupReleases(IEnumerable<ReleaseInfo> releases, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
var cleanReleases = base.CleanupReleases(releases, searchCriteria);
|
||||
|
||||
return FilterReleasesByQuery(cleanReleases, searchCriteria).ToList();
|
||||
}
|
||||
|
||||
private IndexerCapabilities SetCapabilities()
|
||||
{
|
||||
var caps = new IndexerCapabilities
|
||||
@@ -293,6 +300,10 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
var parser = new HtmlParser();
|
||||
using var doc = parser.ParseDocument(indexerResponse.Content);
|
||||
|
||||
var headerColumns = doc.QuerySelectorAll("table[id=\"torrents\"] > thead > tr > th").Select(x => x.TextContent.Trim()).ToList();
|
||||
var sizeIndex = FindColumnIndexOrDefault(headerColumns, "Sort by size", 5);
|
||||
var filesIndex = FindColumnIndexOrDefault(headerColumns, "Sort by files");
|
||||
|
||||
var rows = doc.QuerySelectorAll("table[id=\"torrents\"] > tbody > tr");
|
||||
foreach (var row in rows)
|
||||
{
|
||||
@@ -314,7 +325,6 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
var dateSplit = descrSplit.Last().Split(new[] { " by " }, StringSplitOptions.None);
|
||||
var publishDate = DateTimeUtil.FromTimeAgo(dateSplit.First());
|
||||
var description = descrSplit.Length > 1 ? "Tags: " + descrSplit.First().Trim() : "";
|
||||
description += dateSplit.Length > 1 ? " Uploaded by: " + dateSplit.Last().Trim() : "";
|
||||
|
||||
var catIcon = row.QuerySelector("td:nth-of-type(1) a");
|
||||
if (catIcon == null)
|
||||
@@ -327,36 +337,40 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
// Torrents - Category column == Icons
|
||||
var cat = _categories.MapTrackerCatToNewznab(catIcon.GetAttribute("href").Substring(1));
|
||||
|
||||
var size = ParseUtil.GetBytes(row.Children[5].TextContent);
|
||||
var size = ParseUtil.GetBytes(row.Children[sizeIndex].TextContent);
|
||||
|
||||
var colIndex = 6;
|
||||
int? files = null;
|
||||
|
||||
if (row.Children.Length == 10)
|
||||
if (filesIndex != -1)
|
||||
{
|
||||
files = ParseUtil.CoerceInt(row.Children[colIndex].TextContent.Replace("Go to files", ""));
|
||||
colIndex++;
|
||||
files = ParseUtil.CoerceInt(row.Children[filesIndex].TextContent.Replace("Go to files", ""));
|
||||
}
|
||||
|
||||
var grabs = ParseUtil.CoerceInt(row.Children[colIndex++].TextContent);
|
||||
var seeders = ParseUtil.CoerceInt(row.Children[colIndex++].TextContent);
|
||||
var leechers = ParseUtil.CoerceInt(row.Children[colIndex].TextContent);
|
||||
var dlVolumeFactor = row.QuerySelector("span.free") != null ? 0 : 1;
|
||||
var colIndex = row.Children.Length == 10 ? 7 : 6;
|
||||
|
||||
var grabsIndex = FindColumnIndexOrDefault(headerColumns, "Sort by snatches", colIndex++);
|
||||
var seedersIndex = FindColumnIndexOrDefault(headerColumns, "Sort by seeders", colIndex++);
|
||||
var leechersIndex = FindColumnIndexOrDefault(headerColumns, "Sort by leechers", colIndex);
|
||||
|
||||
var grabs = ParseUtil.CoerceInt(row.Children[grabsIndex].TextContent);
|
||||
var seeders = ParseUtil.CoerceInt(row.Children[seedersIndex].TextContent);
|
||||
var leechers = ParseUtil.CoerceInt(row.Children[leechersIndex].TextContent);
|
||||
|
||||
var release = new TorrentInfo
|
||||
{
|
||||
Title = title,
|
||||
Guid = details.AbsoluteUri,
|
||||
DownloadUrl = link.AbsoluteUri,
|
||||
InfoUrl = details.AbsoluteUri,
|
||||
PublishDate = publishDate,
|
||||
Title = title,
|
||||
Description = description,
|
||||
Categories = cat,
|
||||
Size = size,
|
||||
Files = files,
|
||||
Grabs = grabs,
|
||||
Seeders = seeders,
|
||||
Peers = seeders + leechers,
|
||||
DownloadVolumeFactor = dlVolumeFactor,
|
||||
PublishDate = publishDate,
|
||||
DownloadVolumeFactor = row.QuerySelector("span.free") != null ? 0 : 1,
|
||||
UploadVolumeFactor = 1,
|
||||
MinimumRatio = 1,
|
||||
MinimumSeedTime = 1209600 // 336 hours
|
||||
@@ -368,6 +382,13 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
return torrentInfos.ToArray();
|
||||
}
|
||||
|
||||
private static int FindColumnIndexOrDefault(List<string> columns, string name, int defaultIndex = -1)
|
||||
{
|
||||
var index = columns.FindIndex(x => x.Equals(name, StringComparison.Ordinal));
|
||||
|
||||
return index != -1 ? index : defaultIndex;
|
||||
}
|
||||
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
|
||||
private static string CleanTitle(string title)
|
||||
|
||||
@@ -10,6 +10,7 @@ using AngleSharp.Html.Parser;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Indexers.Exceptions;
|
||||
using NzbDrone.Core.Indexers.Settings;
|
||||
@@ -20,7 +21,7 @@ using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
public class ImmortalSeed : TorrentIndexerBase<UserPassTorrentBaseSettings>
|
||||
public class ImmortalSeed : TorrentIndexerBase<ImmortalSeedSettings>
|
||||
{
|
||||
public override string Name => "ImmortalSeed";
|
||||
public override string[] IndexerUrls => new[] { "https://immortalseed.me/" };
|
||||
@@ -46,7 +47,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
public override IParseIndexerResponse GetParser()
|
||||
{
|
||||
return new ImmortalSeedParser(Capabilities.Categories);
|
||||
return new ImmortalSeedParser(Settings, Capabilities.Categories);
|
||||
}
|
||||
|
||||
protected override async Task DoLogin()
|
||||
@@ -253,11 +254,14 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
public class ImmortalSeedParser : IParseIndexerResponse
|
||||
{
|
||||
private readonly ImmortalSeedSettings _settings;
|
||||
private readonly IndexerCapabilitiesCategories _categories;
|
||||
|
||||
private readonly Regex _dateAddedRegex = new (@"\d{4}-\d{2}-\d{2} \d{2}:\d{2} [AaPp][Mm]", RegexOptions.Compiled);
|
||||
|
||||
public ImmortalSeedParser(IndexerCapabilitiesCategories categories)
|
||||
public ImmortalSeedParser(ImmortalSeedSettings settings, IndexerCapabilitiesCategories categories)
|
||||
{
|
||||
_settings = settings;
|
||||
_categories = categories;
|
||||
}
|
||||
|
||||
@@ -271,6 +275,23 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
var rows = dom.QuerySelectorAll("table#sortabletable > tbody > tr:has(a[href*=\"details.php?id=\"])");
|
||||
foreach (var row in rows)
|
||||
{
|
||||
var downloadVolumeFactor = 1.0;
|
||||
|
||||
if (row.QuerySelector("img[title^=\"Free Torrent\"], img[title^=\"Sitewide Free Torrent\"]") != null)
|
||||
{
|
||||
downloadVolumeFactor = 0.0;
|
||||
}
|
||||
else if (row.QuerySelector("img[title^=\"Silver Torrent\"]") != null)
|
||||
{
|
||||
downloadVolumeFactor = 0.5;
|
||||
}
|
||||
|
||||
// Skip non-freeleech results when freeleech only is set
|
||||
if (_settings.FreeleechOnly && downloadVolumeFactor != 0.0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// details link, release name gets shortened if it's to long
|
||||
var qDetails = row.QuerySelector("div > a[href*=\"details.php?id=\"]");
|
||||
|
||||
@@ -300,6 +321,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
Peers = peers,
|
||||
Size = ParseUtil.GetBytes(row.QuerySelector("td:nth-of-type(5)")?.TextContent.Trim()),
|
||||
Grabs = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(6)")?.TextContent),
|
||||
DownloadVolumeFactor = downloadVolumeFactor,
|
||||
UploadVolumeFactor = row.QuerySelector("img[title^=\"x2 Torrent\"]") != null ? 2 : 1,
|
||||
MinimumRatio = 1,
|
||||
MinimumSeedTime = 86400 // 24 hours
|
||||
@@ -311,19 +333,6 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
release.PublishDate = DateTime.ParseExact(dateAddedMatch.Value, "yyyy-MM-dd hh:mm tt", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
if (row.QuerySelector("img[title^=\"Free Torrent\"], img[title^=\"Sitewide Free Torrent\"]") != null)
|
||||
{
|
||||
release.DownloadVolumeFactor = 0;
|
||||
}
|
||||
else if (row.QuerySelector("img[title^=\"Silver Torrent\"]") != null)
|
||||
{
|
||||
release.DownloadVolumeFactor = 0.5;
|
||||
}
|
||||
else
|
||||
{
|
||||
release.DownloadVolumeFactor = 1;
|
||||
}
|
||||
|
||||
releaseInfos.Add(release);
|
||||
}
|
||||
|
||||
@@ -332,4 +341,10 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
}
|
||||
|
||||
public class ImmortalSeedSettings : UserPassTorrentBaseSettings
|
||||
{
|
||||
[FieldDefinition(4, Label = "Freeleech Only", Type = FieldType.Checkbox, HelpText = "Show freeleech releases only")]
|
||||
public bool FreeleechOnly { get; set; } = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,12 +265,18 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
Grabs = ParseUtil.CoerceInt(row.Snatch),
|
||||
Seeders = ParseUtil.CoerceInt(row.Seed),
|
||||
Peers = ParseUtil.CoerceInt(row.Seed) + ParseUtil.CoerceInt(row.Leech),
|
||||
Scene = row.Tags?.ContainsIgnoreCase("scene"),
|
||||
MinimumRatio = 0, // ratioless
|
||||
MinimumSeedTime = row.Category.ToLower() == "season" ? 432000 : 86400, // 120 hours for seasons and 24 hours for episodes
|
||||
DownloadVolumeFactor = 0, // ratioless tracker
|
||||
UploadVolumeFactor = 1
|
||||
};
|
||||
|
||||
if (row.TvMazeId.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
release.TvMazeId = ParseUtil.CoerceInt(row.TvMazeId);
|
||||
}
|
||||
|
||||
torrentInfos.Add(release);
|
||||
}
|
||||
|
||||
@@ -339,8 +345,11 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
public string Banner { get; set; }
|
||||
[JsonPropertyName("group_id")]
|
||||
public string TorrentId { get; set; }
|
||||
[JsonPropertyName("series_id")]
|
||||
public string TvMazeId { get; set; }
|
||||
[JsonPropertyName("rls_utc")]
|
||||
public string PublishDateUtc { get; set; }
|
||||
public IEnumerable<string> Tags { get; set; }
|
||||
}
|
||||
|
||||
public class NebulanceTorrents
|
||||
|
||||
@@ -31,6 +31,7 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
{
|
||||
return new NewznabRequestGenerator(_capabilitiesProvider)
|
||||
{
|
||||
Definition = Definition,
|
||||
PageSize = PageSize,
|
||||
Settings = Settings
|
||||
};
|
||||
@@ -101,11 +102,7 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
yield return GetDefinition("NZBNDX", GetSettings("https://www.nzbndx.com"));
|
||||
yield return GetDefinition("NzbPlanet", GetSettings("https://api.nzbplanet.net"));
|
||||
yield return GetDefinition("NZBStars", GetSettings("https://nzbstars.com"));
|
||||
yield return GetDefinition("OZnzb", GetSettings("https://api.oznzb.com"));
|
||||
yield return GetDefinition("SimplyNZBs", GetSettings("https://simplynzbs.com"));
|
||||
yield return GetDefinition("SpotNZB", GetSettings("https://spotnzb.xyz"));
|
||||
yield return GetDefinition("Tabula Rasa", GetSettings("https://www.tabula-rasa.pw", apiPath: @"/api/v1/api"));
|
||||
yield return GetDefinition("VeryCouch LazyMuch", GetSettings("https://api.verycouch.com"));
|
||||
yield return GetDefinition("Generic Newznab", GetSettings(""));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,6 +129,8 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
capabilities.SearchParams.AddIfNotNull(searchParam);
|
||||
}
|
||||
}
|
||||
|
||||
capabilities.SupportsRawSearch = xmlBasicSearch.Attribute("searchEngine")?.Value == "raw";
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -13,10 +13,11 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
public class NewznabRequestGenerator : IIndexerRequestGenerator
|
||||
{
|
||||
private readonly INewznabCapabilitiesProvider _capabilitiesProvider;
|
||||
|
||||
public ProviderDefinition Definition { get; set; }
|
||||
public int MaxPages { get; set; }
|
||||
public int PageSize { get; set; }
|
||||
public NewznabSettings Settings { get; set; }
|
||||
public ProviderDefinition Definition { get; set; }
|
||||
|
||||
public NewznabRequestGenerator(INewznabCapabilitiesProvider capabilitiesProvider)
|
||||
{
|
||||
|
||||
@@ -3,7 +3,6 @@ using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using AngleSharp.Dom;
|
||||
@@ -76,12 +75,16 @@ public class NorBits : TorrentIndexerBase<NorBitsSettings>
|
||||
{
|
||||
LogResponseContent = true,
|
||||
AllowAutoRedirect = true,
|
||||
Method = HttpMethod.Post
|
||||
SuppressHttpError = true
|
||||
};
|
||||
|
||||
var authLoginCheckRequest = requestBuilder3
|
||||
.Post()
|
||||
.AddFormParameter("username", Settings.Username)
|
||||
.AddFormParameter("password", Settings.Password)
|
||||
.AddFormParameter("code", Settings.TwoFactorAuthCode ?? string.Empty)
|
||||
.AddFormParameter("logout", "no")
|
||||
.AddFormParameter("returnto", "/")
|
||||
.SetCookies(indexPage.GetCookies())
|
||||
.SetHeader("Referer", loginUrl)
|
||||
.Build();
|
||||
@@ -174,6 +177,11 @@ public class NorBitsRequestGenerator : IIndexerRequestGenerator
|
||||
{ "scenerelease", "0" }
|
||||
};
|
||||
|
||||
if (_settings.FreeLeechOnly)
|
||||
{
|
||||
parameters.Set("FL", "1");
|
||||
}
|
||||
|
||||
var searchTerm = "search=";
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(imdbId))
|
||||
@@ -343,6 +351,12 @@ public class NorBitsSettings : UserPassTorrentBaseSettings
|
||||
UseFullSearch = false;
|
||||
}
|
||||
|
||||
[FieldDefinition(4, Label = "Use Full Search", HelpText = "Use Full Search from Site", Type = FieldType.Checkbox)]
|
||||
[FieldDefinition(4, Label = "2FA code", Type = FieldType.Textbox, HelpText = "Only fill in the <b>2FA code</b> box if you have enabled <b>2FA</b> on the NorBits Web Site. Otherwise just leave it empty.")]
|
||||
public string TwoFactorAuthCode { get; set; }
|
||||
|
||||
[FieldDefinition(5, Label = "Use Full Search", Type = FieldType.Checkbox, HelpText = "Use Full Search from Site")]
|
||||
public bool UseFullSearch { get; set; }
|
||||
|
||||
[FieldDefinition(6, Label = "FreeLeech Only", Type = FieldType.Checkbox, HelpText = "Search FreeLeech torrents only")]
|
||||
public bool FreeLeechOnly { get; set; }
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
|
||||
public override IndexerCapabilities Capabilities => SetCapabilities();
|
||||
public override bool SupportsRedirect => true;
|
||||
public override TimeSpan RateLimit => TimeSpan.FromSeconds(3);
|
||||
|
||||
public Orpheus(IIndexerHttpClient httpClient,
|
||||
IEventAggregator eventAggregator,
|
||||
@@ -49,10 +50,24 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
return new OrpheusParser(Settings, Capabilities.Categories);
|
||||
}
|
||||
|
||||
protected override IList<ReleaseInfo> CleanupReleases(IEnumerable<ReleaseInfo> releases, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
var cleanReleases = base.CleanupReleases(releases, searchCriteria);
|
||||
|
||||
if (searchCriteria.IsRssSearch)
|
||||
{
|
||||
cleanReleases = cleanReleases.Take(50).ToList();
|
||||
}
|
||||
|
||||
return cleanReleases;
|
||||
}
|
||||
|
||||
private IndexerCapabilities SetCapabilities()
|
||||
{
|
||||
var caps = new IndexerCapabilities
|
||||
{
|
||||
LimitsDefault = 50,
|
||||
LimitsMax = 50,
|
||||
MusicSearchParams = new List<MusicSearchParam>
|
||||
{
|
||||
MusicSearchParam.Q, MusicSearchParam.Artist, MusicSearchParam.Album, MusicSearchParam.Year
|
||||
@@ -200,6 +215,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
}
|
||||
|
||||
var queryCats = _capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories);
|
||||
|
||||
if (queryCats.Any())
|
||||
{
|
||||
queryCats.ForEach(cat => parameters.Set($"filter_cat[{cat}]", "1"));
|
||||
@@ -273,6 +289,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
Title = WebUtility.HtmlDecode(title),
|
||||
Artist = WebUtility.HtmlDecode(result.Artist),
|
||||
Album = WebUtility.HtmlDecode(result.GroupName),
|
||||
Year = int.Parse(result.GroupYear),
|
||||
Container = torrent.Encoding,
|
||||
Codec = torrent.Format,
|
||||
Size = long.Parse(torrent.Size),
|
||||
@@ -345,7 +362,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
private string GetTitle(GazelleRelease result, GazelleTorrent torrent)
|
||||
{
|
||||
var title = $"{result.Artist} - {result.GroupName} [{result.GroupYear}]";
|
||||
var title = $"{result.Artist} - {result.GroupName} ({result.GroupYear})";
|
||||
|
||||
if (result.ReleaseType.IsNotNullOrWhiteSpace() && result.ReleaseType != "Unknown")
|
||||
{
|
||||
@@ -357,14 +374,23 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
title += $" [{$"{torrent.RemasterTitle} {torrent.RemasterYear}".Trim()}]";
|
||||
}
|
||||
|
||||
title += $" [{torrent.Format} {torrent.Encoding}] [{torrent.Media}]";
|
||||
var flags = new List<string>
|
||||
{
|
||||
$"{torrent.Format} {torrent.Encoding}",
|
||||
$"{torrent.Media}"
|
||||
};
|
||||
|
||||
if (torrent.HasLog)
|
||||
{
|
||||
flags.Add("Log (" + torrent.LogScore + "%)");
|
||||
}
|
||||
|
||||
if (torrent.HasCue)
|
||||
{
|
||||
title += " [Cue]";
|
||||
flags.Add("Cue");
|
||||
}
|
||||
|
||||
return title;
|
||||
return $"{title} [{string.Join(" / ", flags)}]";
|
||||
}
|
||||
|
||||
private string GetDownloadUrl(int torrentId, bool canUseToken)
|
||||
@@ -375,7 +401,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
.AddQueryParam("id", torrentId);
|
||||
|
||||
// Orpheus fails to download if usetoken=0 so we need to only add if we will use one
|
||||
if (_settings.UseFreeleechToken)
|
||||
if (_settings.UseFreeleechToken && canUseToken)
|
||||
{
|
||||
url = url.AddQueryParam("usetoken", "1");
|
||||
}
|
||||
|
||||
@@ -29,12 +29,12 @@ namespace NzbDrone.Core.Indexers.Definitions.PassThePopcorn
|
||||
|
||||
public override IIndexerRequestGenerator GetRequestGenerator()
|
||||
{
|
||||
return new PassThePopcornRequestGenerator(Settings);
|
||||
return new PassThePopcornRequestGenerator(Settings, Capabilities);
|
||||
}
|
||||
|
||||
public override IParseIndexerResponse GetParser()
|
||||
{
|
||||
return new PassThePopcornParser(Settings, _logger);
|
||||
return new PassThePopcornParser(Settings);
|
||||
}
|
||||
|
||||
private IndexerCapabilities SetCapabilities()
|
||||
@@ -43,6 +43,10 @@ namespace NzbDrone.Core.Indexers.Definitions.PassThePopcorn
|
||||
{
|
||||
LimitsDefault = PageSize,
|
||||
LimitsMax = PageSize,
|
||||
TvSearchParams = new List<TvSearchParam>
|
||||
{
|
||||
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep, TvSearchParam.ImdbId
|
||||
},
|
||||
MovieSearchParams = new List<MovieSearchParam>
|
||||
{
|
||||
MovieSearchParam.Q, MovieSearchParam.ImdbId
|
||||
@@ -56,18 +60,11 @@ namespace NzbDrone.Core.Indexers.Definitions.PassThePopcorn
|
||||
};
|
||||
|
||||
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.Movies, "Feature Film");
|
||||
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.MoviesForeign);
|
||||
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.MoviesOther);
|
||||
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.MoviesSD);
|
||||
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.MoviesHD);
|
||||
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.Movies3D);
|
||||
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.MoviesBluRay);
|
||||
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.MoviesDVD);
|
||||
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.MoviesWEBDL);
|
||||
caps.Categories.AddCategoryMapping(2, NewznabStandardCategory.Movies, "Short Film");
|
||||
caps.Categories.AddCategoryMapping(3, NewznabStandardCategory.TV, "Miniseries");
|
||||
caps.Categories.AddCategoryMapping(4, NewznabStandardCategory.TV, "Stand-up Comedy");
|
||||
caps.Categories.AddCategoryMapping(5, NewznabStandardCategory.TV, "Live Performance");
|
||||
caps.Categories.AddCategoryMapping(4, NewznabStandardCategory.Movies, "Stand-up Comedy");
|
||||
caps.Categories.AddCategoryMapping(5, NewznabStandardCategory.Movies, "Live Performance");
|
||||
caps.Categories.AddCategoryMapping(6, NewznabStandardCategory.Movies, "Movie Collection");
|
||||
|
||||
return caps;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
using NLog;
|
||||
using System.Text.RegularExpressions;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Common.Serializer;
|
||||
@@ -14,12 +14,12 @@ namespace NzbDrone.Core.Indexers.Definitions.PassThePopcorn
|
||||
public class PassThePopcornParser : IParseIndexerResponse
|
||||
{
|
||||
private readonly PassThePopcornSettings _settings;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public PassThePopcornParser(PassThePopcornSettings settings, Logger logger)
|
||||
private static Regex SeasonRegex => new (@"\bS\d{2,3}(E\d{2,3})?\b", RegexOptions.Compiled);
|
||||
|
||||
public PassThePopcornParser(PassThePopcornSettings settings)
|
||||
{
|
||||
_settings = settings;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
||||
@@ -57,6 +57,7 @@ namespace NzbDrone.Core.Indexers.Definitions.PassThePopcorn
|
||||
foreach (var torrent in result.Torrents)
|
||||
{
|
||||
var id = torrent.Id;
|
||||
var title = torrent.ReleaseName;
|
||||
|
||||
var flags = new HashSet<IndexerFlag>();
|
||||
|
||||
@@ -70,14 +71,21 @@ namespace NzbDrone.Core.Indexers.Definitions.PassThePopcorn
|
||||
flags.Add(PassThePopcornFlag.Approved);
|
||||
}
|
||||
|
||||
var categories = new List<IndexerCategory> { NewznabStandardCategory.Movies };
|
||||
|
||||
if (title != null && SeasonRegex.Match(title).Success)
|
||||
{
|
||||
categories.Add(NewznabStandardCategory.TV);
|
||||
}
|
||||
|
||||
torrentInfos.Add(new TorrentInfo
|
||||
{
|
||||
Guid = $"PassThePopcorn-{id}",
|
||||
Title = torrent.ReleaseName,
|
||||
Title = title,
|
||||
Year = int.Parse(result.Year),
|
||||
InfoUrl = GetInfoUrl(result.GroupId, id),
|
||||
DownloadUrl = GetDownloadUrl(id, jsonResponse.AuthKey, jsonResponse.PassKey),
|
||||
Categories = new List<IndexerCategory> { NewznabStandardCategory.Movies },
|
||||
Categories = categories,
|
||||
Size = long.Parse(torrent.Size),
|
||||
Grabs = int.Parse(torrent.Snatched),
|
||||
Seeders = int.Parse(torrent.Seeders),
|
||||
@@ -86,7 +94,12 @@ namespace NzbDrone.Core.Indexers.Definitions.PassThePopcorn
|
||||
ImdbId = result.ImdbId.IsNotNullOrWhiteSpace() ? int.Parse(result.ImdbId) : 0,
|
||||
Scene = torrent.Scene,
|
||||
IndexerFlags = flags,
|
||||
DownloadVolumeFactor = torrent.FreeleechType is "Freeleech" ? 0 : 1,
|
||||
DownloadVolumeFactor = torrent.FreeleechType?.ToUpperInvariant() switch
|
||||
{
|
||||
"FREELEECH" => 0,
|
||||
"HALF LEECH" => 0.5,
|
||||
_ => 1
|
||||
},
|
||||
UploadVolumeFactor = 1,
|
||||
MinimumRatio = 1,
|
||||
MinimumSeedTime = 345600,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
@@ -11,10 +12,12 @@ namespace NzbDrone.Core.Indexers.Definitions.PassThePopcorn
|
||||
public class PassThePopcornRequestGenerator : IIndexerRequestGenerator
|
||||
{
|
||||
private readonly PassThePopcornSettings _settings;
|
||||
private readonly IndexerCapabilities _capabilities;
|
||||
|
||||
public PassThePopcornRequestGenerator(PassThePopcornSettings settings)
|
||||
public PassThePopcornRequestGenerator(PassThePopcornSettings settings, IndexerCapabilities capabilities)
|
||||
{
|
||||
_settings = settings;
|
||||
_capabilities = capabilities;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
@@ -27,7 +30,7 @@ namespace NzbDrone.Core.Indexers.Definitions.PassThePopcorn
|
||||
}
|
||||
else
|
||||
{
|
||||
pageableRequests.Add(GetRequest($"{searchCriteria.SearchTerm}", searchCriteria));
|
||||
pageableRequests.Add(GetRequest($"{searchCriteria.SanitizedSearchTerm}", searchCriteria));
|
||||
}
|
||||
|
||||
return pageableRequests;
|
||||
@@ -40,7 +43,18 @@ namespace NzbDrone.Core.Indexers.Definitions.PassThePopcorn
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
{
|
||||
return new IndexerPageableRequestChain();
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
if (searchCriteria.ImdbId.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
pageableRequests.Add(GetRequest(searchCriteria.FullImdbId, searchCriteria));
|
||||
}
|
||||
else
|
||||
{
|
||||
pageableRequests.Add(GetRequest($"{searchCriteria.SanitizedTvSearchString}", searchCriteria));
|
||||
}
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
@@ -52,32 +66,43 @@ namespace NzbDrone.Core.Indexers.Definitions.PassThePopcorn
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetRequest($"{searchCriteria.SearchTerm}", searchCriteria));
|
||||
pageableRequests.Add(GetRequest($"{searchCriteria.SanitizedSearchTerm}", searchCriteria));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
private IEnumerable<IndexerRequest> GetRequest(string searchParameters, SearchCriteriaBase searchCriteria)
|
||||
private IEnumerable<IndexerRequest> GetRequest(string searchTerm, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
var parameters = new NameValueCollection
|
||||
{
|
||||
{ "action", "advanced" },
|
||||
{ "json", "noredirect" },
|
||||
{ "grouping", "0" },
|
||||
{ "searchstr", searchParameters }
|
||||
{ "searchstr", searchTerm }
|
||||
};
|
||||
|
||||
if (_settings.FreeleechOnly)
|
||||
{
|
||||
parameters.Set("freetorrent", "1");
|
||||
}
|
||||
|
||||
var queryCats = _capabilities.Categories
|
||||
.MapTorznabCapsToTrackers(searchCriteria.Categories)
|
||||
.Select(int.Parse)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
if (searchCriteria.IsRssSearch && queryCats.Any())
|
||||
{
|
||||
queryCats.ForEach(cat => parameters.Set($"filter_cat[{cat}]", "1"));
|
||||
}
|
||||
|
||||
if (searchCriteria.Limit is > 0 && searchCriteria.Offset is > 0)
|
||||
{
|
||||
var page = (int)(searchCriteria.Offset / searchCriteria.Limit) + 1;
|
||||
parameters.Set("page", page.ToString());
|
||||
}
|
||||
|
||||
if (_settings.FreeleechOnly)
|
||||
{
|
||||
parameters.Set("freetorrent", "1");
|
||||
}
|
||||
|
||||
var searchUrl = $"{_settings.BaseUrl.Trim().TrimEnd('/')}/torrents.php?{parameters.GetQueryString()}";
|
||||
|
||||
var request = new IndexerRequest(searchUrl, HttpAccept.Json);
|
||||
|
||||
@@ -19,7 +19,7 @@ using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions;
|
||||
|
||||
[Obsolete("PirateTheNet has shutdown 2023-10-14")]
|
||||
public class PirateTheNet : TorrentIndexerBase<UserPassTorrentBaseSettings>
|
||||
{
|
||||
public override string Name => "PirateTheNet";
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
public override string Name => "PrivateHD";
|
||||
public override string[] IndexerUrls => new[] { "https://privatehd.to/" };
|
||||
public override string Description => "PrivateHD is a Private Torrent Tracker for HD MOVIES / TV and the sister-site of AvistaZ, CinemaZ, ExoticaZ, and AnimeTorrents";
|
||||
public override string Description => "PrivateHD (PHD) is a Private Torrent Tracker for HD MOVIES / TV and the sister-site of AvistaZ, CinemaZ, ExoticaZ, and AnimeTorrents";
|
||||
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
|
||||
|
||||
public PrivateHD(IIndexerRepository indexerRepository,
|
||||
|
||||
@@ -29,6 +29,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
|
||||
public override IndexerCapabilities Capabilities => SetCapabilities();
|
||||
public override bool SupportsRedirect => true;
|
||||
public override TimeSpan RateLimit => TimeSpan.FromSeconds(3);
|
||||
|
||||
public Redacted(IIndexerHttpClient httpClient,
|
||||
IEventAggregator eventAggregator,
|
||||
@@ -63,10 +64,24 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
return Task.FromResult(request);
|
||||
}
|
||||
|
||||
protected override IList<ReleaseInfo> CleanupReleases(IEnumerable<ReleaseInfo> releases, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
var cleanReleases = base.CleanupReleases(releases, searchCriteria);
|
||||
|
||||
if (searchCriteria.IsRssSearch)
|
||||
{
|
||||
cleanReleases = cleanReleases.Take(50).ToList();
|
||||
}
|
||||
|
||||
return cleanReleases;
|
||||
}
|
||||
|
||||
private IndexerCapabilities SetCapabilities()
|
||||
{
|
||||
var caps = new IndexerCapabilities
|
||||
{
|
||||
LimitsDefault = 50,
|
||||
LimitsMax = 50,
|
||||
MusicSearchParams = new List<MusicSearchParam>
|
||||
{
|
||||
MusicSearchParam.Q, MusicSearchParam.Artist, MusicSearchParam.Album, MusicSearchParam.Year
|
||||
@@ -172,11 +187,17 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
}
|
||||
|
||||
var queryCats = _capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories);
|
||||
|
||||
if (queryCats.Any())
|
||||
{
|
||||
queryCats.ForEach(cat => parameters.Set($"filter_cat[{cat}]", "1"));
|
||||
}
|
||||
|
||||
if (_settings.FreeloadOnly)
|
||||
{
|
||||
parameters.Set("freetorrent", "4");
|
||||
}
|
||||
|
||||
var searchUrl = _settings.BaseUrl.TrimEnd('/') + $"/ajax.php?{parameters.GetQueryString()}";
|
||||
|
||||
var request = new IndexerRequest(searchUrl, HttpAccept.Json);
|
||||
@@ -226,6 +247,12 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
foreach (var torrent in result.Torrents)
|
||||
{
|
||||
// skip non-freeload results when freeload only is set
|
||||
if (_settings.FreeloadOnly && !torrent.IsFreeload)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var id = torrent.TorrentId;
|
||||
|
||||
var title = GetTitle(result, torrent);
|
||||
@@ -235,10 +262,11 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
Guid = infoUrl,
|
||||
InfoUrl = infoUrl,
|
||||
DownloadUrl = GetDownloadUrl(id, torrent.CanUseToken),
|
||||
DownloadUrl = GetDownloadUrl(id, torrent.CanUseToken && !torrent.IsFreeload),
|
||||
Title = WebUtility.HtmlDecode(title),
|
||||
Artist = WebUtility.HtmlDecode(result.Artist),
|
||||
Album = WebUtility.HtmlDecode(result.GroupName),
|
||||
Year = int.Parse(result.GroupYear),
|
||||
Container = torrent.Encoding,
|
||||
Codec = torrent.Format,
|
||||
Size = long.Parse(torrent.Size),
|
||||
@@ -248,8 +276,8 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
Scene = torrent.Scene,
|
||||
Files = torrent.FileCount,
|
||||
Grabs = torrent.Snatches,
|
||||
DownloadVolumeFactor = torrent.IsFreeLeech || torrent.IsNeutralLeech || torrent.IsPersonalFreeLeech ? 0 : 1,
|
||||
UploadVolumeFactor = torrent.IsNeutralLeech ? 0 : 1
|
||||
DownloadVolumeFactor = torrent.IsFreeLeech || torrent.IsNeutralLeech || torrent.IsFreeload || torrent.IsPersonalFreeLeech ? 0 : 1,
|
||||
UploadVolumeFactor = torrent.IsNeutralLeech || torrent.IsFreeload ? 0 : 1
|
||||
};
|
||||
|
||||
var category = torrent.Category;
|
||||
@@ -269,6 +297,12 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
// Non-Audio files are formatted a little differently (1:1 for group and torrents)
|
||||
else
|
||||
{
|
||||
// skip non-freeload results when freeload only is set
|
||||
if (_settings.FreeloadOnly && !result.IsFreeload)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var id = result.TorrentId;
|
||||
var infoUrl = GetInfoUrl(result.GroupId, id);
|
||||
|
||||
@@ -277,15 +311,15 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
Guid = infoUrl,
|
||||
Title = WebUtility.HtmlDecode(result.GroupName),
|
||||
Size = long.Parse(result.Size),
|
||||
DownloadUrl = GetDownloadUrl(id, result.CanUseToken),
|
||||
DownloadUrl = GetDownloadUrl(id, result.CanUseToken && !result.IsFreeload),
|
||||
InfoUrl = infoUrl,
|
||||
Seeders = int.Parse(result.Seeders),
|
||||
Peers = int.Parse(result.Leechers) + int.Parse(result.Seeders),
|
||||
PublishDate = DateTimeOffset.FromUnixTimeSeconds(ParseUtil.CoerceLong(result.GroupTime)).UtcDateTime,
|
||||
Files = result.FileCount,
|
||||
Grabs = result.Snatches,
|
||||
DownloadVolumeFactor = result.IsFreeLeech || result.IsNeutralLeech || result.IsPersonalFreeLeech ? 0 : 1,
|
||||
UploadVolumeFactor = result.IsNeutralLeech ? 0 : 1
|
||||
DownloadVolumeFactor = result.IsFreeLeech || result.IsNeutralLeech || result.IsFreeload || result.IsPersonalFreeLeech ? 0 : 1,
|
||||
UploadVolumeFactor = result.IsNeutralLeech || result.IsFreeload ? 0 : 1
|
||||
};
|
||||
|
||||
var category = result.Category;
|
||||
@@ -311,7 +345,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
private string GetTitle(GazelleRelease result, GazelleTorrent torrent)
|
||||
{
|
||||
var title = $"{result.Artist} - {result.GroupName} [{result.GroupYear}]";
|
||||
var title = $"{result.Artist} - {result.GroupName} ({result.GroupYear})";
|
||||
|
||||
if (result.ReleaseType.IsNotNullOrWhiteSpace() && result.ReleaseType != "Unknown")
|
||||
{
|
||||
@@ -323,14 +357,23 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
title += $" [{$"{torrent.RemasterTitle} {torrent.RemasterYear}".Trim()}]";
|
||||
}
|
||||
|
||||
title += $" [{torrent.Format} {torrent.Encoding}] [{torrent.Media}]";
|
||||
var flags = new List<string>
|
||||
{
|
||||
$"{torrent.Format} {torrent.Encoding}",
|
||||
$"{torrent.Media}"
|
||||
};
|
||||
|
||||
if (torrent.HasLog)
|
||||
{
|
||||
flags.Add("Log (" + torrent.LogScore + "%)");
|
||||
}
|
||||
|
||||
if (torrent.HasCue)
|
||||
{
|
||||
title += " [Cue]";
|
||||
flags.Add("Cue");
|
||||
}
|
||||
|
||||
return title;
|
||||
return $"{title} [{string.Join(" / ", flags)}]";
|
||||
}
|
||||
|
||||
private string GetDownloadUrl(int torrentId, bool canUseToken)
|
||||
@@ -377,12 +420,15 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
UseFreeleechToken = false;
|
||||
}
|
||||
|
||||
[FieldDefinition(2, Label = "API Key", HelpText = "API Key from the Site (Found in Settings => Access Settings)", Privacy = PrivacyLevel.ApiKey)]
|
||||
[FieldDefinition(2, Label = "API Key", Privacy = PrivacyLevel.ApiKey, HelpText = "API Key from the Site (Found in Settings => Access Settings)")]
|
||||
public string Apikey { get; set; }
|
||||
|
||||
[FieldDefinition(3, Label = "Use Freeleech Tokens", HelpText = "Use freeleech tokens when available", Type = FieldType.Checkbox)]
|
||||
[FieldDefinition(3, Label = "Use Freeleech Tokens", Type = FieldType.Checkbox, HelpText = "Use freeleech tokens when available")]
|
||||
public bool UseFreeleechToken { get; set; }
|
||||
|
||||
[FieldDefinition(4, Label = "Freeload Only", Type = FieldType.Checkbox, Advanced = true, HelpTextWarning = "Search freeload torrents only. End date: 6 January 2024, 23:59 UTC.")]
|
||||
public bool FreeloadOnly { get; set; }
|
||||
|
||||
public override NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user