mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2026-04-16 21:35:04 -04:00
Compare commits
95 Commits
v1.9.2.399
...
v1.10.4.40
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
c664eaa9b5 | ||
|
|
b7e57f0c08 | ||
|
|
c06bf0e4ea | ||
|
|
c6db30c35a | ||
|
|
75c30dd318 | ||
|
|
6e7bf55dbd | ||
|
|
eb642dd2f9 | ||
|
|
19a196e2c7 | ||
|
|
93ec6cf89b | ||
|
|
52c6b56a4c | ||
|
|
82688d8a55 | ||
|
|
c81cbc801a | ||
|
|
993d189c61 | ||
|
|
1901af5a51 | ||
|
|
c1b399be39 | ||
|
|
2100e96570 | ||
|
|
3ff144421d | ||
|
|
f37ccba3f9 | ||
|
|
181cb2e0fe | ||
|
|
93c81bb7d3 | ||
|
|
7dd289b5f9 | ||
|
|
09cef8cf94 |
@@ -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,7 +9,7 @@ variables:
|
||||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '1.9.2'
|
||||
majorVersion: '1.10.4'
|
||||
minorVersion: $[counter('minorVersion', 1)]
|
||||
prowlarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
|
||||
class InfoInput extends Component {
|
||||
|
||||
@@ -7,12 +9,12 @@ class InfoInput extends Component {
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
value
|
||||
} = this.props;
|
||||
const { value } = this.props;
|
||||
|
||||
return (
|
||||
<span dangerouslySetInnerHTML={{ __html: value }} />
|
||||
<Alert kind={kinds.INFO}>
|
||||
<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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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',
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NzbDrone.Common.Serializer;
|
||||
|
||||
public class BooleanConverter : JsonConverter<bool>
|
||||
{
|
||||
public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
return reader.TokenType switch
|
||||
{
|
||||
JsonTokenType.True => true,
|
||||
JsonTokenType.False => false,
|
||||
JsonTokenType.Number => reader.GetInt64() switch
|
||||
{
|
||||
1 => true,
|
||||
0 => false,
|
||||
_ => throw new JsonException()
|
||||
},
|
||||
_ => throw new JsonException()
|
||||
};
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteBooleanValue(value);
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ namespace NzbDrone.Common.Serializer
|
||||
serializerSettings.Converters.Add(new STJTimeSpanConverter());
|
||||
serializerSettings.Converters.Add(new STJUtcConverter());
|
||||
serializerSettings.Converters.Add(new DictionaryStringObjectConverter());
|
||||
serializerSettings.Converters.Add(new BooleanConverter());
|
||||
}
|
||||
|
||||
public static T Deserialize<T>(string json)
|
||||
|
||||
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);
|
||||
|
||||
@@ -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,7 +39,7 @@ 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;
|
||||
|
||||
@@ -6,9 +6,8 @@ using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Indexers.PassThePopcorn;
|
||||
using NzbDrone.Core.Indexers.Definitions.PassThePopcorn;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
@@ -21,26 +20,22 @@ namespace NzbDrone.Core.Test.IndexerTests.PTPTests
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
Subject.Definition = new IndexerDefinition()
|
||||
Subject.Definition = new IndexerDefinition
|
||||
{
|
||||
Name = "PTP",
|
||||
Settings = new PassThePopcornSettings() { APIUser = "asdf", APIKey = "sad" }
|
||||
Settings = new PassThePopcornSettings
|
||||
{
|
||||
APIUser = "asdf",
|
||||
APIKey = "sad"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
[TestCase("Files/Indexers/PTP/imdbsearch.json")]
|
||||
public async Task should_parse_feed_from_PTP(string fileName)
|
||||
{
|
||||
var authResponse = new PassThePopcornAuthResponse { Result = "Ok" };
|
||||
|
||||
var authStream = new System.IO.StringWriter();
|
||||
Json.Serialize(authResponse, authStream);
|
||||
var responseJson = ReadAllText(fileName);
|
||||
|
||||
Mocker.GetMock<IIndexerHttpClient>()
|
||||
.Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Post), Subject.Definition))
|
||||
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), authStream.ToString())));
|
||||
|
||||
Mocker.GetMock<IIndexerHttpClient>()
|
||||
.Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get), Subject.Definition))
|
||||
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { ContentType = HttpAccept.Json.Value }, new CookieCollection(), responseJson)));
|
||||
|
||||
@@ -41,14 +41,16 @@ namespace NzbDrone.Core.Datastore
|
||||
|
||||
private static string 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)
|
||||
{
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -108,6 +108,7 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
GetNabElement("files", r.Files, protocol),
|
||||
GetNabElement("grabs", r.Grabs, protocol),
|
||||
GetNabElement("peers", t.Peers, protocol),
|
||||
r.Year == 0 ? null : GetNabElement("year", r.Year, protocol),
|
||||
GetNabElement("author", RemoveInvalidXMLChars(r.Author), protocol),
|
||||
GetNabElement("booktitle", RemoveInvalidXMLChars(r.BookTitle), protocol),
|
||||
GetNabElement("artist", RemoveInvalidXMLChars(r.Artist), protocol),
|
||||
|
||||
@@ -200,7 +200,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
var releaseInfos = new List<ReleaseInfo>();
|
||||
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
using var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
|
||||
var rows = dom.QuerySelectorAll("div#content table > tbody > tr");
|
||||
foreach (var row in rows)
|
||||
|
||||
@@ -77,7 +77,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
else
|
||||
{
|
||||
var parser = new HtmlParser();
|
||||
var document = await parser.ParseDocumentAsync(response.Content);
|
||||
using var document = await parser.ParseDocumentAsync(response.Content);
|
||||
var errorMessage = document.QuerySelector("#content .berror .berror_c")?.TextContent.Trim();
|
||||
|
||||
throw new IndexerAuthException(errorMessage ?? "Unknown error message, please report.");
|
||||
@@ -433,7 +433,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
var torrentInfos = new List<TorrentInfo>();
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
using var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
|
||||
foreach (var t in dom.QuerySelectorAll("#tabs .torrent_c > div"))
|
||||
{
|
||||
@@ -465,7 +465,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
var torrentInfos = new List<ReleaseInfo>();
|
||||
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
using var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
|
||||
var links = dom.QuerySelectorAll(".searchitem > h3 > a[href], #dle-content > .story > .story_h > .lcol > h2 > a[href]");
|
||||
foreach (var link in links)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -275,7 +275,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
var releaseInfos = new List<ReleaseInfo>();
|
||||
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
using var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
|
||||
var rows = dom.QuerySelectorAll("table tr");
|
||||
foreach (var (row, index) in rows.Skip(1).Select((v, i) => (v, i)))
|
||||
|
||||
@@ -253,7 +253,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
var torrentInfos = new List<TorrentInfo>();
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
using var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
|
||||
foreach (var t in dom.QuerySelectorAll("ul.media__tabs__nav > li > a"))
|
||||
{
|
||||
@@ -291,7 +291,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
var torrentInfos = new List<ReleaseInfo>();
|
||||
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
using var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
|
||||
var links = dom.QuerySelectorAll("a.ads-list__item__title");
|
||||
foreach (var link in links)
|
||||
|
||||
@@ -72,7 +72,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
if (CheckIfLoginNeeded(response))
|
||||
{
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(response.Content);
|
||||
using var dom = parser.ParseDocument(response.Content);
|
||||
var errorMessage = dom.QuerySelector("form#loginform")?.TextContent.Trim();
|
||||
|
||||
throw new IndexerAuthException(errorMessage ?? "Unknown error message, please report.");
|
||||
@@ -206,7 +206,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
var torrentInfos = new List<ReleaseInfo>();
|
||||
|
||||
var parser = new HtmlParser();
|
||||
var doc = parser.ParseDocument(indexerResponse.Content);
|
||||
using var doc = parser.ParseDocument(indexerResponse.Content);
|
||||
var rows = doc.QuerySelectorAll("table.torrent_table > tbody > tr.torrent");
|
||||
foreach (var row in rows)
|
||||
{
|
||||
|
||||
@@ -37,7 +37,7 @@ public class AroLol : GazelleBase<AroLolSettings>
|
||||
if (response.Content.Contains("loginform"))
|
||||
{
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(response.Content);
|
||||
using var dom = parser.ParseDocument(response.Content);
|
||||
var errorMessage = dom.QuerySelector("#loginform > .warning")?.TextContent.Trim();
|
||||
|
||||
throw new IndexerAuthException(errorMessage ?? "Unknown error message, please report.");
|
||||
|
||||
@@ -88,7 +88,7 @@ public class AudioBookBay : TorrentIndexerBase<NoAuthTorrentBaseSettings>
|
||||
var response = await _httpClient.ExecuteProxiedAsync(request, Definition);
|
||||
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(response.Content);
|
||||
using var dom = parser.ParseDocument(response.Content);
|
||||
|
||||
var hash = dom.QuerySelector("td:contains(\"Info Hash:\") ~ td")?.TextContent.Trim();
|
||||
if (hash == null)
|
||||
@@ -269,7 +269,7 @@ public class AudioBookBayParser : IParseIndexerResponse
|
||||
{
|
||||
var releaseInfos = new List<ReleaseInfo>();
|
||||
|
||||
var doc = ParseHtmlDocument(indexerResponse.Content);
|
||||
using var doc = ParseHtmlDocument(indexerResponse.Content);
|
||||
|
||||
var rows = doc.QuerySelectorAll("div.post:has(div[class=\"postTitle\"])");
|
||||
foreach (var row in rows)
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
||||
{
|
||||
@@ -9,34 +9,34 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
||||
public string Download { get; set; }
|
||||
public Dictionary<string, string> Category { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "movie_tv")]
|
||||
[JsonPropertyName("movie_tv")]
|
||||
public AvistazIdInfo MovieTvinfo { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "created_at")]
|
||||
[JsonPropertyName("created_at")]
|
||||
public string CreatedAt { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "file_name")]
|
||||
[JsonPropertyName("file_name")]
|
||||
public string FileName { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "info_hash")]
|
||||
[JsonPropertyName("info_hash")]
|
||||
public string InfoHash { get; set; }
|
||||
public int? Leech { get; set; }
|
||||
public int? Completed { get; set; }
|
||||
public int? Seed { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "file_size")]
|
||||
[JsonPropertyName("file_size")]
|
||||
public long? FileSize { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "file_count")]
|
||||
[JsonPropertyName("file_count")]
|
||||
public int? FileCount { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "download_multiply")]
|
||||
[JsonPropertyName("download_multiply")]
|
||||
public double? DownloadMultiply { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "upload_multiply")]
|
||||
[JsonPropertyName("upload_multiply")]
|
||||
public double? UploadMultiply { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "video_quality")]
|
||||
[JsonPropertyName("video_quality")]
|
||||
public string VideoQuality { get; set; }
|
||||
public string Type { get; set; }
|
||||
public List<AvistazLanguage> Audio { get; set; }
|
||||
@@ -64,21 +64,10 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
||||
public string Tmdb { get; set; }
|
||||
public string Tvdb { get; set; }
|
||||
public string Imdb { get; set; }
|
||||
public string Title { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "tv_episode")]
|
||||
public string TvEpisode { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "tv_season")]
|
||||
public string TVSeason { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "tv_full_season")]
|
||||
public bool TVFullSeason { get; set; }
|
||||
}
|
||||
|
||||
public class AvistazAuthResponse
|
||||
{
|
||||
public string Token { get; set; }
|
||||
public string Expiry { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Threading.Tasks;
|
||||
using FluentValidation.Results;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
@@ -16,7 +17,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
||||
public override bool SupportsSearch => true;
|
||||
public override bool SupportsPagination => true;
|
||||
public override int PageSize => 50;
|
||||
public override TimeSpan RateLimit => TimeSpan.FromSeconds(4);
|
||||
public override TimeSpan RateLimit => TimeSpan.FromSeconds(5);
|
||||
public override IndexerCapabilities Capabilities => SetCapabilities();
|
||||
protected virtual string LoginUrl => Settings.BaseUrl + "api/v1/jackett/auth";
|
||||
private IIndexerRepository _indexerRepository;
|
||||
@@ -122,10 +123,11 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
||||
.Accept(HttpAccept.Json)
|
||||
.Build();
|
||||
|
||||
var response = await _httpClient.PostAsync<AvistazAuthResponse>(authLoginRequest);
|
||||
var token = response.Resource.Token;
|
||||
var response = await ExecuteAuth(authLoginRequest);
|
||||
|
||||
return token;
|
||||
var authResponse = STJson.Deserialize<AvistazAuthResponse>(response.Content);
|
||||
|
||||
return authResponse.Token;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Linq;
|
||||
using System.Net;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Indexers.Exceptions;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
@@ -13,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; }
|
||||
@@ -42,9 +43,9 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
||||
throw new IndexerException(indexerResponse, $"Unexpected response header {indexerResponse.HttpResponse.Headers.ContentType} from indexer request, expected {HttpAccept.Json.Value}");
|
||||
}
|
||||
|
||||
var jsonResponse = new HttpResponse<AvistazResponse>(indexerResponse.HttpResponse);
|
||||
var jsonResponse = STJson.Deserialize<AvistazResponse>(indexerResponse.HttpResponse.Content);
|
||||
|
||||
foreach (var row in jsonResponse.Resource.Data)
|
||||
foreach (var row in jsonResponse.Data)
|
||||
{
|
||||
var details = row.Url;
|
||||
var link = row.Download;
|
||||
|
||||
@@ -72,7 +72,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
if (CheckIfLoginNeeded(response))
|
||||
{
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(response.Content);
|
||||
using var dom = parser.ParseDocument(response.Content);
|
||||
var messageEl = dom.QuerySelectorAll("#loginform");
|
||||
var messages = new List<string>();
|
||||
for (var i = 0; i < 13; i++)
|
||||
@@ -242,7 +242,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
var torrentInfos = new List<ReleaseInfo>();
|
||||
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
using var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
var rows = dom.QuerySelectorAll("#torrent_table > tbody > tr.torrent");
|
||||
|
||||
foreach (var row in rows)
|
||||
|
||||
@@ -55,7 +55,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
var response = await _httpClient.ExecuteProxiedAsync(request, Definition);
|
||||
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(response.Content);
|
||||
using var dom = parser.ParseDocument(response.Content);
|
||||
var downloadLink = dom.QuerySelector(".download_link")?.GetAttribute("href");
|
||||
|
||||
if (downloadLink.IsNullOrWhiteSpace())
|
||||
@@ -82,7 +82,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
var loginPage = await ExecuteAuth(new HttpRequest(loginUrl));
|
||||
|
||||
var parser = new HtmlParser();
|
||||
var dom = await parser.ParseDocumentAsync(loginPage.Content);
|
||||
using var dom = await parser.ParseDocumentAsync(loginPage.Content);
|
||||
var loginKey = dom.QuerySelector("input[name=\"loginKey\"]");
|
||||
if (loginKey != null)
|
||||
{
|
||||
@@ -102,7 +102,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
if (CheckIfLoginNeeded(response))
|
||||
{
|
||||
var htmlParser = new HtmlParser();
|
||||
var document = await htmlParser.ParseDocumentAsync(response.Content);
|
||||
using var document = await htmlParser.ParseDocumentAsync(response.Content);
|
||||
var errorMessage = document.QuerySelector("#loginError, .error")?.TextContent.Trim();
|
||||
|
||||
throw new IndexerAuthException(errorMessage ?? "Unknown error message, please report.");
|
||||
@@ -247,7 +247,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
var releaseInfos = new List<ReleaseInfo>();
|
||||
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
using var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
var rows = dom.QuerySelectorAll(".torrents tr.torrent, .torrents tr.torrent_alt");
|
||||
var currentCategories = new List<IndexerCategory> { NewznabStandardCategory.TVAnime };
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json.Serialization;
|
||||
using FluentValidation;
|
||||
using Newtonsoft.Json;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
@@ -19,6 +19,7 @@ using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Validation;
|
||||
using static Newtonsoft.Json.Formatting;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
@@ -129,7 +130,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())
|
||||
@@ -142,7 +143,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
if (searchTypes.Any())
|
||||
{
|
||||
body.Add("types", string.Join(",", searchTypes));
|
||||
body.Add("types", searchTypes.ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,7 +164,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
Method = HttpMethod.Post
|
||||
};
|
||||
request.SetContent(body.ToJson());
|
||||
request.ContentSummary = body.ToJson(Formatting.None);
|
||||
request.ContentSummary = body.ToJson(None);
|
||||
|
||||
yield return new IndexerRequest(request);
|
||||
}
|
||||
@@ -245,16 +246,16 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
throw new IndexerAuthException("API Key invalid or not authorized");
|
||||
}
|
||||
|
||||
var jsonResponse = new HttpResponse<BeyondHDResponse>(indexerHttpResponse);
|
||||
var jsonResponse = STJson.Deserialize<BeyondHDResponse>(indexerResponse.Content);
|
||||
|
||||
if (jsonResponse.Resource.StatusCode == 0)
|
||||
if (jsonResponse.StatusCode == 0)
|
||||
{
|
||||
throw new IndexerException(indexerResponse, $"Indexer Error: {jsonResponse.Resource.StatusMessage}");
|
||||
throw new IndexerException(indexerResponse, $"Indexer Error: {jsonResponse.StatusMessage}");
|
||||
}
|
||||
|
||||
var releaseInfos = new List<ReleaseInfo>();
|
||||
|
||||
foreach (var row in jsonResponse.Resource.Results)
|
||||
foreach (var row in jsonResponse.Results)
|
||||
{
|
||||
var details = row.InfoUrl;
|
||||
var link = row.DownloadLink;
|
||||
@@ -412,10 +413,10 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
public class BeyondHDResponse
|
||||
{
|
||||
[JsonProperty(PropertyName = "status_code")]
|
||||
[JsonPropertyName("status_code")]
|
||||
public int StatusCode { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "status_message")]
|
||||
[JsonPropertyName("status_message")]
|
||||
public string StatusMessage { get; set; }
|
||||
public List<BeyondHDTorrent> Results { get; set; }
|
||||
}
|
||||
@@ -424,36 +425,42 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "info_hash")]
|
||||
[JsonPropertyName("info_hash")]
|
||||
public string InfoHash { get; set; }
|
||||
public string Category { get; set; }
|
||||
public string Type { get; set; }
|
||||
public long Size { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "times_completed")]
|
||||
[JsonPropertyName("times_completed")]
|
||||
public int Grabs { get; set; }
|
||||
public int Seeders { get; set; }
|
||||
public int Leechers { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "created_at")]
|
||||
[JsonPropertyName("created_at")]
|
||||
public string CreatedAt { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "download_url")]
|
||||
[JsonPropertyName("download_url")]
|
||||
public string DownloadLink { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "url")]
|
||||
[JsonPropertyName("url")]
|
||||
public string InfoUrl { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "imdb_id")]
|
||||
[JsonPropertyName("imdb_id")]
|
||||
public string ImdbId { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "tmdb_id")]
|
||||
[JsonPropertyName("tmdb_id")]
|
||||
public string TmdbId { get; set; }
|
||||
|
||||
public bool Freeleech { get; set; }
|
||||
|
||||
public bool Promo25 { get; set; }
|
||||
|
||||
public bool Promo50 { get; set; }
|
||||
|
||||
public bool Promo75 { get; set; }
|
||||
|
||||
public bool Limited { get; set; }
|
||||
|
||||
public bool Internal { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,7 +161,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
var releaseInfos = new List<ReleaseInfo>();
|
||||
|
||||
var parser = new HtmlParser();
|
||||
var doc = parser.ParseDocument(indexerResponse.Content);
|
||||
using var doc = parser.ParseDocument(indexerResponse.Content);
|
||||
var rows = doc.QuerySelectorAll("table.xMenuT > tbody > tr").Skip(1);
|
||||
foreach (var row in rows)
|
||||
{
|
||||
|
||||
@@ -185,7 +185,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
var torrentInfos = new List<ReleaseInfo>();
|
||||
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
using var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
foreach (var child in dom.QuerySelectorAll("#needseed"))
|
||||
{
|
||||
child.Remove();
|
||||
|
||||
@@ -598,7 +598,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann
|
||||
}
|
||||
|
||||
var resultParser = new HtmlParser();
|
||||
var resultDocument = resultParser.ParseDocument(loginResult.Content);
|
||||
using var resultDocument = resultParser.ParseDocument(loginResult.Content);
|
||||
foreach (var error in errorBlocks)
|
||||
{
|
||||
var selection = resultDocument.QuerySelector(error.Selector);
|
||||
@@ -984,7 +984,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann
|
||||
var selectorText = ApplyGoTemplateText(selector.Selector, variables);
|
||||
var parser = new HtmlParser();
|
||||
|
||||
var resultDocument = parser.ParseDocument(response.Content);
|
||||
using var resultDocument = parser.ParseDocument(response.Content);
|
||||
|
||||
var element = resultDocument.QuerySelector(selectorText);
|
||||
if (element == null)
|
||||
@@ -1045,7 +1045,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann
|
||||
if (_definition.Login.Test?.Selector != null && (response.Headers.ContentType?.Contains("text/html") ?? true))
|
||||
{
|
||||
var parser = new HtmlParser();
|
||||
var document = parser.ParseDocument(response.Content);
|
||||
using var document = parser.ParseDocument(response.Content);
|
||||
|
||||
var selection = document.QuerySelectorAll(_definition.Login.Test.Selector);
|
||||
if (selection.Length == 0)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -8,12 +8,12 @@ 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;
|
||||
|
||||
@@ -1,28 +1,40 @@
|
||||
using Newtonsoft.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions.FileList;
|
||||
|
||||
public class FileListTorrent
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public uint Id { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public long Size { get; set; }
|
||||
|
||||
public int Leechers { get; set; }
|
||||
|
||||
public int Seeders { get; set; }
|
||||
[JsonProperty(PropertyName = "times_completed")]
|
||||
|
||||
[JsonPropertyName("times_completed")]
|
||||
public uint TimesCompleted { get; set; }
|
||||
public uint Comments { get; set; }
|
||||
|
||||
public uint Files { get; set; }
|
||||
[JsonProperty(PropertyName = "imdb")]
|
||||
|
||||
[JsonPropertyName("imdb")]
|
||||
public string ImdbId { get; set; }
|
||||
|
||||
public bool Internal { get; set; }
|
||||
[JsonProperty(PropertyName = "freeleech")]
|
||||
|
||||
[JsonPropertyName("freeleech")]
|
||||
public bool FreeLeech { get; set; }
|
||||
[JsonProperty(PropertyName = "doubleup")]
|
||||
|
||||
[JsonPropertyName("doubleup")]
|
||||
public bool DoubleUp { get; set; }
|
||||
[JsonProperty(PropertyName = "upload_date")]
|
||||
|
||||
[JsonPropertyName("upload_date")]
|
||||
public string UploadDate { get; set; }
|
||||
|
||||
public string Category { get; set; }
|
||||
[JsonProperty(PropertyName = "small_description")]
|
||||
|
||||
[JsonPropertyName("small_description")]
|
||||
public string SmallDescription { get; set; }
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using Newtonsoft.Json;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Indexers.Exceptions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
@@ -35,7 +35,7 @@ public class FileListParser : IParseIndexerResponse
|
||||
|
||||
var releaseInfos = new List<ReleaseInfo>();
|
||||
|
||||
var results = JsonConvert.DeserializeObject<List<FileListTorrent>>(indexerResponse.Content);
|
||||
var results = STJson.Deserialize<List<FileListTorrent>>(indexerResponse.Content);
|
||||
|
||||
foreach (var row in results)
|
||||
{
|
||||
@@ -54,7 +54,7 @@ public class FileListParser : IParseIndexerResponse
|
||||
}
|
||||
|
||||
var imdbId = 0;
|
||||
if (row.ImdbId != null && row.ImdbId.Length > 2)
|
||||
if (row.ImdbId is { Length: > 2 })
|
||||
{
|
||||
imdbId = int.Parse(row.ImdbId.Substring(2));
|
||||
}
|
||||
@@ -64,7 +64,7 @@ public class FileListParser : IParseIndexerResponse
|
||||
|
||||
releaseInfos.Add(new TorrentInfo
|
||||
{
|
||||
Guid = string.Format("FileList-{0}", id),
|
||||
Guid = $"FileList-{id}",
|
||||
Title = row.Name,
|
||||
Size = row.Size,
|
||||
Categories = _categories.MapTrackerCatDescToNewznab(row.Category),
|
||||
@@ -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,
|
||||
@@ -91,21 +91,21 @@ public class FileListParser : IParseIndexerResponse
|
||||
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
|
||||
private string GetDownloadUrl(string torrentId)
|
||||
private string GetDownloadUrl(uint torrentId)
|
||||
{
|
||||
var url = new HttpUri(_settings.BaseUrl)
|
||||
.CombinePath("/download.php")
|
||||
.AddQueryParam("id", torrentId)
|
||||
.AddQueryParam("id", torrentId.ToString())
|
||||
.AddQueryParam("passkey", _settings.Passkey);
|
||||
|
||||
return url.FullUri;
|
||||
}
|
||||
|
||||
private string GetInfoUrl(string torrentId)
|
||||
private string GetInfoUrl(uint torrentId)
|
||||
{
|
||||
var url = new HttpUri(_settings.BaseUrl)
|
||||
.CombinePath("/details.php")
|
||||
.AddQueryParam("id", torrentId);
|
||||
.AddQueryParam("id", torrentId.ToString());
|
||||
|
||||
return url.FullUri;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ public class FunFile : TorrentIndexerBase<UserPassTorrentBaseSettings>
|
||||
if (CheckIfLoginNeeded(response))
|
||||
{
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(response.Content);
|
||||
using var dom = await parser.ParseDocumentAsync(response.Content);
|
||||
var errorMessage = dom.QuerySelector("td.mf_content")?.TextContent.Trim();
|
||||
|
||||
throw new IndexerAuthException(errorMessage ?? "Unknown error message, please report.");
|
||||
@@ -276,7 +276,7 @@ public class FunFileParser : IParseIndexerResponse
|
||||
var releaseInfos = new List<TorrentInfo>();
|
||||
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
using var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
|
||||
var rows = dom.QuerySelectorAll("table.mainframe table[cellpadding=\"2\"] > tbody > tr:has(td.row3)");
|
||||
foreach (var row in rows)
|
||||
|
||||
@@ -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 = false;
|
||||
}
|
||||
|
||||
[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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
if (CheckIfLoginNeeded(response))
|
||||
{
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(response.Content);
|
||||
using var dom = parser.ParseDocument(response.Content);
|
||||
var errorMessages = dom
|
||||
.QuerySelectorAll("table.lista td.lista span[style*=\"#FF0000\"], table.lista td.header:contains(\"login attempts\")")
|
||||
.Select(r => r.TextContent.Trim())
|
||||
|
||||
@@ -74,7 +74,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
if (response.Content != null && !response.Content.ContainsIgnoreCase("If your browser doesn't have javascript enabled"))
|
||||
{
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(response.Content);
|
||||
using var dom = await parser.ParseDocumentAsync(response.Content);
|
||||
|
||||
var errorMessage = dom.QuerySelector("div > font[color=\"#FF0000\"]")?.TextContent.Trim();
|
||||
|
||||
@@ -256,7 +256,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
var releaseInfos = new List<ReleaseInfo>();
|
||||
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
using var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
|
||||
var userInfo = dom.QuerySelector("table.navus tr");
|
||||
var userRank = userInfo?.Children[1].TextContent.Replace("Rank:", string.Empty).Trim();
|
||||
|
||||
@@ -291,7 +291,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
var torrentInfos = new List<ReleaseInfo>();
|
||||
|
||||
var parser = new HtmlParser();
|
||||
var doc = parser.ParseDocument(indexerResponse.Content);
|
||||
using var doc = parser.ParseDocument(indexerResponse.Content);
|
||||
|
||||
var rows = doc.QuerySelectorAll("table[id=\"torrents\"] > tbody > tr");
|
||||
foreach (var row in rows)
|
||||
|
||||
@@ -266,7 +266,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
var releaseInfos = new List<ReleaseInfo>();
|
||||
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
using var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
|
||||
var rows = dom.QuerySelectorAll("table#sortabletable > tbody > tr:has(a[href*=\"details.php?id=\"])");
|
||||
foreach (var row in rows)
|
||||
|
||||
@@ -77,7 +77,7 @@ public class Libble : TorrentIndexerBase<LibbleSettings>
|
||||
if (CheckIfLoginNeeded(response))
|
||||
{
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(response.Content);
|
||||
using var dom = parser.ParseDocument(response.Content);
|
||||
var errorMessage = dom.QuerySelector("#loginform > .warning")?.TextContent.Trim();
|
||||
|
||||
throw new IndexerAuthException(errorMessage ?? "Unknown error message, please report.");
|
||||
@@ -238,7 +238,7 @@ public class LibbleParser : IParseIndexerResponse
|
||||
var releaseInfos = new List<ReleaseInfo>();
|
||||
|
||||
var parser = new HtmlParser();
|
||||
var doc = parser.ParseDocument(indexerResponse.Content);
|
||||
using var doc = parser.ParseDocument(indexerResponse.Content);
|
||||
|
||||
var groups = doc.QuerySelectorAll("table#torrent_table > tbody > tr.group:has(strong > a[href*=\"torrents.php?id=\"])");
|
||||
foreach (var group in groups)
|
||||
|
||||
@@ -177,7 +177,7 @@ public class MoreThanTVParser : IParseIndexerResponse
|
||||
try
|
||||
{
|
||||
var parser = new HtmlParser();
|
||||
var document = parser.ParseDocument(indexerResponse.Content);
|
||||
using var document = parser.ParseDocument(indexerResponse.Content);
|
||||
var torrents = document.QuerySelectorAll("#torrent_table > tbody > tr.torrent");
|
||||
var movies = new[] { "movie" };
|
||||
var tv = new[] { "season", "episode" };
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Indexers.Exceptions;
|
||||
@@ -65,6 +68,26 @@ 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);
|
||||
|
||||
return FilterReleasesByQuery(cleanReleases, searchCriteria).ToList();
|
||||
}
|
||||
|
||||
protected override IEnumerable<ReleaseInfo> FilterReleasesByQuery(IEnumerable<ReleaseInfo> releases, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
if (!searchCriteria.IsRssSearch &&
|
||||
searchCriteria.IsIdSearch &&
|
||||
searchCriteria is TvSearchCriteria tvSearchCriteria &&
|
||||
tvSearchCriteria.EpisodeSearchString.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
releases = releases.Where(r => r.Title.IsNotNullOrWhiteSpace() && r.Title.ContainsIgnoreCase(tvSearchCriteria.EpisodeSearchString)).ToList();
|
||||
}
|
||||
|
||||
return releases;
|
||||
}
|
||||
|
||||
private IndexerCapabilities SetCapabilities()
|
||||
{
|
||||
var caps = new IndexerCapabilities
|
||||
@@ -208,7 +231,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from indexer request");
|
||||
}
|
||||
|
||||
var jsonResponse = new HttpResponse<JsonRpcResponse<NebulanceTorrents>>(indexerResponse.HttpResponse).Resource;
|
||||
var jsonResponse = STJson.Deserialize<JsonRpcResponse<NebulanceTorrents>>(indexerResponse.HttpResponse.Content);
|
||||
|
||||
if (jsonResponse.Error != null || jsonResponse.Result == null)
|
||||
{
|
||||
@@ -242,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);
|
||||
}
|
||||
|
||||
@@ -299,25 +328,28 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
public class NebulanceTorrent
|
||||
{
|
||||
[JsonProperty(PropertyName = "rls_name")]
|
||||
[JsonPropertyName("rls_name")]
|
||||
public string ReleaseTitle { get; set; }
|
||||
[JsonProperty(PropertyName = "cat")]
|
||||
[JsonPropertyName("cat")]
|
||||
public string Category { get; set; }
|
||||
public string Size { get; set; }
|
||||
public string Seed { get; set; }
|
||||
public string Leech { get; set; }
|
||||
public string Snatch { get; set; }
|
||||
public string Download { get; set; }
|
||||
[JsonProperty(PropertyName = "file_list")]
|
||||
[JsonPropertyName("file_list")]
|
||||
public string[] FileList { get; set; }
|
||||
[JsonProperty(PropertyName = "group_name")]
|
||||
[JsonPropertyName("group_name")]
|
||||
public string GroupName { get; set; }
|
||||
[JsonProperty(PropertyName = "series_banner")]
|
||||
[JsonPropertyName("series_banner")]
|
||||
public string Banner { get; set; }
|
||||
[JsonProperty(PropertyName = "group_id")]
|
||||
[JsonPropertyName("group_id")]
|
||||
public string TorrentId { get; set; }
|
||||
[JsonProperty(PropertyName = "rls_utc")]
|
||||
[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
|
||||
};
|
||||
@@ -102,10 +103,7 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -263,7 +263,7 @@ public class NorBitsParser : IParseIndexerResponse
|
||||
var releaseInfos = new List<ReleaseInfo>();
|
||||
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
using var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
|
||||
var rows = dom.QuerySelectorAll("#torrentTable > tbody > tr").Skip(1).ToCollection();
|
||||
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -3,7 +3,7 @@ using NLog;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.PassThePopcorn
|
||||
namespace NzbDrone.Core.Indexers.Definitions.PassThePopcorn
|
||||
{
|
||||
public class PassThePopcorn : TorrentIndexerBase<PassThePopcornSettings>
|
||||
{
|
||||
@@ -29,21 +29,23 @@ namespace NzbDrone.Core.Indexers.PassThePopcorn
|
||||
|
||||
public override IIndexerRequestGenerator GetRequestGenerator()
|
||||
{
|
||||
return new PassThePopcornRequestGenerator
|
||||
{
|
||||
Settings = Settings,
|
||||
HttpClient = _httpClient,
|
||||
Logger = _logger
|
||||
};
|
||||
return new PassThePopcornRequestGenerator(Settings, Capabilities);
|
||||
}
|
||||
|
||||
public override IParseIndexerResponse GetParser()
|
||||
{
|
||||
return new PassThePopcornParser(Settings);
|
||||
}
|
||||
|
||||
private IndexerCapabilities SetCapabilities()
|
||||
{
|
||||
var caps = new IndexerCapabilities
|
||||
{
|
||||
SearchParams = new List<SearchParam>
|
||||
LimitsDefault = PageSize,
|
||||
LimitsMax = PageSize,
|
||||
TvSearchParams = new List<TvSearchParam>
|
||||
{
|
||||
SearchParam.Q
|
||||
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep, TvSearchParam.ImdbId
|
||||
},
|
||||
MovieSearchParams = new List<MovieSearchParam>
|
||||
{
|
||||
@@ -58,31 +60,19 @@ namespace NzbDrone.Core.Indexers.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;
|
||||
}
|
||||
|
||||
public override IParseIndexerResponse GetParser()
|
||||
{
|
||||
return new PassThePopcornParser(Settings, Capabilities, _logger);
|
||||
}
|
||||
}
|
||||
|
||||
public class PassThePopcornFlag : IndexerFlag
|
||||
{
|
||||
public static IndexerFlag Golden => new IndexerFlag("golden", "Release follows Golden Popcorn quality rules");
|
||||
public static IndexerFlag Approved => new IndexerFlag("approved", "Release approved by PTP");
|
||||
public static IndexerFlag Golden => new ("golden", "Release follows Golden Popcorn quality rules");
|
||||
public static IndexerFlag Approved => new ("approved", "Release approved by PTP");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,28 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.PassThePopcorn
|
||||
namespace NzbDrone.Core.Indexers.Definitions.PassThePopcorn
|
||||
{
|
||||
public class Director
|
||||
public class PassThePopcornResponse
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Id { get; set; }
|
||||
public string TotalResults { get; set; }
|
||||
public List<PassThePopcornMovie> Movies { get; set; }
|
||||
public string Page { get; set; }
|
||||
public string AuthKey { get; set; }
|
||||
public string PassKey { get; set; }
|
||||
}
|
||||
|
||||
public class Torrent
|
||||
public class PassThePopcornMovie
|
||||
{
|
||||
public string GroupId { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string Year { get; set; }
|
||||
public string Cover { get; set; }
|
||||
public List<string> Tags { get; set; }
|
||||
public string ImdbId { get; set; }
|
||||
public List<PassThePopcornTorrent> Torrents { get; set; }
|
||||
}
|
||||
|
||||
public class PassThePopcornTorrent
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Quality { get; set; }
|
||||
@@ -19,7 +32,7 @@ namespace NzbDrone.Core.Indexers.PassThePopcorn
|
||||
public string Resolution { get; set; }
|
||||
public bool Scene { get; set; }
|
||||
public string Size { get; set; }
|
||||
public DateTime UploadTime { get; set; }
|
||||
public string UploadTime { get; set; }
|
||||
public string RemasterTitle { get; set; }
|
||||
public string Snatched { get; set; }
|
||||
public string Seeders { get; set; }
|
||||
@@ -29,32 +42,4 @@ namespace NzbDrone.Core.Indexers.PassThePopcorn
|
||||
public bool GoldenPopcorn { get; set; }
|
||||
public string FreeleechType { get; set; }
|
||||
}
|
||||
|
||||
public class Movie
|
||||
{
|
||||
public string GroupId { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string Year { get; set; }
|
||||
public string Cover { get; set; }
|
||||
public List<string> Tags { get; set; }
|
||||
public List<Director> Directors { get; set; }
|
||||
public string ImdbId { get; set; }
|
||||
public List<Torrent> Torrents { get; set; }
|
||||
}
|
||||
|
||||
public class PassThePopcornResponse
|
||||
{
|
||||
public string TotalResults { get; set; }
|
||||
public List<Movie> Movies { get; set; }
|
||||
public string Page { get; set; }
|
||||
public string AuthKey { get; set; }
|
||||
public string PassKey { get; set; }
|
||||
}
|
||||
|
||||
public class PassThePopcornAuthResponse
|
||||
{
|
||||
public string Result { get; set; }
|
||||
public string Popcron { get; set; }
|
||||
public string AntiCsrfToken { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +1,36 @@
|
||||
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;
|
||||
using NzbDrone.Core.Indexers.Exceptions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.PassThePopcorn
|
||||
namespace NzbDrone.Core.Indexers.Definitions.PassThePopcorn
|
||||
{
|
||||
public class PassThePopcornParser : IParseIndexerResponse
|
||||
{
|
||||
private readonly IndexerCapabilities _capabilities;
|
||||
private readonly PassThePopcornSettings _settings;
|
||||
private readonly Logger _logger;
|
||||
public PassThePopcornParser(PassThePopcornSettings settings, IndexerCapabilities capabilities, Logger logger)
|
||||
|
||||
private static Regex SeasonRegex => new (@"\bS\d{2,3}(E\d{2,3})?\b", RegexOptions.Compiled);
|
||||
|
||||
public PassThePopcornParser(PassThePopcornSettings settings)
|
||||
{
|
||||
_settings = settings;
|
||||
_capabilities = capabilities;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
||||
{
|
||||
var torrentInfos = new List<ReleaseInfo>();
|
||||
var indexerHttpResponse = indexerResponse.HttpResponse;
|
||||
|
||||
if (indexerHttpResponse.StatusCode != HttpStatusCode.OK)
|
||||
var httpResponse = indexerResponse.HttpResponse;
|
||||
|
||||
if (httpResponse.StatusCode != HttpStatusCode.OK)
|
||||
{
|
||||
// Remove cookie cache
|
||||
if (indexerHttpResponse.HasHttpRedirect && indexerHttpResponse.RedirectUrl
|
||||
.ContainsIgnoreCase("login.php"))
|
||||
{
|
||||
CookiesUpdater(null, null);
|
||||
throw new IndexerAuthException("We are being redirected to the PTP login page. Most likely your session expired or was killed. Recheck your cookie or credentials and try testing the indexer.");
|
||||
}
|
||||
|
||||
if (indexerHttpResponse.StatusCode == HttpStatusCode.Forbidden)
|
||||
if (httpResponse.StatusCode == HttpStatusCode.Forbidden)
|
||||
{
|
||||
throw new RequestLimitReachedException(indexerResponse, "PTP Query Limit Reached. Please try again later.");
|
||||
}
|
||||
@@ -45,19 +38,13 @@ namespace NzbDrone.Core.Indexers.PassThePopcorn
|
||||
throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from indexer request");
|
||||
}
|
||||
|
||||
if (indexerHttpResponse.Headers.ContentType != HttpAccept.Json.Value)
|
||||
if (httpResponse.Headers.ContentType != HttpAccept.Json.Value)
|
||||
{
|
||||
if (indexerHttpResponse.Request.Url.Path.ContainsIgnoreCase("login.php"))
|
||||
{
|
||||
CookiesUpdater(null, null);
|
||||
throw new IndexerAuthException("We are currently on the login page. Most likely your session expired or was killed. Try testing the indexer in the settings.");
|
||||
}
|
||||
|
||||
// Remove cookie cache
|
||||
throw new IndexerException(indexerResponse, $"Unexpected response header {indexerResponse.HttpResponse.Headers.ContentType} from indexer request, expected {HttpAccept.Json.Value}");
|
||||
}
|
||||
|
||||
var jsonResponse = STJson.Deserialize<PassThePopcornResponse>(indexerResponse.Content);
|
||||
|
||||
if (jsonResponse.TotalResults == "0" ||
|
||||
jsonResponse.TotalResults.IsNullOrWhiteSpace() ||
|
||||
jsonResponse.Movies == null)
|
||||
@@ -81,51 +68,39 @@ namespace NzbDrone.Core.Indexers.PassThePopcorn
|
||||
|
||||
if (torrent.Checked)
|
||||
{
|
||||
flags.Add(PassThePopcornFlag.Approved); //title = $"{title} ✔";
|
||||
flags.Add(PassThePopcornFlag.Approved);
|
||||
}
|
||||
|
||||
if (torrent.Scene)
|
||||
var categories = new List<IndexerCategory> { NewznabStandardCategory.Movies };
|
||||
|
||||
if (title != null && SeasonRegex.Match(title).Success)
|
||||
{
|
||||
flags.Add(IndexerFlag.Scene);
|
||||
categories.Add(NewznabStandardCategory.TV);
|
||||
}
|
||||
|
||||
var free = !(torrent.FreeleechType is null);
|
||||
|
||||
// Only add approved torrents
|
||||
try
|
||||
torrentInfos.Add(new TorrentInfo
|
||||
{
|
||||
torrentInfos.Add(new TorrentInfo
|
||||
{
|
||||
Guid = string.Format("PassThePopcorn-{0}", id),
|
||||
Title = title,
|
||||
Size = long.Parse(torrent.Size),
|
||||
DownloadUrl = GetDownloadUrl(id, jsonResponse.AuthKey, jsonResponse.PassKey),
|
||||
InfoUrl = GetInfoUrl(result.GroupId, id),
|
||||
Grabs = int.Parse(torrent.Snatched),
|
||||
Seeders = int.Parse(torrent.Seeders),
|
||||
Peers = int.Parse(torrent.Leechers) + int.Parse(torrent.Seeders),
|
||||
PublishDate = torrent.UploadTime.ToUniversalTime(),
|
||||
ImdbId = result.ImdbId.IsNotNullOrWhiteSpace() ? int.Parse(result.ImdbId) : 0,
|
||||
IndexerFlags = flags,
|
||||
MinimumRatio = 1,
|
||||
MinimumSeedTime = 345600,
|
||||
DownloadVolumeFactor = free ? 0 : 1,
|
||||
UploadVolumeFactor = 1,
|
||||
Categories = new List<IndexerCategory> { NewznabStandardCategory.Movies }
|
||||
});
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error(e, "Encountered exception parsing PTP torrent: {" +
|
||||
$"Size: {torrent.Size}" +
|
||||
$"UploadTime: {torrent.UploadTime}" +
|
||||
$"Seeders: {torrent.Seeders}" +
|
||||
$"Leechers: {torrent.Leechers}" +
|
||||
$"ReleaseName: {torrent.ReleaseName}" +
|
||||
$"ID: {torrent.Id}" +
|
||||
"}. Please immediately report this info on https://github.com/Prowlarr/Prowlarr/issues/1584.");
|
||||
throw;
|
||||
}
|
||||
Guid = $"PassThePopcorn-{id}",
|
||||
Title = title,
|
||||
Year = int.Parse(result.Year),
|
||||
InfoUrl = GetInfoUrl(result.GroupId, id),
|
||||
DownloadUrl = GetDownloadUrl(id, jsonResponse.AuthKey, jsonResponse.PassKey),
|
||||
Categories = categories,
|
||||
Size = long.Parse(torrent.Size),
|
||||
Grabs = int.Parse(torrent.Snatched),
|
||||
Seeders = int.Parse(torrent.Seeders),
|
||||
Peers = int.Parse(torrent.Leechers) + int.Parse(torrent.Seeders),
|
||||
PublishDate = DateTime.Parse(torrent.UploadTime + " +0000", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal),
|
||||
ImdbId = result.ImdbId.IsNotNullOrWhiteSpace() ? int.Parse(result.ImdbId) : 0,
|
||||
Scene = torrent.Scene,
|
||||
IndexerFlags = flags,
|
||||
DownloadVolumeFactor = torrent.FreeleechType is "Freeleech" ? 0 : 1,
|
||||
UploadVolumeFactor = 1,
|
||||
MinimumRatio = 1,
|
||||
MinimumSeedTime = 345600,
|
||||
Genres = result.Tags ?? new List<string>(),
|
||||
PosterUrl = GetPosterUrl(result.Cover)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,5 +130,17 @@ namespace NzbDrone.Core.Indexers.PassThePopcorn
|
||||
|
||||
return url.FullUri;
|
||||
}
|
||||
|
||||
private static string GetPosterUrl(string cover)
|
||||
{
|
||||
if (cover.IsNotNullOrWhiteSpace() &&
|
||||
Uri.TryCreate(cover, UriKind.Absolute, out var posterUri) &&
|
||||
(posterUri.Scheme == Uri.UriSchemeHttp || posterUri.Scheme == Uri.UriSchemeHttps))
|
||||
{
|
||||
return posterUri.AbsoluteUri;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using NLog;
|
||||
using System.Linq;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.PassThePopcorn
|
||||
namespace NzbDrone.Core.Indexers.Definitions.PassThePopcorn
|
||||
{
|
||||
public class PassThePopcornRequestGenerator : IIndexerRequestGenerator
|
||||
{
|
||||
public PassThePopcornSettings Settings { get; set; }
|
||||
private readonly PassThePopcornSettings _settings;
|
||||
private readonly IndexerCapabilities _capabilities;
|
||||
|
||||
public IDictionary<string, string> Cookies { get; set; }
|
||||
|
||||
public IIndexerHttpClient HttpClient { get; set; }
|
||||
public Logger Logger { get; set; }
|
||||
public PassThePopcornRequestGenerator(PassThePopcornSettings settings, IndexerCapabilities capabilities)
|
||||
{
|
||||
_settings = settings;
|
||||
_capabilities = capabilities;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
{
|
||||
@@ -28,57 +30,12 @@ namespace NzbDrone.Core.Indexers.PassThePopcorn
|
||||
}
|
||||
else
|
||||
{
|
||||
pageableRequests.Add(GetRequest($"{searchCriteria.SearchTerm}", searchCriteria));
|
||||
pageableRequests.Add(GetRequest($"{searchCriteria.SanitizedSearchTerm}", searchCriteria));
|
||||
}
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public Func<IDictionary<string, string>> GetCookies { get; set; }
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
|
||||
private IEnumerable<IndexerRequest> GetRequest(string searchParameters, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
var queryParams = new NameValueCollection
|
||||
{
|
||||
{ "action", "advanced" },
|
||||
{ "json", "noredirect" },
|
||||
{ "grouping", "0" },
|
||||
{ "searchstr", searchParameters }
|
||||
};
|
||||
|
||||
if (searchCriteria.Limit is > 0 && searchCriteria.Offset is > 0)
|
||||
{
|
||||
var page = (int)(searchCriteria.Offset / searchCriteria.Limit) + 1;
|
||||
queryParams.Set("page", page.ToString());
|
||||
}
|
||||
|
||||
if (Settings.FreeleechOnly)
|
||||
{
|
||||
queryParams.Set("freetorrent", "1");
|
||||
}
|
||||
|
||||
var request =
|
||||
new IndexerRequest(
|
||||
$"{Settings.BaseUrl.Trim().TrimEnd('/')}/torrents.php?{queryParams.GetQueryString()}",
|
||||
HttpAccept.Json);
|
||||
|
||||
request.HttpRequest.Headers["ApiUser"] = Settings.APIUser;
|
||||
request.HttpRequest.Headers["ApiKey"] = Settings.APIKey;
|
||||
|
||||
if (Settings.APIKey.IsNullOrWhiteSpace())
|
||||
{
|
||||
foreach (var cookie in Cookies)
|
||||
{
|
||||
request.HttpRequest.Cookies[cookie.Key] = cookie.Value;
|
||||
}
|
||||
|
||||
CookiesUpdater(Cookies, DateTime.Now.AddDays(30));
|
||||
}
|
||||
|
||||
yield return request;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
{
|
||||
return new IndexerPageableRequestChain();
|
||||
@@ -86,7 +43,18 @@ namespace NzbDrone.Core.Indexers.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)
|
||||
@@ -98,9 +66,53 @@ namespace NzbDrone.Core.Indexers.PassThePopcorn
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetRequest($"{searchCriteria.SearchTerm}", searchCriteria));
|
||||
pageableRequests.Add(GetRequest($"{searchCriteria.SanitizedSearchTerm}", searchCriteria));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
private IEnumerable<IndexerRequest> GetRequest(string searchTerm, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
var parameters = new NameValueCollection
|
||||
{
|
||||
{ "action", "advanced" },
|
||||
{ "json", "noredirect" },
|
||||
{ "grouping", "0" },
|
||||
{ "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());
|
||||
}
|
||||
|
||||
var searchUrl = $"{_settings.BaseUrl.Trim().TrimEnd('/')}/torrents.php?{parameters.GetQueryString()}";
|
||||
|
||||
var request = new IndexerRequest(searchUrl, HttpAccept.Json);
|
||||
request.HttpRequest.Headers.Add("ApiUser", _settings.APIUser);
|
||||
request.HttpRequest.Headers.Add("ApiKey", _settings.APIKey);
|
||||
|
||||
yield return request;
|
||||
}
|
||||
|
||||
public Func<IDictionary<string, string>> GetCookies { get; set; }
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Indexers.Settings;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.PassThePopcorn
|
||||
namespace NzbDrone.Core.Indexers.Definitions.PassThePopcorn
|
||||
{
|
||||
public class PassThePopcornSettingsValidator : NoAuthSettingsValidator<PassThePopcornSettings>
|
||||
{
|
||||
|
||||
@@ -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";
|
||||
@@ -234,7 +234,7 @@ public class PirateTheNetParser : IParseIndexerResponse
|
||||
var releaseInfos = new List<ReleaseInfo>();
|
||||
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
using var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
|
||||
var rows = dom.QuerySelectorAll("table.main > tbody > tr");
|
||||
foreach (var row in rows.Skip(1))
|
||||
|
||||
@@ -171,7 +171,7 @@ public class PixelHDParser : IParseIndexerResponse
|
||||
var releaseInfos = new List<ReleaseInfo>();
|
||||
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
using var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
|
||||
var groups = dom.QuerySelectorAll("div.browsePoster");
|
||||
foreach (var group in groups)
|
||||
|
||||
@@ -85,7 +85,7 @@ public class PreToMe : TorrentIndexerBase<PreToMeSettings>
|
||||
if (CheckIfLoginNeeded(response))
|
||||
{
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(response.Content);
|
||||
using var dom = parser.ParseDocument(response.Content);
|
||||
var errorMessage = dom.QuerySelector("table.body_table font[color~=\"red\"]")?.TextContent.Trim();
|
||||
|
||||
throw new IndexerAuthException(errorMessage ?? "Unknown error message, please report.");
|
||||
@@ -340,7 +340,7 @@ public class PreToMeParser : IParseIndexerResponse
|
||||
var releaseInfos = new List<ReleaseInfo>();
|
||||
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
using var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
|
||||
var rows = dom.QuerySelectorAll("table > tbody > tr.browse");
|
||||
foreach (var row in rows)
|
||||
|
||||
@@ -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,6 +187,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"));
|
||||
|
||||
@@ -249,7 +249,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
var torrentInfos = new List<TorrentInfo>();
|
||||
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
using var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
var rows = dom.QuerySelectorAll("#torrents-table > tbody > tr");
|
||||
|
||||
foreach (var row in rows.Skip(1))
|
||||
|
||||
@@ -63,7 +63,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
var response = await _httpClient.ExecuteProxiedAsync(request, Definition);
|
||||
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(response.Content);
|
||||
using var dom = parser.ParseDocument(response.Content);
|
||||
var magnetLink = dom.QuerySelector("table.attach a.magnet-link[href^=\"magnet:?\"]")?.GetAttribute("href");
|
||||
|
||||
if (magnetLink == null)
|
||||
@@ -104,7 +104,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
if (!response.Content.Contains("id=\"logged-in-username\""))
|
||||
{
|
||||
var parser = new HtmlParser();
|
||||
var document = await parser.ParseDocumentAsync(response.Content);
|
||||
using var document = await parser.ParseDocumentAsync(response.Content);
|
||||
var errorMessage = document.QuerySelector("h4.warnColor1.tCenter.mrg_16, div.msg-main")?.TextContent.Trim();
|
||||
|
||||
throw new IndexerAuthException(errorMessage ?? "RuTracker Auth Failed");
|
||||
@@ -1580,7 +1580,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
var releaseInfos = new List<ReleaseInfo>();
|
||||
|
||||
var parser = new HtmlParser();
|
||||
var doc = parser.ParseDocument(indexerResponse.Content);
|
||||
using var doc = parser.ParseDocument(indexerResponse.Content);
|
||||
var rows = doc.QuerySelectorAll("table#tor-tbl > tbody > tr");
|
||||
|
||||
foreach (var row in rows)
|
||||
|
||||
@@ -225,7 +225,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
var releaseInfos = new List<ReleaseInfo>();
|
||||
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
using var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
|
||||
var table = dom.QuerySelector("table.movehere");
|
||||
if (table == null)
|
||||
|
||||
@@ -78,7 +78,7 @@ public class Shazbat : TorrentIndexerBase<ShazbatSettings>
|
||||
if (CheckIfLoginNeeded(response))
|
||||
{
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(response.Content);
|
||||
using var dom = await parser.ParseDocumentAsync(response.Content);
|
||||
var errorMessage = dom.QuerySelector("div#fail .modal-body")?.TextContent.Trim();
|
||||
|
||||
throw new IndexerAuthException(errorMessage ?? "Unknown error message, please report.");
|
||||
@@ -223,7 +223,7 @@ public class ShazbatParser : IParseIndexerResponse
|
||||
var releaseInfos = new List<ReleaseInfo>();
|
||||
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
using var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
|
||||
var hasGlobalFreeleech = dom.QuerySelector("span:contains(\"Freeleech until:\"):has(span.datetime)") != null;
|
||||
|
||||
@@ -303,7 +303,7 @@ public class ShazbatParser : IParseIndexerResponse
|
||||
var releaseInfos = new List<ReleaseInfo>();
|
||||
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
using var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
|
||||
if (!hasGlobalFreeleech)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Newtonsoft.Json;
|
||||
@@ -94,6 +95,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
publishedAt
|
||||
slug
|
||||
torrents {
|
||||
synopsis
|
||||
downloaded
|
||||
seeders
|
||||
leechers
|
||||
@@ -113,7 +115,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
var queryCollection = new NameValueCollection
|
||||
{
|
||||
{ "query", query.Replace('\n', ' ').Trim() },
|
||||
{ "variables", Newtonsoft.Json.JsonConvert.SerializeObject(variables) }
|
||||
{ "variables", JsonConvert.SerializeObject(variables) }
|
||||
};
|
||||
|
||||
var requestUrl = string.Format("{0}/graphql?", Settings.BaseUrl.TrimEnd('/')) + queryCollection.GetQueryString();
|
||||
@@ -176,25 +178,26 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
_categories = categories;
|
||||
}
|
||||
|
||||
private string composeTitle(ShizaprojectNode n, ShizaprojectTorrent tr)
|
||||
private string ComposeTitle(ShizaprojectNode n, ShizaprojectTorrent tr)
|
||||
{
|
||||
var title = string.Format("{0} / {1}", n.Name, n.OriginalName);
|
||||
foreach (var tl in n.AlternativeNames)
|
||||
var allNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
title += " / " + tl;
|
||||
n.Name,
|
||||
n.OriginalName
|
||||
};
|
||||
allNames.UnionWith(n.AlternativeNames.ToHashSet());
|
||||
|
||||
var title = $"{string.Join(" / ", allNames)} {tr.Synopsis}";
|
||||
|
||||
if (tr.VideoQualities.Length > 0)
|
||||
{
|
||||
title += $" [{string.Join(" ", tr.VideoQualities)}]";
|
||||
}
|
||||
|
||||
title += " [";
|
||||
foreach (var q in tr.VideoQualities)
|
||||
{
|
||||
title += " " + q;
|
||||
}
|
||||
|
||||
title += " ]";
|
||||
return title;
|
||||
}
|
||||
|
||||
private DateTime getActualPublishDate(ShizaprojectNode n, ShizaprojectTorrent t)
|
||||
private DateTime GetActualPublishDate(ShizaprojectNode n, ShizaprojectTorrent t)
|
||||
{
|
||||
if (n.PublishedAt == null)
|
||||
{
|
||||
@@ -206,7 +209,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
}
|
||||
}
|
||||
|
||||
private string getResolution(string[] qualities)
|
||||
private string GetResolution(string[] qualities)
|
||||
{
|
||||
var resPrefix = "RESOLUTION_";
|
||||
var res = Array.Find(qualities, s => s.StartsWith(resPrefix));
|
||||
@@ -235,7 +238,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
var torrentInfo = new TorrentInfo
|
||||
{
|
||||
Title = composeTitle(e.Node, tr),
|
||||
Title = ComposeTitle(e.Node, tr),
|
||||
InfoUrl = string.Format("{0}/releases/{1}/", _settings.BaseUrl.TrimEnd('/'), e.Node.Slug),
|
||||
DownloadVolumeFactor = 0,
|
||||
UploadVolumeFactor = 1,
|
||||
@@ -243,12 +246,12 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
Peers = tr.Leechers + tr.Seeders,
|
||||
Grabs = tr.Downloaded,
|
||||
Categories = _categories.MapTrackerCatDescToNewznab(e.Node.Type),
|
||||
PublishDate = getActualPublishDate(e.Node, tr),
|
||||
PublishDate = GetActualPublishDate(e.Node, tr),
|
||||
Guid = tr.File.Url,
|
||||
DownloadUrl = tr.File.Url,
|
||||
MagnetUrl = tr.MagnetUri,
|
||||
Size = tr.Size,
|
||||
Resolution = getResolution(tr.VideoQualities)
|
||||
Resolution = GetResolution(tr.VideoQualities)
|
||||
};
|
||||
|
||||
torrentInfos.Add(torrentInfo);
|
||||
@@ -311,6 +314,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
public class ShizaprojectTorrent
|
||||
{
|
||||
public string Synopsis { get; set; }
|
||||
public int Downloaded { get; set; }
|
||||
public int Seeders { get; set; }
|
||||
public int Leechers { get; set; }
|
||||
|
||||
@@ -96,7 +96,7 @@ public class SpeedCD : TorrentIndexerBase<SpeedCDSettings>
|
||||
if (CheckIfLoginNeeded(response))
|
||||
{
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(response.Content);
|
||||
using var dom = parser.ParseDocument(response.Content);
|
||||
var errorMessage = dom.QuerySelector("h5")?.TextContent.Trim();
|
||||
|
||||
if (response.Content.Contains("Wrong Captcha!"))
|
||||
@@ -323,7 +323,7 @@ public class SpeedCDParser : IParseIndexerResponse
|
||||
var releaseInfos = new List<ReleaseInfo>();
|
||||
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
using var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
|
||||
var rows = dom.QuerySelectorAll("div.boxContent > table > tbody > tr");
|
||||
foreach (var row in rows)
|
||||
|
||||
@@ -75,7 +75,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
if (CheckIfLoginNeeded(response))
|
||||
{
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(response.Content);
|
||||
using var dom = parser.ParseDocument(response.Content);
|
||||
var errorMessage = dom.QuerySelector("form#loginform")?.TextContent.Trim();
|
||||
|
||||
throw new IndexerAuthException(errorMessage ?? "Unknown error message, please report.");
|
||||
@@ -224,7 +224,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
var torrentInfos = new List<ReleaseInfo>();
|
||||
|
||||
var parser = new HtmlParser();
|
||||
var doc = parser.ParseDocument(indexerResponse.Content);
|
||||
using var doc = parser.ParseDocument(indexerResponse.Content);
|
||||
|
||||
// get params to build download link (user could be banned without those params)
|
||||
var rssFeedUri = new Uri(_settings.BaseUrl + doc.QuerySelector("link[href^=\"/feeds.php?feed=\"]")
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user