mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2026-03-11 15:39:55 -04:00
Compare commits
111 Commits
v1.1.2.245
...
paging
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ca6f83a4d | ||
|
|
fb5b325271 | ||
|
|
ec8025c3dc | ||
|
|
b42bf2cf20 | ||
|
|
712d95e6ce | ||
|
|
24f6c937da | ||
|
|
e94aa7c499 | ||
|
|
201bc1944b | ||
|
|
09e40e0060 | ||
|
|
348d90a37e | ||
|
|
726dc34424 | ||
|
|
2e9f6cd94b | ||
|
|
495f61f412 | ||
|
|
0f11f414b6 | ||
|
|
d397cdf5fb | ||
|
|
888b514dd8 | ||
|
|
caab337379 | ||
|
|
26bea14137 | ||
|
|
5f26287234 | ||
|
|
6ec761c217 | ||
|
|
b85679de56 | ||
|
|
71775b97a3 | ||
|
|
5bb3dbfbf5 | ||
|
|
b608a7a904 | ||
|
|
4ad992f76a | ||
|
|
95497480a2 | ||
|
|
cc57866ab0 | ||
|
|
dbc4989a95 | ||
|
|
af4961e3e6 | ||
|
|
0ec54906c6 | ||
|
|
35f85fc986 | ||
|
|
0aedafb278 | ||
|
|
54dce448a8 | ||
|
|
3c915002c6 | ||
|
|
e32f8f4330 | ||
|
|
5abb5ada49 | ||
|
|
6579385110 | ||
|
|
1c6e5543df | ||
|
|
85737aacbe | ||
|
|
30c3aedeb1 | ||
|
|
1640980e2b | ||
|
|
99bc56efb6 | ||
|
|
04276eb587 | ||
|
|
34c560fd3a | ||
|
|
caa8bb05a7 | ||
|
|
773e8ff1f4 | ||
|
|
0984976378 | ||
|
|
fcb3c96455 | ||
|
|
acf7a425b5 | ||
|
|
da898fe958 | ||
|
|
5bb3ea0806 | ||
|
|
b41cb80e33 | ||
|
|
a39341be4b | ||
|
|
27b3d8618a | ||
|
|
550b9b58df | ||
|
|
035ad33b72 | ||
|
|
85f8e0c451 | ||
|
|
ea2061a7d3 | ||
|
|
ea6d01a49b | ||
|
|
252cd97e35 | ||
|
|
a8ea05af07 | ||
|
|
24d6a0cb06 | ||
|
|
8e1771b5a9 | ||
|
|
d767a82e84 | ||
|
|
76bfd29f23 | ||
|
|
c923982711 | ||
|
|
f03a64f9ac | ||
|
|
e713e58e83 | ||
|
|
4fb5d3432b | ||
|
|
a31b107a90 | ||
|
|
f91ffb8328 | ||
|
|
a3ba070296 | ||
|
|
bccb0bd5c8 | ||
|
|
4517f271c4 | ||
|
|
2ae2a0b184 | ||
|
|
b5e43e7a1a | ||
|
|
3a52048dc2 | ||
|
|
8b898733ab | ||
|
|
f99a2e1164 | ||
|
|
306209fcc2 | ||
|
|
5d09c2b5fa | ||
|
|
41a9d2d732 | ||
|
|
49b120ba55 | ||
|
|
a88fc34a78 | ||
|
|
c46b7c5e4b | ||
|
|
94c45541ae | ||
|
|
f8082047a5 | ||
|
|
011fd57f7d | ||
|
|
6c35c3fc6f | ||
|
|
5da02c49eb | ||
|
|
1a339b9ab2 | ||
|
|
94edd7538e | ||
|
|
9b2274805e | ||
|
|
dbf86efb0a | ||
|
|
529fbfd9bd | ||
|
|
0ed5bfe0d0 | ||
|
|
6a43eb0031 | ||
|
|
a12001a5ef | ||
|
|
b57014762d | ||
|
|
a51a8bf921 | ||
|
|
e8dc5b3206 | ||
|
|
d4f22f3596 | ||
|
|
b6018a4cd7 | ||
|
|
ec389987df | ||
|
|
6b62504916 | ||
|
|
626d777d3c | ||
|
|
234707b291 | ||
|
|
15734ca0da | ||
|
|
19913e5b01 | ||
|
|
156f6505be | ||
|
|
e383287972 |
@@ -117,7 +117,6 @@ dotnet_diagnostic.CA1003.severity = suggestion
|
||||
dotnet_diagnostic.CA1008.severity = suggestion
|
||||
dotnet_diagnostic.CA1010.severity = suggestion
|
||||
dotnet_diagnostic.CA1012.severity = suggestion
|
||||
dotnet_diagnostic.CA1014.severity = suggestion
|
||||
dotnet_diagnostic.CA1016.severity = suggestion
|
||||
dotnet_diagnostic.CA1017.severity = suggestion
|
||||
dotnet_diagnostic.CA1018.severity = suggestion
|
||||
@@ -163,6 +162,7 @@ dotnet_diagnostic.CA1309.severity = suggestion
|
||||
dotnet_diagnostic.CA1310.severity = suggestion
|
||||
dotnet_diagnostic.CA1401.severity = suggestion
|
||||
dotnet_diagnostic.CA1416.severity = suggestion
|
||||
dotnet_diagnostic.CA1419.severity = suggestion
|
||||
dotnet_diagnostic.CA1507.severity = suggestion
|
||||
dotnet_diagnostic.CA1508.severity = suggestion
|
||||
dotnet_diagnostic.CA1707.severity = suggestion
|
||||
@@ -178,9 +178,6 @@ dotnet_diagnostic.CA1720.severity = suggestion
|
||||
dotnet_diagnostic.CA1721.severity = suggestion
|
||||
dotnet_diagnostic.CA1724.severity = suggestion
|
||||
dotnet_diagnostic.CA1725.severity = suggestion
|
||||
dotnet_diagnostic.CA1801.severity = suggestion
|
||||
dotnet_diagnostic.CA1802.severity = suggestion
|
||||
dotnet_diagnostic.CA1805.severity = suggestion
|
||||
dotnet_diagnostic.CA1806.severity = suggestion
|
||||
dotnet_diagnostic.CA1810.severity = suggestion
|
||||
dotnet_diagnostic.CA1812.severity = suggestion
|
||||
@@ -192,13 +189,14 @@ dotnet_diagnostic.CA1819.severity = suggestion
|
||||
dotnet_diagnostic.CA1822.severity = suggestion
|
||||
dotnet_diagnostic.CA1823.severity = suggestion
|
||||
dotnet_diagnostic.CA1824.severity = suggestion
|
||||
dotnet_diagnostic.CA1835.severity = suggestion
|
||||
dotnet_diagnostic.CA1845.severity = suggestion
|
||||
dotnet_diagnostic.CA1848.severity = suggestion
|
||||
dotnet_diagnostic.CA1849.severity = suggestion
|
||||
dotnet_diagnostic.CA2000.severity = suggestion
|
||||
dotnet_diagnostic.CA2002.severity = suggestion
|
||||
dotnet_diagnostic.CA2007.severity = suggestion
|
||||
dotnet_diagnostic.CA2008.severity = suggestion
|
||||
dotnet_diagnostic.CA2009.severity = suggestion
|
||||
dotnet_diagnostic.CA2010.severity = suggestion
|
||||
dotnet_diagnostic.CA2011.severity = suggestion
|
||||
dotnet_diagnostic.CA2012.severity = suggestion
|
||||
dotnet_diagnostic.CA2013.severity = suggestion
|
||||
dotnet_diagnostic.CA2100.severity = suggestion
|
||||
@@ -229,6 +227,7 @@ dotnet_diagnostic.CA2243.severity = suggestion
|
||||
dotnet_diagnostic.CA2244.severity = suggestion
|
||||
dotnet_diagnostic.CA2245.severity = suggestion
|
||||
dotnet_diagnostic.CA2246.severity = suggestion
|
||||
dotnet_diagnostic.CA2254.severity = suggestion
|
||||
dotnet_diagnostic.CA3061.severity = suggestion
|
||||
dotnet_diagnostic.CA3075.severity = suggestion
|
||||
dotnet_diagnostic.CA3076.severity = suggestion
|
||||
@@ -255,6 +254,7 @@ dotnet_diagnostic.CA5385.severity = suggestion
|
||||
dotnet_diagnostic.CA5392.severity = suggestion
|
||||
dotnet_diagnostic.CA5394.severity = suggestion
|
||||
dotnet_diagnostic.CA5397.severity = suggestion
|
||||
dotnet_diagnostic.CA5401.severity = suggestion
|
||||
|
||||
dotnet_diagnostic.SYSLIB0014.severity = none
|
||||
|
||||
|
||||
41
.github/workflows/azuresync.yml
vendored
41
.github/workflows/azuresync.yml
vendored
@@ -1,41 +0,0 @@
|
||||
name: Sync issue to Azure DevOps work item
|
||||
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
[opened, edited, deleted, closed, reopened, labeled, unlabeled, assigned]
|
||||
|
||||
concurrency: azuresync-${{ github.event.issue.number }}
|
||||
|
||||
jobs:
|
||||
alert:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: danhellem/github-actions-issue-to-work-item@master
|
||||
if: "${{ contains(github.event.issue.labels.*.name, 'Type: Bug') == true }}"
|
||||
env:
|
||||
ado_token: "${{ secrets.ADO_PERSONAL_ACCESS_TOKEN }}"
|
||||
github_token: "${{ github.token }}"
|
||||
ado_organization: "Servarr"
|
||||
ado_project: "Servarr"
|
||||
ado_area_path: "Servarr\\Prowlarr"
|
||||
ado_wit: "Bug"
|
||||
ado_new_state: "New"
|
||||
ado_active_state: "Active"
|
||||
ado_close_state: "Closed"
|
||||
ado_bypassrules: true
|
||||
log_level: 100
|
||||
- uses: danhellem/github-actions-issue-to-work-item@master
|
||||
if: "${{ contains(github.event.issue.labels.*.name, 'Type: Bug') == false }}"
|
||||
env:
|
||||
ado_token: "${{ secrets.ADO_PERSONAL_ACCESS_TOKEN }}"
|
||||
github_token: "${{ github.token }}"
|
||||
ado_organization: "Servarr"
|
||||
ado_project: "Servarr"
|
||||
ado_area_path: "Servarr\\Prowlarr"
|
||||
ado_wit: "User Story"
|
||||
ado_new_state: "New"
|
||||
ado_active_state: "Active"
|
||||
ado_close_state: "Closed"
|
||||
ado_bypassrules: true
|
||||
log_level: 100
|
||||
@@ -9,7 +9,7 @@ variables:
|
||||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '1.1.2'
|
||||
majorVersion: '1.3.1'
|
||||
minorVersion: $[counter('minorVersion', 1)]
|
||||
prowlarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
|
||||
|
||||
@@ -39,6 +39,7 @@ module.exports = {
|
||||
plugins: [
|
||||
'filenames',
|
||||
'react',
|
||||
'react-hooks',
|
||||
'simple-import-sort',
|
||||
'import'
|
||||
],
|
||||
@@ -308,7 +309,9 @@ module.exports = {
|
||||
'react/react-in-jsx-scope': 2,
|
||||
'react/self-closing-comp': 2,
|
||||
'react/sort-comp': 2,
|
||||
'react/jsx-wrap-multilines': 2
|
||||
'react/jsx-wrap-multilines': 2,
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
'react-hooks/exhaustive-deps': 'error'
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Fragment, useEffect } from 'react';
|
||||
import React, { Fragment, useCallback, useEffect } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import themes from 'Styles/Themes';
|
||||
@@ -19,7 +19,8 @@ function createMapStateToProps() {
|
||||
|
||||
function ApplyTheme({ theme, children }) {
|
||||
// Update the CSS Variables
|
||||
function updateCSSVariables() {
|
||||
|
||||
const updateCSSVariables = useCallback(() => {
|
||||
const arrayOfVariableKeys = Object.keys(themes[theme]);
|
||||
const arrayOfVariableValues = Object.values(themes[theme]);
|
||||
|
||||
@@ -31,12 +32,12 @@ function ApplyTheme({ theme, children }) {
|
||||
arrayOfVariableValues[index]
|
||||
);
|
||||
});
|
||||
}
|
||||
}, [theme]);
|
||||
|
||||
// On Component Mount and Component Update
|
||||
useEffect(() => {
|
||||
updateCSSVariables(theme);
|
||||
}, [theme]);
|
||||
}, [updateCSSVariables, theme]);
|
||||
|
||||
return <Fragment>{children}</Fragment>;
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ function CustomFiltersModalContent(props) {
|
||||
|
||||
<div className={styles.addButtonContainer}>
|
||||
<Button onPress={onAddCustomFilter}>
|
||||
Add Custom Filter
|
||||
{translate('AddCustomFilter')}
|
||||
</Button>
|
||||
</div>
|
||||
</ModalBody>
|
||||
|
||||
@@ -30,10 +30,10 @@ function ConfirmModal(props) {
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
bindShortcut('enter', onConfirm);
|
||||
} else {
|
||||
unbindShortcut('enter', onConfirm);
|
||||
|
||||
return () => unbindShortcut('enter', onConfirm);
|
||||
}
|
||||
}, [onConfirm]);
|
||||
}, [bindShortcut, unbindShortcut, isOpen, onConfirm]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
|
||||
@@ -61,15 +61,15 @@ class TagsModalContent extends Component {
|
||||
} = this.state;
|
||||
|
||||
const applyTagsOptions = [
|
||||
{ key: 'add', value: 'Add' },
|
||||
{ key: 'remove', value: 'Remove' },
|
||||
{ key: 'replace', value: 'Replace' }
|
||||
{ key: 'add', value: translate('Add') },
|
||||
{ key: 'remove', value: translate('Remove') },
|
||||
{ key: 'replace', value: translate('Replace') }
|
||||
];
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Tags
|
||||
{translate('Tags')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
|
||||
@@ -26,7 +26,7 @@ function IndexerIndexSortMenu(props) {
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
Status
|
||||
{translate('Status')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
@@ -62,7 +62,7 @@ function IndexerIndexSortMenu(props) {
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
{'Priority'}
|
||||
{translate('Priority')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
@@ -71,7 +71,7 @@ function IndexerIndexSortMenu(props) {
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
{'Protocol'}
|
||||
{translate('Protocol')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
@@ -80,7 +80,7 @@ function IndexerIndexSortMenu(props) {
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
{'Privacy'}
|
||||
{translate('Privacy')}
|
||||
</SortMenuItem>
|
||||
</MenuContent>
|
||||
</SortMenu>
|
||||
|
||||
@@ -97,7 +97,7 @@ class IndexerIndexRow extends Component {
|
||||
isIndexerInfoModalOpen
|
||||
} = this.state;
|
||||
|
||||
const baseUrl = fields.find((field) => field.name === 'baseUrl')?.value ?? indexerUrls[0];
|
||||
const baseUrl = fields.find((field) => field.name === 'baseUrl')?.value ?? (Array.isArray(indexerUrls) ? indexerUrls[0] : undefined);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -248,12 +248,12 @@ class IndexerIndexRow extends Component {
|
||||
/>
|
||||
|
||||
{
|
||||
indexerUrls ?
|
||||
baseUrl ?
|
||||
<IconButton
|
||||
className={styles.externalLink}
|
||||
name={icons.EXTERNAL_LINK}
|
||||
title={translate('Website')}
|
||||
to={baseUrl.replace('api.', '')}
|
||||
to={baseUrl.replace(/(:\/\/)api\./, '$1')}
|
||||
/> : null
|
||||
}
|
||||
|
||||
|
||||
@@ -71,6 +71,19 @@ class SearchIndexRow extends Component {
|
||||
});
|
||||
};
|
||||
|
||||
onSavePress = () => {
|
||||
const {
|
||||
downloadUrl,
|
||||
fileName,
|
||||
onSavePress
|
||||
} = this.props;
|
||||
|
||||
onSavePress({
|
||||
downloadUrl,
|
||||
fileName
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -85,7 +98,6 @@ class SearchIndexRow extends Component {
|
||||
publishDate,
|
||||
title,
|
||||
infoUrl,
|
||||
downloadUrl,
|
||||
indexer,
|
||||
size,
|
||||
files,
|
||||
@@ -300,7 +312,7 @@ class SearchIndexRow extends Component {
|
||||
className={styles.downloadLink}
|
||||
name={icons.SAVE}
|
||||
title={translate('Save')}
|
||||
to={downloadUrl}
|
||||
onPress={this.onSavePress}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
@@ -323,6 +335,7 @@ SearchIndexRow.propTypes = {
|
||||
ageMinutes: PropTypes.number.isRequired,
|
||||
publishDate: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
fileName: PropTypes.string.isRequired,
|
||||
infoUrl: PropTypes.string.isRequired,
|
||||
downloadUrl: PropTypes.string.isRequired,
|
||||
indexerId: PropTypes.number.isRequired,
|
||||
@@ -335,6 +348,7 @@ SearchIndexRow.propTypes = {
|
||||
indexerFlags: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onGrabPress: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
isGrabbing: PropTypes.bool.isRequired,
|
||||
isGrabbed: PropTypes.bool.isRequired,
|
||||
grabError: PropTypes.string,
|
||||
|
||||
@@ -51,7 +51,8 @@ class SearchIndexTable extends Component {
|
||||
timeFormat,
|
||||
selectedState,
|
||||
onSelectedChange,
|
||||
onGrabPress
|
||||
onGrabPress,
|
||||
onSavePress
|
||||
} = this.props;
|
||||
|
||||
const release = items[rowIndex];
|
||||
@@ -71,6 +72,7 @@ class SearchIndexTable extends Component {
|
||||
longDateFormat={longDateFormat}
|
||||
timeFormat={timeFormat}
|
||||
onGrabPress={onGrabPress}
|
||||
onSavePress={onSavePress}
|
||||
/>
|
||||
</VirtualTableRow>
|
||||
);
|
||||
@@ -134,6 +136,7 @@ SearchIndexTable.propTypes = {
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
onSortPress: PropTypes.func.isRequired,
|
||||
onGrabPress: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
allSelected: PropTypes.bool.isRequired,
|
||||
allUnselected: PropTypes.bool.isRequired,
|
||||
selectedState: PropTypes.object.isRequired,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { grabRelease, setReleasesSort } from 'Store/Actions/releaseActions';
|
||||
import { grabRelease, saveRelease, setReleasesSort } from 'Store/Actions/releaseActions';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import SearchIndexTable from './SearchIndexTable';
|
||||
|
||||
@@ -25,6 +25,9 @@ function createMapDispatchToProps(dispatch, props) {
|
||||
},
|
||||
onGrabPress(payload) {
|
||||
dispatch(grabRelease(payload));
|
||||
},
|
||||
onSavePress(payload) {
|
||||
dispatch(saveRelease(payload));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ class AddApplicationModalContent extends Component {
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Add Application
|
||||
{translate('AddApplication')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
|
||||
@@ -71,14 +71,14 @@ class Application extends Component {
|
||||
{
|
||||
syncLevel === 'addOnly' &&
|
||||
<Label kind={kinds.WARNING}>
|
||||
Add and Remove Only
|
||||
{translate('AddRemoveOnly')}
|
||||
</Label>
|
||||
}
|
||||
|
||||
{
|
||||
syncLevel === 'fullSync' &&
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
Full Sync
|
||||
{translate('FullSync')}
|
||||
</Label>
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ class Application extends Component {
|
||||
kind={kinds.DISABLED}
|
||||
outline={true}
|
||||
>
|
||||
Disabled
|
||||
{translate('Disabled')}
|
||||
</Label>
|
||||
}
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ class IndexerProxy extends Component {
|
||||
kind={kinds.DISABLED}
|
||||
outline={true}
|
||||
>
|
||||
Disabled
|
||||
{translate('Disabled')}
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
|
||||
@@ -56,17 +56,9 @@ class Notification extends Component {
|
||||
id,
|
||||
name,
|
||||
onGrab,
|
||||
onDownload,
|
||||
onUpgrade,
|
||||
onRename,
|
||||
onDelete,
|
||||
onHealthIssue,
|
||||
onApplicationUpdate,
|
||||
supportsOnGrab,
|
||||
supportsOnDownload,
|
||||
supportsOnUpgrade,
|
||||
supportsOnRename,
|
||||
supportsOnDelete,
|
||||
supportsOnHealthIssue,
|
||||
supportsOnApplicationUpdate
|
||||
} = this.props;
|
||||
@@ -88,34 +80,6 @@ class Notification extends Component {
|
||||
</Label>
|
||||
}
|
||||
|
||||
{
|
||||
supportsOnDelete && onDelete &&
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
{translate('OnDelete')}
|
||||
</Label>
|
||||
}
|
||||
|
||||
{
|
||||
supportsOnDownload && onDownload &&
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
{translate('OnImport')}
|
||||
</Label>
|
||||
}
|
||||
|
||||
{
|
||||
supportsOnUpgrade && onDownload && onUpgrade &&
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
{translate('OnUpgrade')}
|
||||
</Label>
|
||||
}
|
||||
|
||||
{
|
||||
supportsOnRename && onRename &&
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
{translate('OnRename')}
|
||||
</Label>
|
||||
}
|
||||
|
||||
{
|
||||
supportsOnHealthIssue && onHealthIssue &&
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
@@ -132,7 +96,7 @@ class Notification extends Component {
|
||||
}
|
||||
|
||||
{
|
||||
!onGrab && !onDownload && !onRename && !onHealthIssue && !onDelete && !onApplicationUpdate ?
|
||||
!onGrab && !onHealthIssue && !onApplicationUpdate ?
|
||||
<Label
|
||||
kind={kinds.DISABLED}
|
||||
outline={true}
|
||||
@@ -167,17 +131,9 @@ Notification.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
onGrab: PropTypes.bool.isRequired,
|
||||
onDownload: PropTypes.bool.isRequired,
|
||||
onUpgrade: PropTypes.bool.isRequired,
|
||||
onRename: PropTypes.bool.isRequired,
|
||||
onDelete: PropTypes.bool.isRequired,
|
||||
onHealthIssue: PropTypes.bool.isRequired,
|
||||
onApplicationUpdate: PropTypes.bool.isRequired,
|
||||
supportsOnGrab: PropTypes.bool.isRequired,
|
||||
supportsOnDownload: PropTypes.bool.isRequired,
|
||||
supportsOnDelete: PropTypes.bool.isRequired,
|
||||
supportsOnUpgrade: PropTypes.bool.isRequired,
|
||||
supportsOnRename: PropTypes.bool.isRequired,
|
||||
supportsOnHealthIssue: PropTypes.bool.isRequired,
|
||||
supportsOnApplicationUpdate: PropTypes.bool.isRequired,
|
||||
onConfirmDeleteNotification: PropTypes.func.isRequired
|
||||
|
||||
@@ -15,8 +15,11 @@ function NotificationEventItems(props) {
|
||||
} = props;
|
||||
|
||||
const {
|
||||
onGrab,
|
||||
onHealthIssue,
|
||||
onApplicationUpdate,
|
||||
supportsOnGrab,
|
||||
includeManualGrabs,
|
||||
supportsOnHealthIssue,
|
||||
includeHealthWarnings,
|
||||
supportsOnApplicationUpdate
|
||||
@@ -31,6 +34,31 @@ function NotificationEventItems(props) {
|
||||
link="https://wiki.servarr.com/prowlarr/settings#connections"
|
||||
/>
|
||||
<div className={styles.events}>
|
||||
<div>
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="onGrab"
|
||||
helpText={translate('OnGrabHelpText')}
|
||||
isDisabled={!supportsOnGrab.value}
|
||||
{...onGrab}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{
|
||||
onGrab.value &&
|
||||
<div>
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="includeManualGrabs"
|
||||
helpText={translate('IncludeManualGrabsHelpText')}
|
||||
isDisabled={!supportsOnGrab.value}
|
||||
{...includeManualGrabs}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div>
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
|
||||
@@ -20,7 +20,7 @@ function PendingChangesModal(props) {
|
||||
|
||||
useEffect(() => {
|
||||
bindShortcut('enter', onConfirm);
|
||||
}, [onConfirm]);
|
||||
}, [bindShortcut, onConfirm]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
|
||||
@@ -103,9 +103,6 @@ export default {
|
||||
[SELECT_NOTIFICATION_SCHEMA]: (state, { payload }) => {
|
||||
return selectProviderSchema(state, section, payload, (selectedSchema) => {
|
||||
selectedSchema.onGrab = selectedSchema.supportsOnGrab;
|
||||
selectedSchema.onDownload = selectedSchema.supportsOnDownload;
|
||||
selectedSchema.onUpgrade = selectedSchema.supportsOnUpgrade;
|
||||
selectedSchema.onRename = selectedSchema.supportsOnRename;
|
||||
selectedSchema.onApplicationUpdate = selectedSchema.supportsOnApplicationUpdate;
|
||||
|
||||
return selectedSchema;
|
||||
|
||||
@@ -54,7 +54,7 @@ export const defaultState = {
|
||||
},
|
||||
{
|
||||
name: 'grabTitle',
|
||||
label: translate('Grab Title'),
|
||||
label: translate('GrabTitle'),
|
||||
isSortable: false,
|
||||
isVisible: false
|
||||
},
|
||||
@@ -78,7 +78,7 @@ export const defaultState = {
|
||||
},
|
||||
{
|
||||
name: 'elapsedTime',
|
||||
label: translate('Elapsed Time'),
|
||||
label: translate('ElapsedTime'),
|
||||
isSortable: false,
|
||||
isVisible: true
|
||||
},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import $ from 'jquery';
|
||||
import { createAction } from 'redux-actions';
|
||||
import { batchActions } from 'redux-batched-actions';
|
||||
import { filterBuilderTypes, filterBuilderValueTypes, sortDirections } from 'Helpers/Props';
|
||||
@@ -229,6 +230,7 @@ export const CANCEL_FETCH_RELEASES = 'releases/cancelFetchReleases';
|
||||
export const SET_RELEASES_SORT = 'releases/setReleasesSort';
|
||||
export const CLEAR_RELEASES = 'releases/clearReleases';
|
||||
export const GRAB_RELEASE = 'releases/grabRelease';
|
||||
export const SAVE_RELEASE = 'releases/saveRelease';
|
||||
export const BULK_GRAB_RELEASES = 'release/bulkGrabReleases';
|
||||
export const UPDATE_RELEASE = 'releases/updateRelease';
|
||||
export const SET_RELEASES_FILTER = 'releases/setReleasesFilter';
|
||||
@@ -243,6 +245,7 @@ export const cancelFetchReleases = createThunk(CANCEL_FETCH_RELEASES);
|
||||
export const setReleasesSort = createAction(SET_RELEASES_SORT);
|
||||
export const clearReleases = createAction(CLEAR_RELEASES);
|
||||
export const grabRelease = createThunk(GRAB_RELEASE);
|
||||
export const saveRelease = createThunk(SAVE_RELEASE);
|
||||
export const bulkGrabReleases = createThunk(BULK_GRAB_RELEASES);
|
||||
export const updateRelease = createAction(UPDATE_RELEASE);
|
||||
export const setReleasesFilter = createAction(SET_RELEASES_FILTER);
|
||||
@@ -304,6 +307,32 @@ export const actionHandlers = handleThunks({
|
||||
});
|
||||
},
|
||||
|
||||
[SAVE_RELEASE]: function(getState, payload, dispatch) {
|
||||
const link = payload.downloadUrl;
|
||||
const file = payload.fileName;
|
||||
|
||||
$.ajax({
|
||||
url: link,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Prowlarr-Client': true
|
||||
},
|
||||
xhrFields: {
|
||||
responseType: 'blob'
|
||||
},
|
||||
success: function(data) {
|
||||
const a = document.createElement('a');
|
||||
const url = window.URL.createObjectURL(data);
|
||||
a.href = url;
|
||||
a.download = file;
|
||||
document.body.append(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
[BULK_GRAB_RELEASES]: function(getState, payload, dispatch) {
|
||||
dispatch(set({
|
||||
section,
|
||||
|
||||
@@ -25,7 +25,7 @@ const columns = [
|
||||
},
|
||||
{
|
||||
name: 'size',
|
||||
label: 'Size',
|
||||
label: translate('Size'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
|
||||
@@ -113,7 +113,7 @@ class Updates extends Component {
|
||||
/>
|
||||
|
||||
<div className={styles.message}>
|
||||
The latest version of Prowlarr is already installed
|
||||
{translate('TheLatestVersionIsAlreadyInstalled', ['Prowlarr'])}
|
||||
</div>
|
||||
|
||||
{
|
||||
|
||||
@@ -16,6 +16,11 @@ function addApiKey(ajaxOptions) {
|
||||
ajaxOptions.headers['X-Api-Key'] = window.Prowlarr.apiKey;
|
||||
}
|
||||
|
||||
function addUIHeader(ajaxOptions) {
|
||||
ajaxOptions.headers = ajaxOptions.headers || {};
|
||||
ajaxOptions.headers['X-Prowlarr-Client'] = true;
|
||||
}
|
||||
|
||||
function addContentType(ajaxOptions) {
|
||||
if (
|
||||
ajaxOptions.contentType == null &&
|
||||
@@ -42,6 +47,7 @@ export default function createAjaxRequest(originalAjaxOptions) {
|
||||
if (isRelative(ajaxOptions)) {
|
||||
addRootUrl(ajaxOptions);
|
||||
addApiKey(ajaxOptions);
|
||||
addUIHeader(ajaxOptions);
|
||||
addContentType(ajaxOptions);
|
||||
}
|
||||
|
||||
|
||||
@@ -105,6 +105,7 @@
|
||||
"eslint-plugin-filenames": "1.3.2",
|
||||
"eslint-plugin-import": "2.26.0",
|
||||
"eslint-plugin-react": "7.31.11",
|
||||
"eslint-plugin-react-hooks": "4.6.0",
|
||||
"eslint-plugin-simple-import-sort": "8.0.0",
|
||||
"esprint": "3.6.0",
|
||||
"file-loader": "6.2.0",
|
||||
|
||||
3
src/.globalconfig
Normal file
3
src/.globalconfig
Normal file
@@ -0,0 +1,3 @@
|
||||
is_global = true
|
||||
|
||||
dotnet_diagnostic.CA1014.severity = none
|
||||
@@ -1,6 +1,7 @@
|
||||
<Project>
|
||||
<!-- Common to all Prowlarr Projects -->
|
||||
<PropertyGroup>
|
||||
<AnalysisLevel>6.0-all</AnalysisLevel>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles>
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
|
||||
@@ -278,7 +278,7 @@ namespace NzbDrone.Common.Test
|
||||
[Test]
|
||||
public void GetUpdateClientExePath()
|
||||
{
|
||||
GetIAppDirectoryInfo().GetUpdateClientExePath(PlatformType.DotNet).Should().BeEquivalentTo(@"C:\Temp\prowlarr_update\Prowlarr.Update.exe".AsOsAgnostic());
|
||||
GetIAppDirectoryInfo().GetUpdateClientExePath().Should().BeEquivalentTo(@"C:\Temp\prowlarr_update\Prowlarr.Update".AsOsAgnostic().ProcessNameToExe());
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace NzbDrone.Common.EnvironmentInfo
|
||||
bool IsAdmin { get; }
|
||||
bool IsWindowsService { get; }
|
||||
bool IsWindowsTray { get; }
|
||||
bool IsStarting { get; set; }
|
||||
bool IsExiting { get; set; }
|
||||
bool IsTray { get; }
|
||||
RuntimeMode Mode { get; }
|
||||
|
||||
@@ -2,13 +2,6 @@ using System;
|
||||
|
||||
namespace NzbDrone.Common.EnvironmentInfo
|
||||
{
|
||||
public enum PlatformType
|
||||
{
|
||||
DotNet = 0,
|
||||
Mono = 1,
|
||||
NetCore = 2
|
||||
}
|
||||
|
||||
public interface IPlatformInfo
|
||||
{
|
||||
Version Version { get; }
|
||||
@@ -16,36 +9,18 @@ namespace NzbDrone.Common.EnvironmentInfo
|
||||
|
||||
public class PlatformInfo : IPlatformInfo
|
||||
{
|
||||
private static PlatformType _platform;
|
||||
private static Version _version;
|
||||
|
||||
static PlatformInfo()
|
||||
{
|
||||
_platform = PlatformType.NetCore;
|
||||
_version = Environment.Version;
|
||||
}
|
||||
|
||||
public static PlatformType Platform => _platform;
|
||||
public static bool IsMono => Platform == PlatformType.Mono;
|
||||
public static bool IsDotNet => Platform == PlatformType.DotNet;
|
||||
public static bool IsNetCore => Platform == PlatformType.NetCore;
|
||||
|
||||
public static string PlatformName
|
||||
{
|
||||
get
|
||||
{
|
||||
if (IsDotNet)
|
||||
{
|
||||
return ".NET";
|
||||
}
|
||||
else if (IsMono)
|
||||
{
|
||||
return "Mono";
|
||||
}
|
||||
else
|
||||
{
|
||||
return ".NET Core";
|
||||
}
|
||||
return ".NET";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ namespace NzbDrone.Common.EnvironmentInfo
|
||||
_logger = logger;
|
||||
|
||||
IsWindowsService = hostLifetime is WindowsServiceLifetime;
|
||||
IsStarting = true;
|
||||
|
||||
// net6.0 will return Radarr.dll for entry assembly, we need the actual
|
||||
// executable name (Radarr on linux). On mono this will return the location of
|
||||
@@ -82,6 +83,7 @@ namespace NzbDrone.Common.EnvironmentInfo
|
||||
|
||||
public bool IsWindowsService { get; private set; }
|
||||
|
||||
public bool IsStarting { get; set; }
|
||||
public bool IsExiting { get; set; }
|
||||
public bool IsTray
|
||||
{
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace NzbDrone.Common.Extensions
|
||||
|
||||
var info = new FileInfo(path.Trim());
|
||||
|
||||
//UNC
|
||||
// UNC
|
||||
if (OsInfo.IsWindows && info.FullName.StartsWith(@"\\"))
|
||||
{
|
||||
return info.FullName.TrimEnd('/', '\\', ' ');
|
||||
@@ -166,7 +166,7 @@ namespace NzbDrone.Common.Extensions
|
||||
var parentDirInfo = dirInfo.Parent;
|
||||
if (parentDirInfo == null)
|
||||
{
|
||||
//Drive letter
|
||||
// Drive letter
|
||||
return dirInfo.Name.ToUpper();
|
||||
}
|
||||
|
||||
@@ -238,9 +238,9 @@ namespace NzbDrone.Common.Extensions
|
||||
return null;
|
||||
}
|
||||
|
||||
public static string ProcessNameToExe(this string processName, PlatformType runtime)
|
||||
public static string ProcessNameToExe(this string processName)
|
||||
{
|
||||
if (OsInfo.IsWindows || runtime != PlatformType.NetCore)
|
||||
if (OsInfo.IsWindows)
|
||||
{
|
||||
processName += ".exe";
|
||||
}
|
||||
@@ -248,11 +248,6 @@ namespace NzbDrone.Common.Extensions
|
||||
return processName;
|
||||
}
|
||||
|
||||
public static string ProcessNameToExe(this string processName)
|
||||
{
|
||||
return processName.ProcessNameToExe(PlatformInfo.Platform);
|
||||
}
|
||||
|
||||
public static string GetAppDataPath(this IAppFolderInfo appFolderInfo)
|
||||
{
|
||||
return appFolderInfo.AppDataFolder;
|
||||
@@ -318,9 +313,9 @@ namespace NzbDrone.Common.Extensions
|
||||
return Path.Combine(GetUpdatePackageFolder(appFolderInfo), UPDATE_CLIENT_FOLDER_NAME);
|
||||
}
|
||||
|
||||
public static string GetUpdateClientExePath(this IAppFolderInfo appFolderInfo, PlatformType runtime)
|
||||
public static string GetUpdateClientExePath(this IAppFolderInfo appFolderInfo)
|
||||
{
|
||||
return Path.Combine(GetUpdateSandboxFolder(appFolderInfo), UPDATE_CLIENT_EXE_NAME).ProcessNameToExe(runtime);
|
||||
return Path.Combine(GetUpdateSandboxFolder(appFolderInfo), UPDATE_CLIENT_EXE_NAME).ProcessNameToExe();
|
||||
}
|
||||
|
||||
public static string GetDatabase(this IAppFolderInfo appFolderInfo)
|
||||
|
||||
@@ -131,7 +131,7 @@ namespace NzbDrone.Common.Extensions
|
||||
|
||||
public static string WrapInQuotes(this string text)
|
||||
{
|
||||
if (!text.Contains(" "))
|
||||
if (!text.Contains(' '))
|
||||
{
|
||||
return text;
|
||||
}
|
||||
@@ -255,7 +255,20 @@ namespace NzbDrone.Common.Extensions
|
||||
|
||||
public static string ToUrlHost(this string input)
|
||||
{
|
||||
return input.Contains(":") ? $"[{input}]" : input;
|
||||
return input.Contains(':') ? $"[{input}]" : input;
|
||||
}
|
||||
|
||||
public static bool IsAllDigits(this string input)
|
||||
{
|
||||
foreach (var c in input)
|
||||
{
|
||||
if (c < '0' || c > '9')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ namespace NzbDrone.Common.Http.Dispatchers
|
||||
{
|
||||
if (request.ResponseStream != null && responseMessage.StatusCode == HttpStatusCode.OK)
|
||||
{
|
||||
responseMessage.Content.CopyTo(request.ResponseStream, null, cts.Token);
|
||||
await responseMessage.Content.CopyToAsync(request.ResponseStream, null, cts.Token);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -11,15 +11,13 @@ namespace NzbDrone.Common.Http
|
||||
{
|
||||
if (response.Headers.ContainsKey("Retry-After"))
|
||||
{
|
||||
var retryAfter = response.Headers["Retry-After"].ToString();
|
||||
int seconds;
|
||||
DateTime date;
|
||||
var retryAfter = response.Headers["Retry-After"];
|
||||
|
||||
if (int.TryParse(retryAfter, out seconds))
|
||||
if (int.TryParse(retryAfter, out var seconds))
|
||||
{
|
||||
RetryAfter = TimeSpan.FromSeconds(seconds);
|
||||
}
|
||||
else if (DateTime.TryParse(retryAfter, out date))
|
||||
else if (DateTime.TryParse(retryAfter, out var date))
|
||||
{
|
||||
RetryAfter = date.ToUniversalTime() - DateTime.UtcNow;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NzbDrone.Common.Http
|
||||
{
|
||||
|
||||
@@ -226,7 +226,7 @@ namespace NzbDrone.Common.OAuth
|
||||
#if WINRT
|
||||
return CultureInfo.InvariantCulture.CompareInfo.Compare(left, right, CompareOptions.IgnoreCase) == 0;
|
||||
#else
|
||||
return string.Compare(left, right, StringComparison.InvariantCultureIgnoreCase) == 0;
|
||||
return string.Equals(left, right, StringComparison.InvariantCultureIgnoreCase);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DryIoc.dll" Version="5.3.1" />
|
||||
<PackageReference Include="DryIoc.dll" Version="5.3.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.1" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
|
||||
|
||||
@@ -64,7 +64,7 @@ namespace NzbDrone.Common
|
||||
|
||||
var args = $"create {serviceName} " +
|
||||
$"DisplayName= \"{serviceName}\" " +
|
||||
$"binpath= \"{Process.GetCurrentProcess().MainModule.FileName}\" " +
|
||||
$"binpath= \"{Environment.ProcessPath}\" " +
|
||||
"start= auto " +
|
||||
"depend= EventLog/Tcpip/http " +
|
||||
"obj= \"NT AUTHORITY\\LocalService\"";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
@@ -19,7 +19,7 @@ namespace NzbDrone.Common.TPL
|
||||
private readonly int _maxDegreeOfParallelism;
|
||||
|
||||
/// <summary>Whether the scheduler is currently processing work items.</summary>
|
||||
private int _delegatesQueuedOrRunning = 0;
|
||||
private int _delegatesQueuedOrRunning;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes an instance of the LimitedConcurrencyLevelTaskScheduler class with the
|
||||
|
||||
@@ -68,9 +68,9 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
|
||||
{
|
||||
var results = Subject.GetSearchRequests(new MovieSearchCriteria { Categories = new[] { NewznabStandardCategory.MoviesSD.Id, NewznabStandardCategory.MoviesDVD.Id } });
|
||||
|
||||
results.GetAllTiers().Should().HaveCount(1);
|
||||
results.Should().HaveCount(1);
|
||||
|
||||
var page = results.GetAllTiers().First().First();
|
||||
var page = results.First();
|
||||
|
||||
page.Url.Query.Should().Contain("&category=1%2C2");
|
||||
}
|
||||
@@ -81,9 +81,9 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
|
||||
_movieSearchCriteria.ImdbId = "0076759";
|
||||
var results = Subject.GetSearchRequests(_movieSearchCriteria);
|
||||
|
||||
results.GetAllTiers().Should().HaveCount(1);
|
||||
results.Should().HaveCount(1);
|
||||
|
||||
var page = results.GetAllTiers().First().First();
|
||||
var page = results.First();
|
||||
|
||||
page.Url.Query.Should().Contain("type=imdb");
|
||||
page.Url.Query.Should().Contain("query=tt0076759");
|
||||
@@ -96,9 +96,9 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
|
||||
|
||||
var results = Subject.GetSearchRequests(_movieSearchCriteria);
|
||||
|
||||
results.GetAllTiers().Should().HaveCount(1);
|
||||
results.Should().HaveCount(1);
|
||||
|
||||
var page = results.GetAllTiers().First().First();
|
||||
var page = results.First();
|
||||
|
||||
page.Url.Query.Should().Contain("type=name");
|
||||
page.Url.Query.Should().Contain("query=Star+Wars");
|
||||
|
||||
@@ -10,7 +10,7 @@ using NUnit.Framework;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Indexers.HDBits;
|
||||
using NzbDrone.Core.Indexers.Definitions.HDBits;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
@@ -5,7 +5,7 @@ using Newtonsoft.Json;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Indexers.HDBits;
|
||||
using NzbDrone.Core.Indexers.Definitions.HDBits;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
@@ -14,11 +14,13 @@ namespace NzbDrone.Core.Test.IndexerTests.HDBitsTests
|
||||
public class HDBitsRequestGeneratorFixture : CoreTest<HDBitsRequestGenerator>
|
||||
{
|
||||
private MovieSearchCriteria _movieSearchCriteria;
|
||||
private TvSearchCriteria _tvSearchSeasonEpisodeCriteria;
|
||||
private TvSearchCriteria _tvSearchDailyEpisodeCriteria;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
Subject.Settings = new HDBitsSettings()
|
||||
Subject.Settings = new HDBitsSettings
|
||||
{
|
||||
ApiKey = "abcd",
|
||||
Username = "somename"
|
||||
@@ -47,9 +49,25 @@ namespace NzbDrone.Core.Test.IndexerTests.HDBitsTests
|
||||
|
||||
_movieSearchCriteria = new MovieSearchCriteria
|
||||
{
|
||||
Categories = new int[] { 2000, 2010 },
|
||||
Categories = new[] { 2000, 2010 },
|
||||
ImdbId = "0076759"
|
||||
};
|
||||
|
||||
_tvSearchSeasonEpisodeCriteria = new TvSearchCriteria
|
||||
{
|
||||
Categories = new[] { 5000, 5010 },
|
||||
TvdbId = 392256,
|
||||
Season = 1,
|
||||
Episode = "3"
|
||||
};
|
||||
|
||||
_tvSearchDailyEpisodeCriteria = new TvSearchCriteria
|
||||
{
|
||||
Categories = new[] { 5000, 5010 },
|
||||
TvdbId = 289574,
|
||||
Season = 2023,
|
||||
Episode = "01/03"
|
||||
};
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -58,9 +76,9 @@ namespace NzbDrone.Core.Test.IndexerTests.HDBitsTests
|
||||
var results = Subject.GetSearchRequests(_movieSearchCriteria);
|
||||
var imdbQuery = int.Parse(_movieSearchCriteria.ImdbId);
|
||||
|
||||
results.GetAllTiers().Should().HaveCount(1);
|
||||
results.Should().HaveCount(1);
|
||||
|
||||
var page = results.GetAllTiers().First().First();
|
||||
var page = results.First();
|
||||
|
||||
var encoding = HttpHeader.GetEncodingFromContentType(page.HttpRequest.Headers.ContentType);
|
||||
|
||||
@@ -70,5 +88,49 @@ namespace NzbDrone.Core.Test.IndexerTests.HDBitsTests
|
||||
query.Category.Should().HaveCount(1);
|
||||
query.ImdbInfo.Id.Should().Be(imdbQuery);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_search_by_tvdbid_season_episode_if_supported()
|
||||
{
|
||||
var results = Subject.GetSearchRequests(_tvSearchSeasonEpisodeCriteria);
|
||||
var tvdbQuery = _tvSearchSeasonEpisodeCriteria.TvdbId;
|
||||
|
||||
results.Should().HaveCount(1);
|
||||
|
||||
var page = results.First();
|
||||
|
||||
var encoding = HttpHeader.GetEncodingFromContentType(page.HttpRequest.Headers.ContentType);
|
||||
|
||||
var body = encoding.GetString(page.HttpRequest.ContentData);
|
||||
var query = JsonConvert.DeserializeObject<TorrentQuery>(body);
|
||||
|
||||
query.Category.Should().HaveCount(3);
|
||||
query.TvdbInfo.Id.Should().Be(tvdbQuery);
|
||||
query.Search.Should().BeNullOrWhiteSpace();
|
||||
query.TvdbInfo.Season.Should().Be(1);
|
||||
query.TvdbInfo.Episode.Should().Be("3");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_search_by_tvdbid_daily_episode_if_supported()
|
||||
{
|
||||
var results = Subject.GetSearchRequests(_tvSearchDailyEpisodeCriteria);
|
||||
var tvdbQuery = _tvSearchDailyEpisodeCriteria.TvdbId;
|
||||
|
||||
results.Should().HaveCount(1);
|
||||
|
||||
var page = results.First();
|
||||
|
||||
var encoding = HttpHeader.GetEncodingFromContentType(page.HttpRequest.Headers.ContentType);
|
||||
|
||||
var body = encoding.GetString(page.HttpRequest.ContentData);
|
||||
var query = JsonConvert.DeserializeObject<TorrentQuery>(body);
|
||||
|
||||
query.Category.Should().HaveCount(3);
|
||||
query.TvdbInfo.Id.Should().Be(tvdbQuery);
|
||||
query.Search.Should().Be("2023-01-03");
|
||||
query.TvdbInfo.Season.Should().BeNull();
|
||||
query.TvdbInfo.Episode.Should().BeNull();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,9 +51,9 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
_movieSearchCriteria.Offset = 0;
|
||||
var results = Subject.GetSearchRequests(_movieSearchCriteria);
|
||||
|
||||
results.GetAllTiers().Should().HaveCount(1);
|
||||
results.Should().HaveCount(1);
|
||||
|
||||
var pages = results.GetAllTiers().First().Take(3).ToList();
|
||||
var pages = results.Take(3).ToList();
|
||||
|
||||
pages[0].Url.FullUri.Should().Contain("&offset=0");
|
||||
}
|
||||
@@ -63,9 +63,9 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
{
|
||||
var results = Subject.GetSearchRequests(_movieSearchCriteria);
|
||||
|
||||
results.GetAllTiers().Should().HaveCount(1);
|
||||
results.Should().HaveCount(1);
|
||||
|
||||
var pages = results.GetAllTiers().First().Take(500).ToList();
|
||||
var pages = results.Take(500).ToList();
|
||||
|
||||
pages.Count.Should().BeLessThan(500);
|
||||
}
|
||||
@@ -77,9 +77,9 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
|
||||
var results = Subject.GetSearchRequests(_movieSearchCriteria);
|
||||
|
||||
results.GetAllTiers().Should().HaveCount(1);
|
||||
results.Should().HaveCount(1);
|
||||
|
||||
var page = results.GetAllTiers().First().First();
|
||||
var page = results.First();
|
||||
|
||||
page.Url.Query.Should().NotContain("imdbid=0076759");
|
||||
page.Url.Query.Should().Contain("q=Star");
|
||||
@@ -92,9 +92,9 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
_capabilities.MovieSearchParams = new List<MovieSearchParam> { MovieSearchParam.Q, MovieSearchParam.ImdbId };
|
||||
|
||||
var results = Subject.GetSearchRequests(_movieSearchCriteria);
|
||||
results.GetTier(0).Should().HaveCount(1);
|
||||
results.Should().HaveCount(1);
|
||||
|
||||
var page = results.GetAllTiers().First().First();
|
||||
var page = results.First();
|
||||
|
||||
page.Url.Query.Should().Contain("imdbid=0076759");
|
||||
}
|
||||
@@ -106,9 +106,9 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
_capabilities.MovieSearchParams = new List<MovieSearchParam> { MovieSearchParam.Q, MovieSearchParam.TmdbId };
|
||||
|
||||
var results = Subject.GetSearchRequests(_movieSearchCriteria);
|
||||
results.GetTier(0).Should().HaveCount(1);
|
||||
results.Should().HaveCount(1);
|
||||
|
||||
var page = results.GetAllTiers().First().First();
|
||||
var page = results.First();
|
||||
|
||||
page.Url.Query.Should().Contain("tmdbid=11");
|
||||
}
|
||||
@@ -120,9 +120,9 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
_capabilities.MovieSearchParams = new List<MovieSearchParam> { MovieSearchParam.Q, MovieSearchParam.ImdbId, MovieSearchParam.TmdbId };
|
||||
|
||||
var results = Subject.GetSearchRequests(_movieSearchCriteria);
|
||||
results.GetTier(0).Should().HaveCount(1);
|
||||
results.Should().HaveCount(1);
|
||||
|
||||
var page = results.GetAllTiers().First().First();
|
||||
var page = results.First();
|
||||
|
||||
page.Url.Query.Should().Contain("tmdbid=11");
|
||||
page.Url.Query.Should().NotContain("imdbid=0076759");
|
||||
@@ -136,9 +136,9 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
_capabilities.MovieSearchParams = new List<MovieSearchParam> { MovieSearchParam.Q, MovieSearchParam.ImdbId, MovieSearchParam.TmdbId };
|
||||
|
||||
var results = Subject.GetSearchRequests(_movieSearchCriteria);
|
||||
results.GetTier(0).Should().HaveCount(1);
|
||||
results.Should().HaveCount(1);
|
||||
|
||||
var page = results.GetTier(0).First().First();
|
||||
var page = results.First();
|
||||
|
||||
page.Url.Query.Should().Contain("tmdbid=11");
|
||||
page.Url.Query.Should().Contain("imdbid=0076759");
|
||||
@@ -150,10 +150,9 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
_capabilities.MovieSearchParams = new List<MovieSearchParam> { MovieSearchParam.Q };
|
||||
|
||||
var results = Subject.GetSearchRequests(_movieSearchCriteria);
|
||||
results.Tiers.Should().Be(1);
|
||||
results.GetTier(0).Should().HaveCount(1);
|
||||
results.Should().HaveCount(1);
|
||||
|
||||
var page = results.GetTier(0).First().First();
|
||||
var page = results.First();
|
||||
|
||||
page.Url.Query.Should().Contain("q=");
|
||||
}
|
||||
@@ -167,7 +166,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
|
||||
var results = Subject.GetSearchRequests(_movieSearchCriteria);
|
||||
|
||||
var page = results.GetTier(0).First().First();
|
||||
var page = results.First();
|
||||
|
||||
page.Url.Query.Should().Contain("q=");
|
||||
}
|
||||
@@ -178,9 +177,9 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
_capabilities.MovieSearchParams = new List<MovieSearchParam> { MovieSearchParam.Q, MovieSearchParam.ImdbId, MovieSearchParam.TmdbId };
|
||||
|
||||
var results = Subject.GetSearchRequests(_movieSearchCriteria);
|
||||
results.Tiers.Should().Be(1);
|
||||
results.Should().HaveCount(1);
|
||||
|
||||
var pageTier2 = results.GetTier(0).First().First();
|
||||
var pageTier2 = results.First();
|
||||
|
||||
pageTier2.Url.Query.Should().NotContain("tmdbid=11");
|
||||
pageTier2.Url.Query.Should().NotContain("imdbid=0076759");
|
||||
@@ -193,9 +192,9 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
_capabilities.TvSearchParams = new List<TvSearchParam> { TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep };
|
||||
|
||||
var results = Subject.GetSearchRequests(_tvSearchCriteria);
|
||||
results.Tiers.Should().Be(1);
|
||||
results.Should().HaveCount(1);
|
||||
|
||||
var pageTier = results.GetTier(0).First().First();
|
||||
var pageTier = results.First();
|
||||
|
||||
pageTier.Url.Query.Should().Contain("season=00");
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ using NUnit.Framework;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Indexers.Rarbg;
|
||||
using NzbDrone.Core.Indexers.Definitions.Rarbg;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
@@ -23,14 +23,14 @@ namespace NzbDrone.Core.Test.IndexerTests.RarbgTests
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
Subject.Definition = new IndexerDefinition()
|
||||
Subject.Definition = new IndexerDefinition
|
||||
{
|
||||
Name = "Rarbg",
|
||||
Settings = new RarbgSettings()
|
||||
};
|
||||
|
||||
Mocker.GetMock<IRarbgTokenProvider>()
|
||||
.Setup(v => v.GetToken(It.IsAny<RarbgSettings>()))
|
||||
.Setup(v => v.GetToken(It.IsAny<RarbgSettings>(), Subject.RateLimit))
|
||||
.Returns("validtoken");
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ namespace NzbDrone.Core.Test.IndexerTests.RarbgTests
|
||||
torrentInfo.Title.Should().Be("Sense8.S01E01.WEBRip.x264-FGT");
|
||||
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
||||
torrentInfo.DownloadUrl.Should().Be("magnet:?xt=urn:btih:d8bde635f573acb390c7d7e7efc1556965fdc802&dn=Sense8.S01E01.WEBRip.x264-FGT&tr=http%3A%2F%2Ftracker.trackerfix.com%3A80%2Fannounce&tr=udp%3A%2F%2F9.rarbg.me%3A2710&tr=udp%3A%2F%2F9.rarbg.to%3A2710&tr=udp%3A%2F%2Fopen.demonii.com%3A1337%2Fannounce");
|
||||
torrentInfo.InfoUrl.Should().Be($"https://torrentapi.org/redirect_to_info.php?token=i5cx7b9agd&p=8_6_4_4_5_6__d8bde635f5&app_id={BuildInfo.AppName}");
|
||||
torrentInfo.InfoUrl.Should().Be($"https://torrentapi.org/redirect_to_info.php?token=i5cx7b9agd&p=8_6_4_4_5_6__d8bde635f5&app_id=rralworP_{BuildInfo.Version}");
|
||||
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2015-06-05 16:58:11 +0000").ToUniversalTime());
|
||||
torrentInfo.Size.Should().Be(564198371);
|
||||
|
||||
@@ -162,7 +162,7 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests
|
||||
releaseInfo.InfoHash.Should().Be("(removed)");
|
||||
releaseInfo.Seeders.Should().Be(3);
|
||||
releaseInfo.Peers.Should().Be(3);
|
||||
releaseInfo.Categories.Count().Should().Be(4);
|
||||
releaseInfo.Categories.Count.Should().Be(4);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using FluentAssertions;
|
||||
using FluentValidation.Results;
|
||||
using NUnit.Framework;
|
||||
@@ -56,6 +55,11 @@ namespace NzbDrone.Core.Test.NotificationTests
|
||||
{
|
||||
TestLogger.Info("OnApplicationUpdate was called");
|
||||
}
|
||||
|
||||
public override void OnGrab(GrabMessage message)
|
||||
{
|
||||
TestLogger.Info("OnGrab was called");
|
||||
}
|
||||
}
|
||||
|
||||
private class TestNotificationWithNoEvents : NotificationBase<TestSetting>
|
||||
@@ -76,6 +80,7 @@ namespace NzbDrone.Core.Test.NotificationTests
|
||||
|
||||
notification.SupportsOnHealthIssue.Should().BeTrue();
|
||||
notification.SupportsOnApplicationUpdate.Should().BeTrue();
|
||||
notification.SupportsOnGrab.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -85,6 +90,7 @@ namespace NzbDrone.Core.Test.NotificationTests
|
||||
|
||||
notification.SupportsOnHealthIssue.Should().BeFalse();
|
||||
notification.SupportsOnApplicationUpdate.Should().BeFalse();
|
||||
notification.SupportsOnGrab.Should().BeFalse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
43
src/NzbDrone.Core.Test/ParserTests/DateTimeUtilFixture.cs
Normal file
43
src/NzbDrone.Core.Test/ParserTests/DateTimeUtilFixture.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Threading;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.ParserTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class DateTimeUtilFixture : CoreTest
|
||||
{
|
||||
[TestCase("pt-BR")]
|
||||
[TestCase("en-US")]
|
||||
public void should_format_date_invariant(string culture)
|
||||
{
|
||||
Thread.CurrentThread.CurrentCulture = new CultureInfo(culture);
|
||||
|
||||
var dateNow = DateTime.Now;
|
||||
|
||||
DateTimeUtil.FromUnknown(dateNow.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture))
|
||||
.ToString(DateTimeUtil.Rfc1123ZPattern, CultureInfo.InvariantCulture)
|
||||
.Should().Be(dateNow.ToString("ddd, dd MMM yyyy HH':'mm':'ss z", CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
[TestCase("2022-08-08 02:07:39 -02:00", "2006-01-02 15:04:05 -07:00", "yyyy-MM-dd HH:mm:ss zzz", "2022-08-08 04:07:39 +00:00")]
|
||||
[TestCase("2022-08-08 02:07:39 -02:00", "yyyy-MM-dd HH:mm:ss zzz", "yyyy-MM-dd HH:mm:ss zzz", "2022-08-08 04:07:39 +00:00")]
|
||||
[TestCase("2022-08-08 -02:00", "2006-01-02 -07:00", "yyyy-MM-dd zzz", "2022-08-08 +00:00")]
|
||||
[TestCase("2022-08-08 -02:00", "yyyy-MM-dd zzz", "yyyy-MM-dd zzz", "2022-08-08 +00:00")]
|
||||
[TestCase("02:07:39 -02:00", "15:04:05 -07:00", "HH:mm:ss zzz", "04:07:39 +00:00")]
|
||||
[TestCase("02:07:39 -02:00", "HH:mm:ss zzz", "HH:mm:ss zzz", "04:07:39 +00:00")]
|
||||
[TestCase("-02:00", "zzz", "zzz", "+00:00")]
|
||||
[TestCase("-02:00", "-07:00", "zzz", "+00:00")]
|
||||
public void parse_datetime_golang(string dateInput, string format, string standardFormat, string expectedDate)
|
||||
{
|
||||
DateTimeUtil.ParseDateTimeGoLang(dateInput, format)
|
||||
.ToUniversalTime()
|
||||
.ToString(standardFormat, CultureInfo.InvariantCulture)
|
||||
.Should().Be(expectedDate);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,5 +52,16 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
{
|
||||
ParseUtil.CoerceDouble(original).Should().Be(parsedInt);
|
||||
}
|
||||
|
||||
[TestCase(null, null)]
|
||||
[TestCase("", null)]
|
||||
[TestCase("1", 1)]
|
||||
[TestCase("1000 grabs", 1000)]
|
||||
[TestCase("asdf123asdf", 123)]
|
||||
[TestCase("asdf123asdf456asdf", 123)]
|
||||
public void should_parse_long_from_string(string original, long? parsedInt)
|
||||
{
|
||||
ParseUtil.GetLongFromString(original).Should().Be(parsedInt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<PackageReference Include="Dapper" Version="2.0.123" />
|
||||
<PackageReference Include="NBuilder" Version="6.1.0" />
|
||||
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
||||
<PackageReference Include="YamlDotNet" Version="12.3.1" />
|
||||
<PackageReference Include="YamlDotNet" Version="13.0.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\NzbDrone.Test.Common\Prowlarr.Test.Common.csproj" />
|
||||
|
||||
@@ -40,12 +40,11 @@ namespace NzbDrone.Core.Test.UpdateTests
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Ignore("TODO No Updates On Server")]
|
||||
public void should_get_recent_updates()
|
||||
{
|
||||
const string branch = "develop";
|
||||
UseRealHttp();
|
||||
var recent = Subject.GetRecentUpdates(branch, new Version(2, 0), null);
|
||||
var recent = Subject.GetRecentUpdates(branch, new Version(1, 0), null);
|
||||
var recentWithChanges = recent.Where(c => c.Changes != null);
|
||||
|
||||
recent.Should().NotBeEmpty();
|
||||
|
||||
@@ -69,7 +69,7 @@ namespace NzbDrone.Core.Test.UpdateTests
|
||||
.Returns(true);
|
||||
|
||||
Mocker.GetMock<IDiskProvider>()
|
||||
.Setup(v => v.FileExists(It.Is<string>(s => s.EndsWith("Prowlarr.Update.exe"))))
|
||||
.Setup(v => v.FileExists(It.Is<string>(s => s.EndsWith("Prowlarr.Update".ProcessNameToExe()))))
|
||||
.Returns(true);
|
||||
|
||||
_sandboxFolder = Mocker.GetMock<IAppFolderInfo>().Object.GetUpdateSandboxFolder();
|
||||
@@ -165,7 +165,7 @@ namespace NzbDrone.Core.Test.UpdateTests
|
||||
public void should_return_with_warning_if_updater_doesnt_exists()
|
||||
{
|
||||
Mocker.GetMock<IDiskProvider>()
|
||||
.Setup(v => v.FileExists(It.Is<string>(s => s.EndsWith("Prowlarr.Update.exe"))))
|
||||
.Setup(v => v.FileExists(It.Is<string>(s => s.EndsWith("Prowlarr.Update".ProcessNameToExe()))))
|
||||
.Returns(false);
|
||||
|
||||
Subject.Execute(new ApplicationUpdateCommand());
|
||||
|
||||
@@ -19,14 +19,14 @@ namespace NzbDrone.Core.Authentication
|
||||
|
||||
public class UserService : IUserService
|
||||
{
|
||||
private const int ITERATIONS = 10000;
|
||||
private const int SALT_SIZE = 128 / 8;
|
||||
private const int NUMBER_OF_BYTES = 256 / 8;
|
||||
|
||||
private readonly IUserRepository _repo;
|
||||
private readonly IAppFolderInfo _appFolderInfo;
|
||||
private readonly IDiskProvider _diskProvider;
|
||||
|
||||
private static readonly int ITERATIONS = 10000;
|
||||
private static readonly int SALT_SIZE = 128 / 8;
|
||||
private static readonly int NUMBER_OF_BYTES = 256 / 8;
|
||||
|
||||
public UserService(IUserRepository repo, IAppFolderInfo appFolderInfo, IDiskProvider diskProvider)
|
||||
{
|
||||
_repo = repo;
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(029)]
|
||||
public class add_on_grab_to_notifications : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Alter.Table("Notifications").AddColumn("OnGrab").AsBoolean().WithDefaultValue(false);
|
||||
Alter.Table("Notifications").AddColumn("IncludeManualGrabs").AsBoolean().WithDefaultValue(false).NotNullable();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Text;
|
||||
@@ -250,7 +250,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
||||
}
|
||||
|
||||
Index = end + 1;
|
||||
identifier.Append(Buffer.Substring(start, end - start));
|
||||
identifier.Append(Buffer.AsSpan(start, end - start));
|
||||
|
||||
if (Buffer[Index] != escape)
|
||||
{
|
||||
|
||||
@@ -66,6 +66,7 @@ namespace NzbDrone.Core.Datastore
|
||||
|
||||
Mapper.Entity<NotificationDefinition>("Notifications").RegisterModel()
|
||||
.Ignore(x => x.ImplementationName)
|
||||
.Ignore(i => i.SupportsOnGrab)
|
||||
.Ignore(i => i.SupportsOnHealthIssue)
|
||||
.Ignore(i => i.SupportsOnApplicationUpdate);
|
||||
|
||||
|
||||
@@ -15,9 +15,9 @@ namespace NzbDrone.Core.Datastore
|
||||
|
||||
private const DbType EnumerableMultiParameter = (DbType)(-1);
|
||||
private readonly string _paramNamePrefix;
|
||||
private readonly bool _requireConcreteValue = false;
|
||||
private int _paramCount = 0;
|
||||
private bool _gotConcreteValue = false;
|
||||
private readonly bool _requireConcreteValue;
|
||||
private int _paramCount;
|
||||
private bool _gotConcreteValue;
|
||||
|
||||
public WhereBuilderPostgres(Expression filter, bool requireConcreteValue, int seq)
|
||||
{
|
||||
|
||||
@@ -15,9 +15,9 @@ namespace NzbDrone.Core.Datastore
|
||||
|
||||
private const DbType EnumerableMultiParameter = (DbType)(-1);
|
||||
private readonly string _paramNamePrefix;
|
||||
private readonly bool _requireConcreteValue = false;
|
||||
private int _paramCount = 0;
|
||||
private bool _gotConcreteValue = false;
|
||||
private readonly bool _requireConcreteValue;
|
||||
private int _paramCount;
|
||||
private bool _gotConcreteValue;
|
||||
|
||||
public WhereBuilderSqlite(Expression filter, bool requireConcreteValue, int seq)
|
||||
{
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using NLog;
|
||||
using NzbDrone.Common.EnsureThat;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Common.Instrumentation.Extensions;
|
||||
@@ -61,16 +60,17 @@ namespace NzbDrone.Core.Download
|
||||
|
||||
// Get the seed configuration for this release.
|
||||
// remoteMovie.SeedConfiguration = _seedConfigProvider.GetSeedConfiguration(remoteMovie);
|
||||
|
||||
// Limit grabs to 2 per second.
|
||||
if (release.DownloadUrl.IsNotNullOrWhiteSpace() && !release.DownloadUrl.StartsWith("magnet:"))
|
||||
{
|
||||
var url = new HttpUri(release.DownloadUrl);
|
||||
_rateLimitService.WaitAndPulse(url.Host, TimeSpan.FromSeconds(2));
|
||||
}
|
||||
|
||||
var indexer = _indexerFactory.GetInstance(_indexerFactory.Get(release.IndexerId));
|
||||
|
||||
var grabEvent = new IndexerDownloadEvent(release, true, source, host, release.Title, release.DownloadUrl)
|
||||
{
|
||||
DownloadClient = downloadClient.Name,
|
||||
DownloadClientId = downloadClient.Definition.Id,
|
||||
DownloadClientName = downloadClient.Definition.Name,
|
||||
Redirect = redirect,
|
||||
GrabTrigger = source == "Prowlarr" ? GrabTrigger.Manual : GrabTrigger.Api
|
||||
};
|
||||
|
||||
string downloadClientId;
|
||||
try
|
||||
{
|
||||
@@ -81,19 +81,20 @@ namespace NzbDrone.Core.Download
|
||||
catch (ReleaseUnavailableException)
|
||||
{
|
||||
_logger.Trace("Release {0} no longer available on indexer.", release);
|
||||
_eventAggregator.PublishEvent(new IndexerDownloadEvent(release.IndexerId, false, source, host, release.Title, release.DownloadUrl, redirect));
|
||||
grabEvent.Successful = false;
|
||||
_eventAggregator.PublishEvent(grabEvent);
|
||||
throw;
|
||||
}
|
||||
catch (DownloadClientRejectedReleaseException)
|
||||
{
|
||||
_logger.Trace("Release {0} rejected by download client, possible duplicate.", release);
|
||||
_eventAggregator.PublishEvent(new IndexerDownloadEvent(release.IndexerId, false, source, host, release.Title, release.DownloadUrl, redirect));
|
||||
grabEvent.Successful = false;
|
||||
_eventAggregator.PublishEvent(grabEvent);
|
||||
throw;
|
||||
}
|
||||
catch (ReleaseDownloadException ex)
|
||||
{
|
||||
var http429 = ex.InnerException as TooManyRequestsException;
|
||||
if (http429 != null)
|
||||
if (ex.InnerException is TooManyRequestsException http429)
|
||||
{
|
||||
_indexerStatusService.RecordFailure(release.IndexerId, http429.RetryAfter);
|
||||
}
|
||||
@@ -102,14 +103,21 @@ namespace NzbDrone.Core.Download
|
||||
_indexerStatusService.RecordFailure(release.IndexerId);
|
||||
}
|
||||
|
||||
_eventAggregator.PublishEvent(new IndexerDownloadEvent(release.IndexerId, false, source, host, release.Title, release.DownloadUrl, redirect));
|
||||
grabEvent.Successful = false;
|
||||
|
||||
_eventAggregator.PublishEvent(grabEvent);
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
_logger.ProgressInfo("Report sent to {0}. {1}", downloadClient.Definition.Name, downloadTitle);
|
||||
|
||||
_eventAggregator.PublishEvent(new IndexerDownloadEvent(release.IndexerId, true, source, host, release.Title, release.DownloadUrl, redirect));
|
||||
if (!string.IsNullOrWhiteSpace(downloadClientId))
|
||||
{
|
||||
grabEvent.DownloadId = downloadClientId;
|
||||
}
|
||||
|
||||
_eventAggregator.PublishEvent(grabEvent);
|
||||
}
|
||||
|
||||
public async Task<byte[]> DownloadReport(string link, int indexerId, string source, string host, string title)
|
||||
@@ -127,22 +135,35 @@ namespace NzbDrone.Core.Download
|
||||
var success = false;
|
||||
var downloadedBytes = Array.Empty<byte>();
|
||||
|
||||
var release = new ReleaseInfo
|
||||
{
|
||||
Title = title,
|
||||
DownloadUrl = link,
|
||||
IndexerId = indexerId,
|
||||
Indexer = indexer.Definition.Name,
|
||||
DownloadProtocol = indexer.Protocol
|
||||
};
|
||||
|
||||
var grabEvent = new IndexerDownloadEvent(release, success, source, host, release.Title, release.DownloadUrl)
|
||||
{
|
||||
GrabTrigger = source == "Prowlarr" ? GrabTrigger.Manual : GrabTrigger.Api
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
downloadedBytes = await indexer.Download(url);
|
||||
_indexerStatusService.RecordSuccess(indexerId);
|
||||
success = true;
|
||||
grabEvent.Successful = true;
|
||||
}
|
||||
catch (ReleaseUnavailableException)
|
||||
{
|
||||
_logger.Trace("Release {0} no longer available on indexer.", link);
|
||||
_eventAggregator.PublishEvent(new IndexerDownloadEvent(indexerId, success, source, host, title, url.AbsoluteUri));
|
||||
_eventAggregator.PublishEvent(grabEvent);
|
||||
throw;
|
||||
}
|
||||
catch (ReleaseDownloadException ex)
|
||||
{
|
||||
var http429 = ex.InnerException as TooManyRequestsException;
|
||||
if (http429 != null)
|
||||
if (ex.InnerException is TooManyRequestsException http429)
|
||||
{
|
||||
_indexerStatusService.RecordFailure(indexerId, http429.RetryAfter);
|
||||
}
|
||||
@@ -151,17 +172,36 @@ namespace NzbDrone.Core.Download
|
||||
_indexerStatusService.RecordFailure(indexerId);
|
||||
}
|
||||
|
||||
_eventAggregator.PublishEvent(new IndexerDownloadEvent(indexerId, success, source, host, title, url.AbsoluteUri));
|
||||
_eventAggregator.PublishEvent(grabEvent);
|
||||
throw;
|
||||
}
|
||||
|
||||
_eventAggregator.PublishEvent(new IndexerDownloadEvent(indexerId, success, source, host, title, url.AbsoluteUri));
|
||||
_logger.Trace("Downloaded {0} bytes from {1}", downloadedBytes.Length, link);
|
||||
_eventAggregator.PublishEvent(grabEvent);
|
||||
|
||||
return downloadedBytes;
|
||||
}
|
||||
|
||||
public void RecordRedirect(string link, int indexerId, string source, string host, string title)
|
||||
{
|
||||
_eventAggregator.PublishEvent(new IndexerDownloadEvent(indexerId, true, source, host, title, link, true));
|
||||
var indexer = _indexerFactory.GetInstance(_indexerFactory.Get(indexerId));
|
||||
|
||||
var release = new ReleaseInfo
|
||||
{
|
||||
Title = title,
|
||||
DownloadUrl = link,
|
||||
IndexerId = indexerId,
|
||||
Indexer = indexer.Definition.Name,
|
||||
DownloadProtocol = indexer.Protocol
|
||||
};
|
||||
|
||||
var grabEvent = new IndexerDownloadEvent(release, true, source, host, release.Title, release.DownloadUrl)
|
||||
{
|
||||
Redirect = true,
|
||||
GrabTrigger = source == "Prowlarr" ? GrabTrigger.Manual : GrabTrigger.Api
|
||||
};
|
||||
|
||||
_eventAggregator.PublishEvent(grabEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,8 +34,8 @@ namespace NzbDrone.Core.HealthCheck
|
||||
|
||||
private readonly ICached<HealthCheck> _healthCheckResults;
|
||||
|
||||
private bool _hasRunHealthChecksAfterGracePeriod = false;
|
||||
private bool _isRunningHealthChecksAfterGracePeriod = false;
|
||||
private bool _hasRunHealthChecksAfterGracePeriod;
|
||||
private bool _isRunningHealthChecksAfterGracePeriod;
|
||||
|
||||
public HealthCheckService(IEnumerable<IProvideHealthCheck> healthChecks,
|
||||
IServerSideNotificationService serverSideNotificationService,
|
||||
@@ -55,7 +55,7 @@ namespace NzbDrone.Core.HealthCheck
|
||||
_startupHealthChecks = _healthChecks.Where(v => v.CheckOnStartup).ToArray();
|
||||
_scheduledHealthChecks = _healthChecks.Where(v => v.CheckOnSchedule).ToArray();
|
||||
_eventDrivenHealthChecks = GetEventDrivenHealthChecks();
|
||||
_startupGracePeriodEndTime = runtimeInfo.StartTime + TimeSpan.FromMinutes(15);
|
||||
_startupGracePeriodEndTime = runtimeInfo.StartTime.AddMinutes(15);
|
||||
}
|
||||
|
||||
public List<HealthCheck> Results()
|
||||
|
||||
@@ -52,7 +52,7 @@ namespace NzbDrone.Core.HealthCheck
|
||||
.AddQueryParam("version", BuildInfo.Version)
|
||||
.AddQueryParam("os", OsInfo.Os.ToString().ToLowerInvariant())
|
||||
.AddQueryParam("arch", RuntimeInformation.OSArchitecture)
|
||||
.AddQueryParam("runtime", PlatformInfo.Platform.ToString().ToLowerInvariant())
|
||||
.AddQueryParam("runtime", "netcore")
|
||||
.AddQueryParam("branch", _configFileProvider.Branch)
|
||||
.Build();
|
||||
try
|
||||
|
||||
@@ -19,6 +19,7 @@ namespace NzbDrone.Core.History
|
||||
List<History> Since(DateTime date, HistoryEventType? eventType);
|
||||
void Cleanup(int days);
|
||||
int CountSince(int indexerId, DateTime date, List<HistoryEventType> eventTypes);
|
||||
History FindFirstForIndexerSince(int indexerId, DateTime date, List<HistoryEventType> eventTypes, int limit);
|
||||
}
|
||||
|
||||
public class HistoryRepository : BasicRepository<History>, IHistoryRepository
|
||||
@@ -115,5 +116,24 @@ namespace NzbDrone.Core.History
|
||||
return conn.ExecuteScalar<int>(sql.RawSql, sql.Parameters);
|
||||
}
|
||||
}
|
||||
|
||||
public History FindFirstForIndexerSince(int indexerId, DateTime date, List<HistoryEventType> eventTypes, int limit)
|
||||
{
|
||||
var intEvents = eventTypes.Select(t => (int)t).ToList();
|
||||
|
||||
var builder = Builder()
|
||||
.Where<History>(x => x.IndexerId == indexerId)
|
||||
.Where<History>(x => x.Date >= date)
|
||||
.Where<History>(x => intEvents.Contains((int)x.EventType));
|
||||
|
||||
var query = Query(builder);
|
||||
|
||||
if (limit > 0)
|
||||
{
|
||||
query = query.OrderByDescending(h => h.Date).Take(limit).ToList();
|
||||
}
|
||||
|
||||
return query.MinBy(h => h.Date);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ namespace NzbDrone.Core.History
|
||||
List<History> Between(DateTime start, DateTime end);
|
||||
List<History> Since(DateTime date, HistoryEventType? eventType);
|
||||
int CountSince(int indexerId, DateTime date, List<HistoryEventType> eventTypes);
|
||||
History FindFirstForIndexerSince(int indexerId, DateTime date, List<HistoryEventType> eventTypes, int limit);
|
||||
}
|
||||
|
||||
public class HistoryService : IHistoryService,
|
||||
@@ -121,7 +122,7 @@ namespace NzbDrone.Core.History
|
||||
{
|
||||
Date = DateTime.UtcNow,
|
||||
IndexerId = message.IndexerId,
|
||||
EventType = message.Query.RssSearch ? HistoryEventType.IndexerRss : HistoryEventType.IndexerQuery,
|
||||
EventType = message.Query.IsRssSearch ? HistoryEventType.IndexerRss : HistoryEventType.IndexerQuery,
|
||||
Successful = message.QueryResult.Response?.StatusCode == HttpStatusCode.OK
|
||||
};
|
||||
|
||||
@@ -173,7 +174,7 @@ namespace NzbDrone.Core.History
|
||||
history.Data.Add("Categories", string.Join(",", message.Query.Categories) ?? string.Empty);
|
||||
history.Data.Add("Source", message.Query.Source ?? string.Empty);
|
||||
history.Data.Add("Host", message.Query.Host ?? string.Empty);
|
||||
history.Data.Add("QueryResults", message.QueryResult.Releases?.Count().ToString() ?? string.Empty);
|
||||
history.Data.Add("QueryResults", message.QueryResult.Releases?.Count.ToString() ?? string.Empty);
|
||||
history.Data.Add("Url", message.QueryResult.Response?.Request.Url.FullUri ?? string.Empty);
|
||||
|
||||
_historyRepository.Insert(history);
|
||||
@@ -184,7 +185,7 @@ namespace NzbDrone.Core.History
|
||||
var history = new History
|
||||
{
|
||||
Date = DateTime.UtcNow,
|
||||
IndexerId = message.IndexerId,
|
||||
IndexerId = message.Release.IndexerId,
|
||||
EventType = HistoryEventType.ReleaseGrabbed,
|
||||
Successful = message.Successful
|
||||
};
|
||||
@@ -232,5 +233,10 @@ namespace NzbDrone.Core.History
|
||||
{
|
||||
return _historyRepository.CountSince(indexerId, date, eventTypes);
|
||||
}
|
||||
|
||||
public History FindFirstForIndexerSince(int indexerId, DateTime date, List<HistoryEventType> eventTypes, int limit)
|
||||
{
|
||||
return _historyRepository.FindFirstForIndexerSince(indexerId, date, eventTypes, limit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text.RegularExpressions;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
|
||||
namespace NzbDrone.Core.Http.CloudFlare
|
||||
{
|
||||
public class CloudFlareDetectionService
|
||||
{
|
||||
private static readonly HashSet<string> CloudflareServerNames = new HashSet<string> { "cloudflare", "cloudflare-nginx", "ddos-guard" };
|
||||
private static readonly HashSet<string> CloudflareServerNames = new () { "cloudflare", "cloudflare-nginx", "ddos-guard" };
|
||||
private readonly Logger _logger;
|
||||
|
||||
public CloudFlareDetectionService(Logger logger)
|
||||
@@ -33,7 +34,7 @@ namespace NzbDrone.Core.Http.CloudFlare
|
||||
responseHtml.Contains("<title>Access denied</title>") ||
|
||||
responseHtml.Contains("<title>Attention Required! | Cloudflare</title>") ||
|
||||
responseHtml.Trim().Equals("error code: 1020") ||
|
||||
responseHtml.Contains("<title>DDOS-GUARD</title>"))
|
||||
responseHtml.Contains("<title>DDOS-GUARD</title>", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -41,7 +42,7 @@ namespace NzbDrone.Core.Http.CloudFlare
|
||||
|
||||
// detect Custom CloudFlare for EbookParadijs, Film-Paleis, MuziekFabriek and Puur-Hollands
|
||||
if (response.Headers.Vary == "Accept-Encoding,User-Agent" &&
|
||||
response.Headers.ContentEncoding == "" &&
|
||||
response.Headers.ContentEncoding.IsNullOrWhiteSpace() &&
|
||||
response.Content.ToLower().Contains("ddos"))
|
||||
{
|
||||
return true;
|
||||
|
||||
@@ -10,17 +10,15 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
|
||||
public int? Year { get; set; }
|
||||
public string Genre { get; set; }
|
||||
|
||||
public override bool RssSearch
|
||||
{
|
||||
get
|
||||
{
|
||||
if (SearchTerm.IsNullOrWhiteSpace() && Author.IsNullOrWhiteSpace() && Title.IsNullOrWhiteSpace())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
public override bool IsRssSearch =>
|
||||
SearchTerm.IsNullOrWhiteSpace() &&
|
||||
!IsIdSearch;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
public override bool IsIdSearch =>
|
||||
Author.IsNotNullOrWhiteSpace() ||
|
||||
Title.IsNotNullOrWhiteSpace() ||
|
||||
Publisher.IsNotNullOrWhiteSpace() ||
|
||||
Genre.IsNotNullOrWhiteSpace() ||
|
||||
Year.HasValue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,18 +13,17 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
|
||||
public int? Year { get; set; }
|
||||
public string Genre { get; set; }
|
||||
|
||||
public override bool RssSearch
|
||||
{
|
||||
get
|
||||
{
|
||||
if (SearchTerm.IsNullOrWhiteSpace() && ImdbId.IsNullOrWhiteSpace() && !TmdbId.HasValue && !TraktId.HasValue)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
public override bool IsRssSearch =>
|
||||
SearchTerm.IsNullOrWhiteSpace() &&
|
||||
!IsIdSearch;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
public override bool IsIdSearch =>
|
||||
ImdbId.IsNotNullOrWhiteSpace() ||
|
||||
Genre.IsNotNullOrWhiteSpace() ||
|
||||
TmdbId.HasValue ||
|
||||
TraktId.HasValue ||
|
||||
DoubanId.HasValue ||
|
||||
Year.HasValue;
|
||||
|
||||
public string FullImdbId => ParseUtil.GetFullImdbId(ImdbId);
|
||||
|
||||
|
||||
@@ -11,17 +11,16 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
|
||||
public string Track { get; set; }
|
||||
public int? Year { get; set; }
|
||||
|
||||
public override bool RssSearch
|
||||
{
|
||||
get
|
||||
{
|
||||
if (SearchTerm.IsNullOrWhiteSpace() && Album.IsNullOrWhiteSpace() && Artist.IsNullOrWhiteSpace() && Label.IsNullOrWhiteSpace())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
public override bool IsRssSearch =>
|
||||
SearchTerm.IsNullOrWhiteSpace() &&
|
||||
!IsIdSearch;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
public override bool IsIdSearch =>
|
||||
Album.IsNotNullOrWhiteSpace() ||
|
||||
Artist.IsNotNullOrWhiteSpace() ||
|
||||
Label.IsNotNullOrWhiteSpace() ||
|
||||
Genre.IsNotNullOrWhiteSpace() ||
|
||||
Track.IsNotNullOrWhiteSpace() ||
|
||||
Year.HasValue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,72 +7,39 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
|
||||
{
|
||||
public abstract class SearchCriteriaBase
|
||||
{
|
||||
private static readonly Regex SpecialCharacter = new Regex(@"[`'.]", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
private static readonly Regex NonWord = new Regex(@"[\W]", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
private static readonly Regex BeginningThe = new Regex(@"^the\s", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
private static readonly Regex StandardizeDashesRegex = new (@"\p{Pd}+", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
private static readonly Regex StandardizeSingleQuotesRegex = new (@"[\u0060\u00B4\u2018\u2019]", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
public virtual bool InteractiveSearch { get; set; }
|
||||
public List<int> IndexerIds { get; set; }
|
||||
public string SearchTerm { get; set; }
|
||||
public int[] Categories { get; set; }
|
||||
public string SearchType { get; set; }
|
||||
public int? Limit { get; set; }
|
||||
public int? Offset { get; set; }
|
||||
public int Limit { get; set; }
|
||||
public int Offset { get; set; }
|
||||
public string Source { get; set; }
|
||||
public string Host { get; set; }
|
||||
|
||||
public virtual string SearchQuery
|
||||
public override string ToString() => $"{SearchQuery}, Offset: {Offset}, Limit: {Limit}, Categories: [{string.Join(", ", Categories)}]";
|
||||
|
||||
public virtual string SearchQuery => $"Term: [{SearchTerm}]";
|
||||
|
||||
public virtual bool IsRssSearch => SearchTerm.IsNullOrWhiteSpace();
|
||||
|
||||
public virtual bool IsIdSearch => false;
|
||||
|
||||
public string SanitizedSearchTerm => GetSanitizedTerm(SearchTerm);
|
||||
|
||||
private static string GetSanitizedTerm(string term)
|
||||
{
|
||||
get
|
||||
{
|
||||
return $"Term: [{SearchTerm}]";
|
||||
}
|
||||
}
|
||||
term ??= "";
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{SearchQuery}, Offset: {Offset ?? 0}, Limit: {Limit ?? 0}, Categories: [{string.Join(", ", Categories)}]";
|
||||
}
|
||||
term = StandardizeDashesRegex.Replace(term, "-");
|
||||
term = StandardizeSingleQuotesRegex.Replace(term, "'");
|
||||
|
||||
public virtual bool RssSearch
|
||||
{
|
||||
get
|
||||
{
|
||||
if (SearchTerm.IsNullOrWhiteSpace())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
var safeTitle = term.Where(c => char.IsLetterOrDigit(c) || char.IsWhiteSpace(c) || c is '-' or '.' or '_' or '(' or ')' or '@' or '/' or '\'' or '[' or ']' or '+' or '%');
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public string SanitizedSearchTerm
|
||||
{
|
||||
get
|
||||
{
|
||||
var term = SearchTerm;
|
||||
if (SearchTerm == null)
|
||||
{
|
||||
term = "";
|
||||
}
|
||||
|
||||
var safeTitle = term.Where(c => (char.IsLetterOrDigit(c)
|
||||
|| char.IsWhiteSpace(c)
|
||||
|| c == '-'
|
||||
|| c == '.'
|
||||
|| c == '_'
|
||||
|| c == '('
|
||||
|| c == ')'
|
||||
|| c == '@'
|
||||
|| c == '/'
|
||||
|| c == '\''
|
||||
|| c == '['
|
||||
|| c == ']'
|
||||
|| c == '+'
|
||||
|| c == '%'));
|
||||
return string.Concat(safeTitle);
|
||||
}
|
||||
return string.Concat(safeTitle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,23 +21,25 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
|
||||
public int? Year { get; set; }
|
||||
public string Genre { get; set; }
|
||||
|
||||
public string SanitizedTvSearchString => (SanitizedSearchTerm + " " + EpisodeSearchString).Trim();
|
||||
public string SanitizedTvSearchString => $"{SanitizedSearchTerm} {EpisodeSearchString}".Trim();
|
||||
public string EpisodeSearchString => GetEpisodeSearchString();
|
||||
|
||||
public string FullImdbId => ParseUtil.GetFullImdbId(ImdbId);
|
||||
|
||||
public override bool RssSearch
|
||||
{
|
||||
get
|
||||
{
|
||||
if (SearchTerm.IsNullOrWhiteSpace() && ImdbId.IsNullOrWhiteSpace() && !TvdbId.HasValue && !RId.HasValue && !TraktId.HasValue && !TvMazeId.HasValue)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
public override bool IsRssSearch =>
|
||||
SearchTerm.IsNullOrWhiteSpace() &&
|
||||
!IsIdSearch;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
public override bool IsIdSearch =>
|
||||
Episode.IsNotNullOrWhiteSpace() ||
|
||||
ImdbId.IsNotNullOrWhiteSpace() ||
|
||||
Season.HasValue ||
|
||||
TvdbId.HasValue ||
|
||||
RId.HasValue ||
|
||||
TraktId.HasValue ||
|
||||
TvMazeId.HasValue ||
|
||||
TmdbId.HasValue ||
|
||||
DoubanId.HasValue;
|
||||
|
||||
public override string SearchQuery
|
||||
{
|
||||
|
||||
@@ -138,8 +138,8 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
|
||||
spec.SearchTerm = query.q;
|
||||
spec.SearchType = query.t;
|
||||
spec.Limit = query.limit;
|
||||
spec.Offset = query.offset;
|
||||
spec.Limit = query.limit ?? 100;
|
||||
spec.Offset = query.offset ?? 0;
|
||||
spec.Source = query.source;
|
||||
spec.Host = query.host;
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ namespace NzbDrone.Core.IndexerStats
|
||||
var elapsedTimeEvents = sortedEvents.Where(h => int.TryParse(h.Data.GetValueOrDefault("elapsedTime"), out temp))
|
||||
.Select(h => temp);
|
||||
|
||||
indexerStats.AverageResponseTime = elapsedTimeEvents.Count() > 0 ? (int)elapsedTimeEvents.Average() : 0;
|
||||
indexerStats.AverageResponseTime = elapsedTimeEvents.Any() ? (int)elapsedTimeEvents.Average() : 0;
|
||||
|
||||
foreach (var historyEvent in sortedEvents)
|
||||
{
|
||||
|
||||
@@ -29,13 +29,14 @@ namespace NzbDrone.Core.IndexerVersions
|
||||
/* Update Service will fall back if version # does not exist for an indexer per Ta */
|
||||
|
||||
private const string DEFINITION_BRANCH = "master";
|
||||
private const int DEFINITION_VERSION = 8;
|
||||
private const int DEFINITION_VERSION = 9;
|
||||
|
||||
//Used when moving yml to C#
|
||||
private readonly List<string> _defintionBlocklist = new List<string>()
|
||||
// Used when moving yml to C#
|
||||
private readonly List<string> _definitionBlocklist = new ()
|
||||
{
|
||||
"aither",
|
||||
"animeworld",
|
||||
"audiobookbay",
|
||||
"beyond-hd-oneurl",
|
||||
"beyond-hd",
|
||||
"blutopia",
|
||||
@@ -89,7 +90,7 @@ namespace NzbDrone.Core.IndexerVersions
|
||||
{
|
||||
var request = new HttpRequest($"https://indexers.prowlarr.com/{DEFINITION_BRANCH}/{DEFINITION_VERSION}");
|
||||
var response = _httpClient.Get<List<CardigannMetaDefinition>>(request);
|
||||
indexerList = response.Resource.Where(i => !_defintionBlocklist.Contains(i.File)).ToList();
|
||||
indexerList = response.Resource.Where(i => !_definitionBlocklist.Contains(i.File)).ToList();
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -125,7 +126,7 @@ namespace NzbDrone.Core.IndexerVersions
|
||||
|
||||
public List<string> GetBlocklist()
|
||||
{
|
||||
return _defintionBlocklist;
|
||||
return _definitionBlocklist;
|
||||
}
|
||||
|
||||
private List<CardigannMetaDefinition> ReadDefinitionsFromDisk(List<CardigannMetaDefinition> defs, string path, SearchOption options = SearchOption.TopDirectoryOnly)
|
||||
@@ -227,10 +228,10 @@ namespace NzbDrone.Core.IndexerVersions
|
||||
if (definition.Settings == null)
|
||||
{
|
||||
definition.Settings = new List<SettingsField>
|
||||
{
|
||||
new SettingsField { Name = "username", Label = "Username", Type = "text" },
|
||||
new SettingsField { Name = "password", Label = "Password", Type = "password" }
|
||||
};
|
||||
{
|
||||
new () { Name = "username", Label = "Username", Type = "text" },
|
||||
new () { Name = "password", Label = "Password", Type = "password" }
|
||||
};
|
||||
}
|
||||
|
||||
if (definition.Encoding == null)
|
||||
|
||||
@@ -100,45 +100,29 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
_capabilities = capabilities;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
{
|
||||
return new IndexerPageableRequestChain();
|
||||
return new List<IndexerRequest>();
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories);
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories);
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories);
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories);
|
||||
}
|
||||
|
||||
private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories)
|
||||
|
||||
@@ -9,10 +9,8 @@ using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using AngleSharp.Html.Parser;
|
||||
using FluentValidation;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Indexers.Exceptions;
|
||||
using NzbDrone.Core.Indexers.Settings;
|
||||
@@ -20,14 +18,13 @@ using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
public class Anidub : TorrentIndexerBase<UserPassTorrentBaseSettings>
|
||||
{
|
||||
public override string Name => "Anidub";
|
||||
public override string[] IndexerUrls => new string[] { "https://tr.anidub.com/" };
|
||||
public override string[] IndexerUrls => new[] { "https://tr.anidub.com/" };
|
||||
public override string Description => "Anidub is russian anime voiceover group and eponymous anime tracker.";
|
||||
public override string Language => "ru-RU";
|
||||
public override Encoding Encoding => Encoding.UTF8;
|
||||
@@ -42,31 +39,29 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
public override IIndexerRequestGenerator GetRequestGenerator()
|
||||
{
|
||||
return new AnidubRequestGenerator() { Settings = Settings, Capabilities = Capabilities };
|
||||
return new AnidubRequestGenerator(Settings);
|
||||
}
|
||||
|
||||
public override IParseIndexerResponse GetParser()
|
||||
{
|
||||
return new AnidubParser(Settings, Capabilities.Categories) { HttpClient = _httpClient, Logger = _logger };
|
||||
return new AnidubParser(Settings, Capabilities.Categories, RateLimit, _httpClient, _logger);
|
||||
}
|
||||
|
||||
protected override async Task DoLogin()
|
||||
{
|
||||
UpdateCookies(null, null);
|
||||
|
||||
var mainPage = await ExecuteAuth(new HttpRequest(Settings.BaseUrl));
|
||||
|
||||
var requestBuilder = new HttpRequestBuilder(Settings.BaseUrl + "index.php")
|
||||
{
|
||||
LogResponseContent = true,
|
||||
AllowAutoRedirect = true
|
||||
AllowAutoRedirect = true,
|
||||
Method = HttpMethod.Post
|
||||
};
|
||||
|
||||
var mainPage = await ExecuteAuth(new HttpRequest(Settings.BaseUrl));
|
||||
|
||||
requestBuilder.Method = HttpMethod.Post;
|
||||
requestBuilder.PostProcess += r => r.RequestTimeout = TimeSpan.FromSeconds(15);
|
||||
requestBuilder.SetCookies(mainPage.GetCookies());
|
||||
|
||||
var authLoginRequest = requestBuilder
|
||||
.SetCookies(mainPage.GetCookies())
|
||||
.AddFormParameter("login_name", Settings.Username)
|
||||
.AddFormParameter("login_password", Settings.Password)
|
||||
.AddFormParameter("login", "submit")
|
||||
@@ -77,27 +72,22 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
if (response.Content != null && !CheckIfLoginNeeded(response))
|
||||
{
|
||||
UpdateCookies(response.GetCookies(), DateTime.Now + TimeSpan.FromDays(30));
|
||||
UpdateCookies(response.GetCookies(), DateTime.Now.AddDays(30));
|
||||
_logger.Debug("Anidub authentication succeeded");
|
||||
}
|
||||
else
|
||||
{
|
||||
const string ErrorSelector = "#content .berror .berror_c";
|
||||
var parser = new HtmlParser();
|
||||
var document = await parser.ParseDocumentAsync(response.Content);
|
||||
var errorMessage = document.QuerySelector(ErrorSelector).TextContent.Trim();
|
||||
throw new IndexerAuthException("Anidub authentication failed. Error: " + errorMessage);
|
||||
var errorMessage = document.QuerySelector("#content .berror .berror_c")?.TextContent.Trim();
|
||||
|
||||
throw new IndexerAuthException(errorMessage ?? "Unknown error message, please report.");
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
|
||||
{
|
||||
if (httpResponse.Content.Contains("index.php?action=logout"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return !httpResponse.Content.Contains("index.php?action=logout");
|
||||
}
|
||||
|
||||
private IndexerCapabilities SetCapabilities()
|
||||
@@ -138,31 +128,32 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
caps.Categories.AddCategoryMapping(15, NewznabStandardCategory.BooksComics, "Манга");
|
||||
caps.Categories.AddCategoryMapping(16, NewznabStandardCategory.Audio, "OST");
|
||||
caps.Categories.AddCategoryMapping(17, NewznabStandardCategory.Audio, "Подкасты");
|
||||
|
||||
return caps;
|
||||
}
|
||||
}
|
||||
|
||||
public class AnidubRequestGenerator : IIndexerRequestGenerator
|
||||
{
|
||||
public UserPassTorrentBaseSettings Settings { get; set; }
|
||||
public IndexerCapabilities Capabilities { get; set; }
|
||||
private readonly UserPassTorrentBaseSettings _settings;
|
||||
|
||||
public AnidubRequestGenerator()
|
||||
public AnidubRequestGenerator(UserPassTorrentBaseSettings settings)
|
||||
{
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories)
|
||||
private IEnumerable<IndexerRequest> GetPagedRequests(string term)
|
||||
{
|
||||
var requestUrl = string.Empty;
|
||||
string requestUrl;
|
||||
var isSearch = !string.IsNullOrWhiteSpace(term);
|
||||
|
||||
if (isSearch)
|
||||
{
|
||||
requestUrl = string.Format("{0}/index.php?do=search", Settings.BaseUrl.TrimEnd('/'));
|
||||
requestUrl = $"{_settings.BaseUrl.TrimEnd('/')}/index.php?do=search";
|
||||
}
|
||||
else
|
||||
{
|
||||
requestUrl = Settings.BaseUrl;
|
||||
requestUrl = _settings.BaseUrl;
|
||||
}
|
||||
|
||||
var request = new IndexerRequest(requestUrl, HttpAccept.Html);
|
||||
@@ -203,49 +194,29 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
yield return request;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}");
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedTvSearchString), searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests($"{searchCriteria.SanitizedTvSearchString}");
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}");
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}");
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}");
|
||||
}
|
||||
|
||||
public Func<IDictionary<string, string>> GetCookies { get; set; }
|
||||
@@ -256,33 +227,37 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
private readonly UserPassTorrentBaseSettings _settings;
|
||||
private readonly IndexerCapabilitiesCategories _categories;
|
||||
public IIndexerHttpClient HttpClient { get; set; }
|
||||
public Logger Logger { get; set; }
|
||||
private readonly TimeSpan _rateLimit;
|
||||
private readonly IIndexerHttpClient _httpClient;
|
||||
private readonly Logger _logger;
|
||||
|
||||
private static Dictionary<string, string> CategoriesMap => new Dictionary<string, string>
|
||||
{
|
||||
{ "/anime_tv/full", "14" },
|
||||
{ "/anime_tv/anime_ongoing", "10" },
|
||||
{ "/anime_tv/shonen", "11" },
|
||||
{ "/anime_tv", "2" },
|
||||
{ "/xxx", "13" },
|
||||
{ "/manga", "15" },
|
||||
{ "/ost", "16" },
|
||||
{ "/podcast", "17" },
|
||||
{ "/anime_movie", "3" },
|
||||
{ "/anime_ova/anime_ona", "5" },
|
||||
{ "/anime_ova", "4" },
|
||||
{ "/dorama/japan_dorama", "6" },
|
||||
{ "/dorama/korea_dorama", "7" },
|
||||
{ "/dorama/china_dorama", "8" },
|
||||
{ "/dorama", "9" },
|
||||
{ "/anons_ongoing", "12" }
|
||||
};
|
||||
private static Dictionary<string, string> CategoriesMap => new ()
|
||||
{
|
||||
{ "/anime_tv/full", "14" },
|
||||
{ "/anime_tv/anime_ongoing", "10" },
|
||||
{ "/anime_tv/shonen", "11" },
|
||||
{ "/anime_tv", "2" },
|
||||
{ "/xxx", "13" },
|
||||
{ "/manga", "15" },
|
||||
{ "/ost", "16" },
|
||||
{ "/podcast", "17" },
|
||||
{ "/anime_movie", "3" },
|
||||
{ "/anime_ova/anime_ona", "5" },
|
||||
{ "/anime_ova", "4" },
|
||||
{ "/dorama/japan_dorama", "6" },
|
||||
{ "/dorama/korea_dorama", "7" },
|
||||
{ "/dorama/china_dorama", "8" },
|
||||
{ "/dorama", "9" },
|
||||
{ "/anons_ongoing", "12" }
|
||||
};
|
||||
|
||||
public AnidubParser(UserPassTorrentBaseSettings settings, IndexerCapabilitiesCategories categories)
|
||||
public AnidubParser(UserPassTorrentBaseSettings settings, IndexerCapabilitiesCategories categories, TimeSpan rateLimit, IIndexerHttpClient httpClient, Logger logger)
|
||||
{
|
||||
_settings = settings;
|
||||
_categories = categories;
|
||||
_rateLimit = rateLimit;
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
private static string GetTitle(AngleSharp.Html.Dom.IHtmlDocument content, AngleSharp.Dom.IElement tabNode)
|
||||
@@ -327,9 +302,9 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
private static int GetReleaseLeechers(AngleSharp.Dom.IElement tabNode)
|
||||
{
|
||||
const string LeechersSelector = ".list.down > .li_swing_m";
|
||||
const string leechersSelector = ".list.down > .li_swing_m";
|
||||
|
||||
var leechersStr = tabNode.QuerySelector(LeechersSelector).TextContent;
|
||||
var leechersStr = tabNode.QuerySelector(leechersSelector).TextContent;
|
||||
int.TryParse(leechersStr, out var leechers);
|
||||
return leechers;
|
||||
}
|
||||
@@ -345,18 +320,18 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
private static int GetReleaseGrabs(AngleSharp.Dom.IElement tabNode)
|
||||
{
|
||||
const string GrabsSelector = ".list.down > .li_download_m";
|
||||
const string grabsSelector = ".list.down > .li_download_m";
|
||||
|
||||
var grabsStr = tabNode.QuerySelector(GrabsSelector).TextContent;
|
||||
var grabsStr = tabNode.QuerySelector(grabsSelector).TextContent;
|
||||
int.TryParse(grabsStr, out var grabs);
|
||||
return grabs;
|
||||
}
|
||||
|
||||
private static string GetDateFromDocument(AngleSharp.Html.Dom.IHtmlDocument content)
|
||||
{
|
||||
const string DateSelector = ".story_inf > li:nth-child(2)";
|
||||
const string dateSelector = ".story_inf > li:nth-child(2)";
|
||||
|
||||
var domDate = content.QuerySelector(DateSelector).LastChild;
|
||||
var domDate = content.QuerySelector(dateSelector).LastChild;
|
||||
|
||||
if (domDate?.NodeName != "#text")
|
||||
{
|
||||
@@ -397,16 +372,16 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
return utcDate.AddHours(-russianStandardTimeDiff);
|
||||
}
|
||||
|
||||
Logger.Warn($"[AniDub] Date time couldn't be parsed on. Date text: {dateText}");
|
||||
_logger.Warn($"[AniDub] Date time couldn't be parsed on. Date text: {dateText}");
|
||||
|
||||
return DateTime.UtcNow;
|
||||
}
|
||||
|
||||
private static long GetReleaseSize(AngleSharp.Dom.IElement tabNode)
|
||||
{
|
||||
const string SizeSelector = ".list.down > .red";
|
||||
const string sizeSelector = ".list.down > .red";
|
||||
|
||||
var sizeStr = tabNode.QuerySelector(SizeSelector).TextContent;
|
||||
var sizeStr = tabNode.QuerySelector(sizeSelector).TextContent;
|
||||
return ParseUtil.GetBytes(sizeStr);
|
||||
}
|
||||
|
||||
@@ -446,11 +421,11 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
var release = new TorrentInfo
|
||||
{
|
||||
Title = GetTitle(dom, t),
|
||||
InfoUrl = indexerResponse.Request.Url.ToString(),
|
||||
InfoUrl = indexerResponse.Request.Url.FullUri,
|
||||
DownloadVolumeFactor = 0,
|
||||
UploadVolumeFactor = 1,
|
||||
|
||||
Guid = indexerResponse.Request.Url.ToString() + t.Id,
|
||||
Guid = indexerResponse.Request.Url.FullUri + t.Id,
|
||||
Seeders = GetReleaseSeeders(t),
|
||||
Peers = GetReleaseSeeders(t) + GetReleaseLeechers(t),
|
||||
Grabs = GetReleaseGrabs(t),
|
||||
@@ -472,36 +447,30 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
var domQuery = string.Empty;
|
||||
|
||||
if (indexerResponse.Request.Url.Query.Contains("do=search"))
|
||||
{
|
||||
domQuery = ".searchitem > h3 > a";
|
||||
}
|
||||
else
|
||||
{
|
||||
domQuery = "#dle-content > .story > .story_h > .lcol > h2 > a";
|
||||
}
|
||||
|
||||
var links = dom.QuerySelectorAll(domQuery);
|
||||
var links = dom.QuerySelectorAll(".searchitem > h3 > a[href], #dle-content > .story > .story_h > .lcol > h2 > a[href]");
|
||||
foreach (var link in links)
|
||||
{
|
||||
var url = link.GetAttribute("href");
|
||||
|
||||
var releaseRequest = new IndexerRequest(url, HttpAccept.Html);
|
||||
var releaseResponse = new IndexerResponse(releaseRequest, HttpClient.Execute(releaseRequest.HttpRequest));
|
||||
var releaseRequest = new HttpRequestBuilder(url)
|
||||
.WithRateLimit(_rateLimit.TotalSeconds)
|
||||
.SetHeader("Referer", _settings.BaseUrl)
|
||||
.Accept(HttpAccept.Html)
|
||||
.Build();
|
||||
|
||||
var releaseIndexerRequest = new IndexerRequest(releaseRequest);
|
||||
var releaseResponse = new IndexerResponse(releaseIndexerRequest, _httpClient.Execute(releaseIndexerRequest.HttpRequest));
|
||||
|
||||
// Throw common http errors here before we try to parse
|
||||
if (releaseResponse.HttpResponse.HasHttpError)
|
||||
{
|
||||
if (releaseResponse.HttpResponse.StatusCode == HttpStatusCode.TooManyRequests)
|
||||
{
|
||||
throw new TooManyRequestsException(releaseRequest.HttpRequest, releaseResponse.HttpResponse);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new IndexerException(releaseResponse, "Http error code: " + releaseResponse.HttpResponse.StatusCode);
|
||||
throw new TooManyRequestsException(releaseResponse.HttpRequest, releaseResponse.HttpResponse);
|
||||
}
|
||||
|
||||
throw new IndexerException(releaseResponse, $"HTTP Error - {releaseResponse.HttpResponse.StatusCode}. {url}");
|
||||
}
|
||||
|
||||
torrentInfos.AddRange(ParseRelease(releaseResponse));
|
||||
|
||||
@@ -107,34 +107,30 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
=> GetRequestWithSearchType(searchCriteria, "anime");
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
=> GetRequestWithSearchType(searchCriteria, "music");
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
=> GetRequestWithSearchType(searchCriteria, "anime");
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
=> GetRequestWithSearchType(searchCriteria, "anime");
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
=> GetRequestWithSearchType(searchCriteria, "anime");
|
||||
|
||||
private IndexerPageableRequestChain GetRequestWithSearchType(SearchCriteriaBase searchCriteria, string searchType)
|
||||
private IEnumerable<IndexerRequest> GetRequestWithSearchType(SearchCriteriaBase searchCriteria, string searchType)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
// TODO: Remove this once Prowlarr has proper support for non Pageable Indexers and can tell Sonarr that indexer doesn't support pagination in a proper way, for now just return empty release list on all request containing an offset
|
||||
if (searchCriteria.Offset is > 0)
|
||||
{
|
||||
return pageableRequests;
|
||||
return new List<IndexerRequest>();
|
||||
}
|
||||
|
||||
pageableRequests.Add(GetRequest(searchType, searchCriteria.SanitizedSearchTerm, searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
return GetRequest(searchType, searchCriteria.SanitizedSearchTerm, searchCriteria.Categories);
|
||||
}
|
||||
|
||||
private IEnumerable<IndexerRequest> GetRequest(string searchType, string term, int[] categories)
|
||||
@@ -425,7 +421,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
}
|
||||
|
||||
var releaseGroup = releaseTags.LastOrDefault();
|
||||
if (releaseGroup != null && releaseGroup.Contains("(") && releaseGroup.Contains(")"))
|
||||
if (releaseGroup != null && releaseGroup.Contains('(') && releaseGroup.Contains(')'))
|
||||
{
|
||||
//// Skip raws if set
|
||||
//if (releaseGroup.ToLowerInvariant().StartsWith("raw") && !AllowRaws)
|
||||
|
||||
@@ -7,10 +7,8 @@ using System.Net.Http;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using AngleSharp.Html.Parser;
|
||||
using FluentValidation;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Indexers.Exceptions;
|
||||
using NzbDrone.Core.Indexers.Settings;
|
||||
@@ -18,7 +16,6 @@ using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
@@ -26,7 +23,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
public override string Name => "AnimeTorrents";
|
||||
|
||||
public override string[] IndexerUrls => new string[] { "https://animetorrents.me/" };
|
||||
public override string[] IndexerUrls => new[] { "https://animetorrents.me/" };
|
||||
public override string Description => "Definitive source for anime and manga";
|
||||
private string LoginUrl => Settings.BaseUrl + "login.php";
|
||||
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
|
||||
@@ -40,7 +37,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
public override IIndexerRequestGenerator GetRequestGenerator()
|
||||
{
|
||||
return new AnimeTorrentsRequestGenerator() { Settings = Settings, Capabilities = Capabilities };
|
||||
return new AnimeTorrentsRequestGenerator { Settings = Settings, Capabilities = Capabilities };
|
||||
}
|
||||
|
||||
public override IParseIndexerResponse GetParser()
|
||||
@@ -52,30 +49,29 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
UpdateCookies(null, null);
|
||||
|
||||
var loginPage = await ExecuteAuth(new HttpRequest(LoginUrl));
|
||||
|
||||
var requestBuilder = new HttpRequestBuilder(LoginUrl)
|
||||
{
|
||||
LogResponseContent = true,
|
||||
AllowAutoRedirect = true
|
||||
AllowAutoRedirect = true,
|
||||
Method = HttpMethod.Post
|
||||
};
|
||||
|
||||
var loginPage = await ExecuteAuth(new HttpRequest(LoginUrl));
|
||||
requestBuilder.Method = HttpMethod.Post;
|
||||
requestBuilder.PostProcess += r => r.RequestTimeout = TimeSpan.FromSeconds(15);
|
||||
requestBuilder.SetCookies(loginPage.GetCookies());
|
||||
|
||||
var authLoginRequest = requestBuilder
|
||||
.SetCookies(loginPage.GetCookies())
|
||||
.AddFormParameter("username", Settings.Username)
|
||||
.AddFormParameter("password", Settings.Password)
|
||||
.AddFormParameter("form", "login")
|
||||
.AddFormParameter("rememberme[]", "1")
|
||||
.SetHeader("Content-Type", "multipart/form-data")
|
||||
.SetHeader("Content-Type", "application/x-www-form-urlencoded")
|
||||
.Build();
|
||||
|
||||
var response = await ExecuteAuth(authLoginRequest);
|
||||
|
||||
if (response.Content != null && response.Content.Contains("logout.php"))
|
||||
{
|
||||
UpdateCookies(response.GetCookies(), DateTime.Now + TimeSpan.FromDays(30));
|
||||
UpdateCookies(response.GetCookies(), DateTime.Now.AddDays(30));
|
||||
|
||||
_logger.Debug("AnimeTorrents authentication succeeded");
|
||||
}
|
||||
@@ -87,12 +83,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
|
||||
{
|
||||
if (httpResponse.Content.Contains("Access Denied!") || httpResponse.Content.Contains("login.php"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return httpResponse.Content.Contains("Access Denied!") || httpResponse.Content.Contains("login.php");
|
||||
}
|
||||
|
||||
private IndexerCapabilities SetCapabilities()
|
||||
@@ -100,13 +91,13 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
var caps = new IndexerCapabilities
|
||||
{
|
||||
TvSearchParams = new List<TvSearchParam>
|
||||
{
|
||||
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
|
||||
},
|
||||
{
|
||||
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
|
||||
},
|
||||
MovieSearchParams = new List<MovieSearchParam>
|
||||
{
|
||||
MovieSearchParam.Q
|
||||
}
|
||||
{
|
||||
MovieSearchParam.Q
|
||||
}
|
||||
};
|
||||
|
||||
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.MoviesSD, "Anime Movie");
|
||||
@@ -138,10 +129,6 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
public UserPassTorrentBaseSettings Settings { get; set; }
|
||||
public IndexerCapabilities Capabilities { get; set; }
|
||||
|
||||
public AnimeTorrentsRequestGenerator()
|
||||
{
|
||||
}
|
||||
|
||||
private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories)
|
||||
{
|
||||
var searchString = term;
|
||||
@@ -177,49 +164,29 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
yield return request;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories);
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories);
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories);
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories);
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories);
|
||||
}
|
||||
|
||||
public Func<IDictionary<string, string>> GetCookies { get; set; }
|
||||
|
||||
@@ -5,10 +5,8 @@ using System.Net;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using AngleSharp.Html.Parser;
|
||||
using FluentValidation;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Indexers.Exceptions;
|
||||
using NzbDrone.Core.Indexers.Settings;
|
||||
@@ -16,14 +14,13 @@ using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
public class Animedia : TorrentIndexerBase<NoAuthTorrentBaseSettings>
|
||||
{
|
||||
public override string Name => "Animedia";
|
||||
public override string[] IndexerUrls => new string[] { "https://tt.animedia.tv/" };
|
||||
public override string[] IndexerUrls => new[] { "https://tt.animedia.tv/" };
|
||||
public override string Description => "Animedia is russian anime voiceover group and eponymous anime tracker.";
|
||||
public override string Language => "ru-RU";
|
||||
public override Encoding Encoding => Encoding.UTF8;
|
||||
@@ -38,12 +35,12 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
public override IIndexerRequestGenerator GetRequestGenerator()
|
||||
{
|
||||
return new AnimediaRequestGenerator() { Settings = Settings, Capabilities = Capabilities };
|
||||
return new AnimediaRequestGenerator(Settings);
|
||||
}
|
||||
|
||||
public override IParseIndexerResponse GetParser()
|
||||
{
|
||||
return new AnimediaParser(Settings, Capabilities.Categories) { HttpClient = _httpClient, Logger = _logger };
|
||||
return new AnimediaParser(Settings, Capabilities.Categories, RateLimit, _httpClient);
|
||||
}
|
||||
|
||||
private IndexerCapabilities SetCapabilities()
|
||||
@@ -51,38 +48,40 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
var caps = new IndexerCapabilities
|
||||
{
|
||||
TvSearchParams = new List<TvSearchParam>
|
||||
{
|
||||
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
|
||||
},
|
||||
{
|
||||
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
|
||||
},
|
||||
MovieSearchParams = new List<MovieSearchParam>
|
||||
{
|
||||
MovieSearchParam.Q
|
||||
}
|
||||
{
|
||||
MovieSearchParam.Q
|
||||
}
|
||||
};
|
||||
|
||||
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.TVAnime, "TV Anime");
|
||||
caps.Categories.AddCategoryMapping(2, NewznabStandardCategory.TVAnime, "OVA/ONA/Special");
|
||||
caps.Categories.AddCategoryMapping(3, NewznabStandardCategory.TV, "Dorama");
|
||||
caps.Categories.AddCategoryMapping(4, NewznabStandardCategory.Movies, "Movies");
|
||||
|
||||
return caps;
|
||||
}
|
||||
}
|
||||
|
||||
public class AnimediaRequestGenerator : IIndexerRequestGenerator
|
||||
{
|
||||
public NoAuthTorrentBaseSettings Settings { get; set; }
|
||||
public IndexerCapabilities Capabilities { get; set; }
|
||||
private readonly NoAuthTorrentBaseSettings _settings;
|
||||
|
||||
public AnimediaRequestGenerator()
|
||||
public AnimediaRequestGenerator(NoAuthTorrentBaseSettings settings)
|
||||
{
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories)
|
||||
private IEnumerable<IndexerRequest> GetPagedRequests(string term)
|
||||
{
|
||||
var requestUrl = string.Empty;
|
||||
string requestUrl;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(term))
|
||||
{
|
||||
requestUrl = Settings.BaseUrl;
|
||||
requestUrl = _settings.BaseUrl;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -94,50 +93,37 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{ "orderby_sort", "entry_date|desc" }
|
||||
};
|
||||
|
||||
requestUrl = string.Format("{0}/ajax/search_result/P0?{1}", Settings.BaseUrl.TrimEnd('/'), queryCollection.GetQueryString());
|
||||
requestUrl = $"{_settings.BaseUrl.TrimEnd('/')}/ajax/search_result/P0?{queryCollection.GetQueryString()}";
|
||||
}
|
||||
|
||||
var request = new IndexerRequest(requestUrl, HttpAccept.Html);
|
||||
yield return request;
|
||||
yield return new IndexerRequest(requestUrl, HttpAccept.Html);
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}");
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedTvSearchString), searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests($"{searchCriteria.SanitizedTvSearchString}");
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}");
|
||||
}
|
||||
|
||||
// Animedia doesn't support music, but this function required by interface
|
||||
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
{
|
||||
return new IndexerPageableRequestChain();
|
||||
return new List<IndexerRequest>();
|
||||
}
|
||||
|
||||
// Animedia doesn't support books, but this function required by interface
|
||||
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
{
|
||||
return new IndexerPageableRequestChain();
|
||||
return new List<IndexerRequest>();
|
||||
}
|
||||
|
||||
public Func<IDictionary<string, string>> GetCookies { get; set; }
|
||||
@@ -148,6 +134,9 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
private readonly NoAuthTorrentBaseSettings _settings;
|
||||
private readonly IndexerCapabilitiesCategories _categories;
|
||||
private readonly TimeSpan _rateLimit;
|
||||
private readonly IIndexerHttpClient _httpClient;
|
||||
|
||||
private static readonly Regex EpisodesInfoQueryRegex = new Regex(@"сери[ия] (\d+)(?:-(\d+))? из.*", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
private static readonly Regex ResolutionInfoQueryRegex = new Regex(@"качество (\d+)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
private static readonly Regex SizeInfoQueryRegex = new Regex(@"размер:(.*)\n", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
@@ -155,25 +144,25 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
private static readonly Regex CategorieMovieRegex = new Regex(@"Фильм", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
private static readonly Regex CategorieOVARegex = new Regex(@"ОВА|OVA|ОНА|ONA|Special", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
private static readonly Regex CategorieDoramaRegex = new Regex(@"Дорама", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
public IIndexerHttpClient HttpClient { get; set; }
|
||||
public Logger Logger { get; set; }
|
||||
|
||||
public AnimediaParser(NoAuthTorrentBaseSettings settings, IndexerCapabilitiesCategories categories)
|
||||
public AnimediaParser(NoAuthTorrentBaseSettings settings, IndexerCapabilitiesCategories categories, TimeSpan rateLimit, IIndexerHttpClient httpClient)
|
||||
{
|
||||
_settings = settings;
|
||||
_categories = categories;
|
||||
_rateLimit = rateLimit;
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
private string composeTitle(AngleSharp.Html.Dom.IHtmlDocument dom, AngleSharp.Dom.IElement t, AngleSharp.Dom.IElement tr)
|
||||
private string ComposeTitle(AngleSharp.Html.Dom.IHtmlDocument dom, AngleSharp.Dom.IElement t, AngleSharp.Dom.IElement tr)
|
||||
{
|
||||
var name_ru = dom.QuerySelector("div.media__post__header > h1").TextContent.Trim();
|
||||
var name_en = dom.QuerySelector("div.media__panel > div:nth-of-type(1) > div.col-l:nth-of-type(1) > div > span").TextContent.Trim();
|
||||
var name_orig = dom.QuerySelector("div.media__panel > div:nth-of-type(1) > div.col-l:nth-of-type(2) > div > span").TextContent.Trim();
|
||||
var nameRu = dom.QuerySelector("div.media__post__header > h1")?.TextContent.Trim() ?? string.Empty;
|
||||
var nameEn = dom.QuerySelector("div.media__panel > div:nth-of-type(1) > div.col-l:nth-of-type(1) > div > span")?.TextContent.Trim() ?? string.Empty;
|
||||
var nameOrig = dom.QuerySelector("div.media__panel > div:nth-of-type(1) > div.col-l:nth-of-type(2) > div > span")?.TextContent.Trim() ?? string.Empty;
|
||||
|
||||
var title = name_ru + " / " + name_en;
|
||||
if (name_en != name_orig)
|
||||
var title = nameRu + " / " + nameEn;
|
||||
if (nameEn != nameOrig)
|
||||
{
|
||||
title += " / " + name_orig;
|
||||
title += " / " + nameOrig;
|
||||
}
|
||||
|
||||
var tabName = t.TextContent;
|
||||
@@ -183,7 +172,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
tabName = "";
|
||||
}
|
||||
|
||||
var heading = tr.QuerySelector("h3.tracker_info_bold").TextContent;
|
||||
var heading = tr.QuerySelector("h3.tracker_info_bold")?.TextContent.Trim() ?? string.Empty;
|
||||
|
||||
// Parse episodes info from heading if episods info present
|
||||
var match = EpisodesInfoQueryRegex.Match(heading);
|
||||
@@ -192,40 +181,40 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
if (string.IsNullOrEmpty(match.Groups[2].Value))
|
||||
{
|
||||
heading += " E" + match.Groups[1].Value;
|
||||
heading += $" E{match.Groups[1].Value}";
|
||||
}
|
||||
else
|
||||
{
|
||||
heading += string.Format(" E{0}-{1}", match.Groups[1].Value, match.Groups[2].Value);
|
||||
heading += $" E{match.Groups[1].Value}-{match.Groups[2].Value}";
|
||||
}
|
||||
}
|
||||
|
||||
return title + " - " + heading + " [" + getResolution(tr) + "p]";
|
||||
return title + " - " + heading + " [" + GetResolution(tr) + "p]";
|
||||
}
|
||||
|
||||
private string getResolution(AngleSharp.Dom.IElement tr)
|
||||
private string GetResolution(AngleSharp.Dom.IElement tr)
|
||||
{
|
||||
var resolution = tr.QuerySelector("div.tracker_info_left").TextContent;
|
||||
var resolution = tr.QuerySelector("div.tracker_info_left")?.TextContent.Trim() ?? string.Empty;
|
||||
return ResolutionInfoQueryRegex.Match(resolution).Groups[1].Value;
|
||||
}
|
||||
|
||||
private long getReleaseSize(AngleSharp.Dom.IElement tr)
|
||||
private long GetReleaseSize(AngleSharp.Dom.IElement tr)
|
||||
{
|
||||
var sizeStr = tr.QuerySelector("div.tracker_info_left").TextContent;
|
||||
var sizeStr = tr.QuerySelector("div.tracker_info_left")?.TextContent.Trim() ?? string.Empty;
|
||||
return ParseUtil.GetBytes(SizeInfoQueryRegex.Match(sizeStr).Groups[1].Value.Trim());
|
||||
}
|
||||
|
||||
private DateTime getReleaseDate(AngleSharp.Dom.IElement tr)
|
||||
private DateTime GetReleaseDate(AngleSharp.Dom.IElement tr)
|
||||
{
|
||||
var sizeStr = tr.QuerySelector("div.tracker_info_left").TextContent;
|
||||
var sizeStr = tr.QuerySelector("div.tracker_info_left")?.TextContent.Trim() ?? string.Empty;
|
||||
return DateTime.Parse(ReleaseDateInfoQueryRegex.Match(sizeStr).Groups[1].Value.Trim());
|
||||
}
|
||||
|
||||
private ICollection<IndexerCategory> MapCategories(AngleSharp.Html.Dom.IHtmlDocument dom, AngleSharp.Dom.IElement t, AngleSharp.Dom.IElement tr)
|
||||
{
|
||||
var rName = t.TextContent;
|
||||
var rDesc = tr.QuerySelector("h3.tracker_info_bold").TextContent;
|
||||
var type = dom.QuerySelector("div.releases-date:contains('Тип:')").TextContent;
|
||||
var rDesc = tr.QuerySelector("h3.tracker_info_bold")?.TextContent.Trim() ?? string.Empty;
|
||||
var type = dom.QuerySelector("div.releases-date:contains('Тип:')")?.TextContent.Trim() ?? string.Empty;
|
||||
|
||||
// Check OVA first cause OVA looks like anime with OVA in release name or description
|
||||
if (CategorieOVARegex.IsMatch(rName) || CategorieOVARegex.IsMatch(rDesc))
|
||||
@@ -256,28 +245,28 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
foreach (var t in dom.QuerySelectorAll("ul.media__tabs__nav > li > a"))
|
||||
{
|
||||
var tr_id = t.Attributes["href"].Value;
|
||||
var tr = dom.QuerySelector("div" + tr_id);
|
||||
var trId = t.GetAttribute("href");
|
||||
var tr = dom.QuerySelector("div" + trId);
|
||||
var seeders = int.Parse(tr.QuerySelector("div.circle_green_text_top").TextContent);
|
||||
var url = indexerResponse.HttpRequest.Url.ToString();
|
||||
var url = indexerResponse.HttpRequest.Url.FullUri;
|
||||
|
||||
var release = new TorrentInfo
|
||||
{
|
||||
Title = composeTitle(dom, t, tr),
|
||||
Title = ComposeTitle(dom, t, tr),
|
||||
InfoUrl = url,
|
||||
DownloadVolumeFactor = 0,
|
||||
UploadVolumeFactor = 1,
|
||||
|
||||
Guid = url + tr_id,
|
||||
Guid = url + trId,
|
||||
Seeders = seeders,
|
||||
Peers = seeders + int.Parse(tr.QuerySelector("div.circle_red_text_top").TextContent),
|
||||
Grabs = int.Parse(tr.QuerySelector("div.circle_grey_text_top").TextContent),
|
||||
Categories = MapCategories(dom, t, tr),
|
||||
PublishDate = getReleaseDate(tr),
|
||||
DownloadUrl = tr.QuerySelector("div.download_tracker > a.btn__green").Attributes["href"].Value,
|
||||
MagnetUrl = tr.QuerySelector("div.download_tracker > a.btn__d-gray").Attributes["href"].Value,
|
||||
Size = getReleaseSize(tr),
|
||||
Resolution = getResolution(tr)
|
||||
PublishDate = GetReleaseDate(tr),
|
||||
DownloadUrl = tr.QuerySelector("div.download_tracker > a.btn__green").GetAttribute("href"),
|
||||
MagnetUrl = tr.QuerySelector("div.download_tracker > a.btn__d-gray").GetAttribute("href"),
|
||||
Size = GetReleaseSize(tr),
|
||||
Resolution = GetResolution(tr)
|
||||
};
|
||||
torrentInfos.Add(release);
|
||||
}
|
||||
@@ -291,6 +280,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
|
||||
var links = dom.QuerySelectorAll("a.ads-list__item__title");
|
||||
foreach (var link in links)
|
||||
{
|
||||
@@ -302,20 +292,24 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
url = "https:" + url;
|
||||
}
|
||||
|
||||
var releaseRequest = new IndexerRequest(url, HttpAccept.Html);
|
||||
var releaseResponse = new IndexerResponse(releaseRequest, HttpClient.Execute(releaseRequest.HttpRequest));
|
||||
var releaseRequest = new HttpRequestBuilder(url)
|
||||
.WithRateLimit(_rateLimit.TotalSeconds)
|
||||
.SetHeader("Referer", _settings.BaseUrl)
|
||||
.Accept(HttpAccept.Html)
|
||||
.Build();
|
||||
|
||||
var releaseIndexerRequest = new IndexerRequest(releaseRequest);
|
||||
var releaseResponse = new IndexerResponse(releaseIndexerRequest, _httpClient.Execute(releaseIndexerRequest.HttpRequest));
|
||||
|
||||
// Throw common http errors here before we try to parse
|
||||
if (releaseResponse.HttpResponse.HasHttpError)
|
||||
{
|
||||
if (releaseResponse.HttpResponse.StatusCode == HttpStatusCode.TooManyRequests)
|
||||
{
|
||||
throw new TooManyRequestsException(releaseRequest.HttpRequest, releaseResponse.HttpResponse);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new IndexerException(releaseResponse, "Http error code: " + releaseResponse.HttpResponse.StatusCode);
|
||||
throw new TooManyRequestsException(releaseResponse.HttpRequest, releaseResponse.HttpResponse);
|
||||
}
|
||||
|
||||
throw new IndexerException(releaseResponse, $"HTTP Error - {releaseResponse.HttpResponse.StatusCode}. {url}");
|
||||
}
|
||||
|
||||
torrentInfos.AddRange(ParseRelease(releaseResponse));
|
||||
|
||||
@@ -52,55 +52,42 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
var requestBuilder = new HttpRequestBuilder(LoginUrl)
|
||||
{
|
||||
LogResponseContent = true,
|
||||
AllowAutoRedirect = true
|
||||
AllowAutoRedirect = true,
|
||||
Method = HttpMethod.Post
|
||||
};
|
||||
|
||||
requestBuilder.Method = HttpMethod.Post;
|
||||
requestBuilder.PostProcess += r => r.RequestTimeout = TimeSpan.FromSeconds(15);
|
||||
|
||||
var cookies = Cookies;
|
||||
|
||||
Cookies = null;
|
||||
|
||||
var authLoginRequest = requestBuilder
|
||||
.AddFormParameter("username", Settings.Username)
|
||||
.AddFormParameter("password", Settings.Password)
|
||||
.AddFormParameter("keeplogged", "1")
|
||||
.AddFormParameter("login", "Log+In!")
|
||||
.SetHeader("Content-Type", "multipart/form-data")
|
||||
.SetHeader("Content-Type", "application/x-www-form-urlencoded")
|
||||
.SetHeader("Referer", LoginUrl)
|
||||
.Build();
|
||||
|
||||
var headers = new NameValueCollection
|
||||
{
|
||||
{ "Referer", LoginUrl }
|
||||
};
|
||||
|
||||
authLoginRequest.Headers.Add(headers);
|
||||
|
||||
var response = await ExecuteAuth(authLoginRequest);
|
||||
|
||||
if (CheckIfLoginNeeded(response))
|
||||
{
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(response.Content);
|
||||
var errorMessage = dom.QuerySelector("form#loginform").TextContent.Trim();
|
||||
var errorMessage = dom.QuerySelector("form#loginform")?.TextContent.Trim();
|
||||
|
||||
throw new IndexerAuthException(errorMessage);
|
||||
throw new IndexerAuthException(errorMessage ?? "Unknown error message, please report.");
|
||||
}
|
||||
|
||||
cookies = response.GetCookies();
|
||||
UpdateCookies(cookies, DateTime.Now + TimeSpan.FromDays(30));
|
||||
UpdateCookies(cookies, DateTime.Now.AddDays(30));
|
||||
|
||||
_logger.Debug("Anthelion authentication succeeded.");
|
||||
}
|
||||
|
||||
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
|
||||
{
|
||||
if (!httpResponse.Content.Contains("logout.php"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return !httpResponse.Content.Contains("logout.php");
|
||||
}
|
||||
|
||||
private IndexerCapabilities SetCapabilities()
|
||||
@@ -108,13 +95,13 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
var caps = new IndexerCapabilities
|
||||
{
|
||||
TvSearchParams = new List<TvSearchParam>
|
||||
{
|
||||
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
|
||||
},
|
||||
{
|
||||
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
|
||||
},
|
||||
MovieSearchParams = new List<MovieSearchParam>
|
||||
{
|
||||
MovieSearchParam.Q
|
||||
}
|
||||
{
|
||||
MovieSearchParam.Q
|
||||
}
|
||||
};
|
||||
|
||||
caps.Categories.AddCategoryMapping("1", NewznabStandardCategory.Movies, "Film/Feature");
|
||||
@@ -131,10 +118,6 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
public UserPassTorrentBaseSettings Settings { get; set; }
|
||||
public IndexerCapabilities Capabilities { get; set; }
|
||||
|
||||
public AnthelionRequestGenerator()
|
||||
{
|
||||
}
|
||||
|
||||
private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories, string imdbId = null)
|
||||
{
|
||||
var searchUrl = string.Format("{0}/torrents.php", Settings.BaseUrl.TrimEnd('/'));
|
||||
@@ -163,45 +146,29 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
yield return request;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories, searchCriteria.FullImdbId));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories, searchCriteria.FullImdbId);
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
return pageableRequests;
|
||||
return new List<IndexerRequest>();
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedTvSearchString), searchCriteria.Categories, searchCriteria.FullImdbId));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedTvSearchString), searchCriteria.Categories, searchCriteria.FullImdbId);
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
return pageableRequests;
|
||||
return new List<IndexerRequest>();
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories);
|
||||
}
|
||||
|
||||
public Func<IDictionary<string, string>> GetCookies { get; set; }
|
||||
|
||||
@@ -29,7 +29,7 @@ public class AroLol : GazelleBase<AroLolSettings>
|
||||
protected override HttpRequestBuilder AuthLoginRequestBuilder()
|
||||
{
|
||||
return base.AuthLoginRequestBuilder()
|
||||
.AddFormParameter("twofa", Settings.TwoFactorAuthCode.Trim());
|
||||
.AddFormParameter("twofa", Settings.TwoFactorAuthCode?.Trim() ?? "");
|
||||
}
|
||||
|
||||
protected override bool CheckForLoginError(HttpResponse response)
|
||||
|
||||
350
src/NzbDrone.Core/Indexers/Definitions/AudioBookBay.cs
Normal file
350
src/NzbDrone.Core/Indexers/Definitions/AudioBookBay.cs
Normal file
@@ -0,0 +1,350 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using AngleSharp.Html.Dom;
|
||||
using AngleSharp.Html.Parser;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Indexers.Settings;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions;
|
||||
|
||||
public class AudioBookBay : TorrentIndexerBase<NoAuthTorrentBaseSettings>
|
||||
{
|
||||
public override string Name => "AudioBook Bay";
|
||||
public override string[] IndexerUrls => new[]
|
||||
{
|
||||
"https://audiobookbay.li/",
|
||||
"https://audiobookbay.se/"
|
||||
};
|
||||
public override string[] LegacyUrls => new[]
|
||||
{
|
||||
"https://audiobookbay.la/",
|
||||
"http://audiobookbay.net/",
|
||||
"https://audiobookbay.unblockit.tv/",
|
||||
"http://audiobookbay.nl/",
|
||||
"http://audiobookbay.ws/",
|
||||
"https://audiobookbay.unblockit.how/",
|
||||
"https://audiobookbay.unblockit.cam/",
|
||||
"https://audiobookbay.unblockit.biz/",
|
||||
"https://audiobookbay.unblockit.day/",
|
||||
"https://audiobookbay.unblockit.llc/",
|
||||
"https://audiobookbay.unblockit.blue/",
|
||||
"https://audiobookbay.unblockit.name/",
|
||||
"http://audiobookbay.fi/",
|
||||
"http://audiobookbay.se/",
|
||||
"http://audiobookbayabb.com/",
|
||||
"https://audiobookbay.unblockit.ist/",
|
||||
"https://audiobookbay.unblockit.bet/",
|
||||
"https://audiobookbay.unblockit.cat/",
|
||||
"https://audiobookbay.unblockit.nz/",
|
||||
"https://audiobookbay.fi/",
|
||||
"https://audiobookbay.unblockit.page/",
|
||||
"https://audiobookbay.unblockit.pet/",
|
||||
"https://audiobookbay.unblockit.ink/",
|
||||
"https://audiobookbay.unblockit.bio/" // error 502
|
||||
};
|
||||
public override string Description => "AudioBook Bay (ABB) is a public Torrent Tracker for AUDIOBOOKS";
|
||||
public override string Language => "en-US";
|
||||
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
|
||||
public override IndexerPrivacy Privacy => IndexerPrivacy.Public;
|
||||
public override int PageSize => 15;
|
||||
public override IndexerCapabilities Capabilities => SetCapabilities();
|
||||
|
||||
public AudioBookBay(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
|
||||
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public override IIndexerRequestGenerator GetRequestGenerator()
|
||||
{
|
||||
return new AudioBookBayRequestGenerator(Settings, Capabilities);
|
||||
}
|
||||
|
||||
public override IParseIndexerResponse GetParser()
|
||||
{
|
||||
return new AudioBookBayParser(Settings, Capabilities.Categories);
|
||||
}
|
||||
|
||||
public override async Task<byte[]> Download(Uri link)
|
||||
{
|
||||
var request = new HttpRequestBuilder(link.ToString())
|
||||
.SetCookies(GetCookies() ?? new Dictionary<string, string>())
|
||||
.Accept(HttpAccept.Html)
|
||||
.Build();
|
||||
|
||||
var response = await _httpClient.ExecuteProxiedAsync(request, Definition);
|
||||
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(response.Content);
|
||||
|
||||
var hash = dom.QuerySelector("td:contains(\"Info Hash:\") ~ td")?.TextContent.Trim();
|
||||
if (hash == null)
|
||||
{
|
||||
throw new Exception($"Failed to fetch hash from {link}");
|
||||
}
|
||||
|
||||
var title = dom.QuerySelector("div.postTitle h1")?.TextContent.Trim();
|
||||
if (title == null)
|
||||
{
|
||||
throw new Exception($"Failed to fetch title from {link}");
|
||||
}
|
||||
|
||||
title = StringUtil.MakeValidFileName(title, '_', false);
|
||||
|
||||
var magnet = MagnetLinkBuilder.BuildPublicMagnetLink(hash, title);
|
||||
|
||||
return await base.Download(new Uri(magnet));
|
||||
}
|
||||
|
||||
private IndexerCapabilities SetCapabilities()
|
||||
{
|
||||
var caps = new IndexerCapabilities
|
||||
{
|
||||
BookSearchParams = new List<BookSearchParam>
|
||||
{
|
||||
BookSearchParam.Q
|
||||
}
|
||||
};
|
||||
|
||||
// Age
|
||||
caps.Categories.AddCategoryMapping("children", NewznabStandardCategory.AudioAudiobook, "Children");
|
||||
caps.Categories.AddCategoryMapping("teen-young-adult", NewznabStandardCategory.AudioAudiobook, "Teen & Young Adult");
|
||||
caps.Categories.AddCategoryMapping("adults", NewznabStandardCategory.AudioAudiobook, "Adults");
|
||||
|
||||
// Category
|
||||
caps.Categories.AddCategoryMapping("postapocalyptic", NewznabStandardCategory.AudioAudiobook, "(Post)apocalyptic");
|
||||
caps.Categories.AddCategoryMapping("action", NewznabStandardCategory.AudioAudiobook, "Action");
|
||||
caps.Categories.AddCategoryMapping("adventure", NewznabStandardCategory.AudioAudiobook, "Adventure");
|
||||
caps.Categories.AddCategoryMapping("art", NewznabStandardCategory.AudioAudiobook, "Art");
|
||||
caps.Categories.AddCategoryMapping("autobiography-biographies", NewznabStandardCategory.AudioAudiobook, "Autobiography & Biographies");
|
||||
caps.Categories.AddCategoryMapping("business", NewznabStandardCategory.AudioAudiobook, "Business");
|
||||
caps.Categories.AddCategoryMapping("computer", NewznabStandardCategory.AudioAudiobook, "Computer");
|
||||
caps.Categories.AddCategoryMapping("contemporary", NewznabStandardCategory.AudioAudiobook, "Contemporary");
|
||||
caps.Categories.AddCategoryMapping("crime", NewznabStandardCategory.AudioAudiobook, "Crime");
|
||||
caps.Categories.AddCategoryMapping("detective", NewznabStandardCategory.AudioAudiobook, "Detective");
|
||||
caps.Categories.AddCategoryMapping("doctor-who-sci-fi", NewznabStandardCategory.AudioAudiobook, "Doctor Who");
|
||||
caps.Categories.AddCategoryMapping("education", NewznabStandardCategory.AudioAudiobook, "Education");
|
||||
caps.Categories.AddCategoryMapping("fantasy", NewznabStandardCategory.AudioAudiobook, "Fantasy");
|
||||
caps.Categories.AddCategoryMapping("general-fiction", NewznabStandardCategory.AudioAudiobook, "General Fiction");
|
||||
caps.Categories.AddCategoryMapping("historical-fiction", NewznabStandardCategory.AudioAudiobook, "Historical Fiction");
|
||||
caps.Categories.AddCategoryMapping("history", NewznabStandardCategory.AudioAudiobook, "History");
|
||||
caps.Categories.AddCategoryMapping("horror", NewznabStandardCategory.AudioAudiobook, "Horror");
|
||||
caps.Categories.AddCategoryMapping("humor", NewznabStandardCategory.AudioAudiobook, "Humor");
|
||||
caps.Categories.AddCategoryMapping("lecture", NewznabStandardCategory.AudioAudiobook, "Lecture");
|
||||
caps.Categories.AddCategoryMapping("lgbt", NewznabStandardCategory.AudioAudiobook, "LGBT");
|
||||
caps.Categories.AddCategoryMapping("literature", NewznabStandardCategory.AudioAudiobook, "Literature");
|
||||
caps.Categories.AddCategoryMapping("litrpg", NewznabStandardCategory.AudioAudiobook, "LitRPG");
|
||||
caps.Categories.AddCategoryMapping("general-non-fiction", NewznabStandardCategory.AudioAudiobook, "Misc. Non-fiction");
|
||||
caps.Categories.AddCategoryMapping("mystery", NewznabStandardCategory.AudioAudiobook, "Mystery");
|
||||
caps.Categories.AddCategoryMapping("paranormal", NewznabStandardCategory.AudioAudiobook, "Paranormal");
|
||||
caps.Categories.AddCategoryMapping("plays-theater", NewznabStandardCategory.AudioAudiobook, "Plays & Theater");
|
||||
caps.Categories.AddCategoryMapping("poetry", NewznabStandardCategory.AudioAudiobook, "Poetry");
|
||||
caps.Categories.AddCategoryMapping("political", NewznabStandardCategory.AudioAudiobook, "Political");
|
||||
caps.Categories.AddCategoryMapping("radio-productions", NewznabStandardCategory.AudioAudiobook, "Radio Productions");
|
||||
caps.Categories.AddCategoryMapping("romance", NewznabStandardCategory.AudioAudiobook, "Romance");
|
||||
caps.Categories.AddCategoryMapping("sci-fi", NewznabStandardCategory.AudioAudiobook, "Sci-Fi");
|
||||
caps.Categories.AddCategoryMapping("science", NewznabStandardCategory.AudioAudiobook, "Science");
|
||||
caps.Categories.AddCategoryMapping("self-help", NewznabStandardCategory.AudioAudiobook, "Self-help");
|
||||
caps.Categories.AddCategoryMapping("spiritual", NewznabStandardCategory.AudioAudiobook, "Spiritual & Religious");
|
||||
caps.Categories.AddCategoryMapping("sports", NewznabStandardCategory.AudioAudiobook, "Sport & Recreation");
|
||||
caps.Categories.AddCategoryMapping("suspense", NewznabStandardCategory.AudioAudiobook, "Suspense");
|
||||
caps.Categories.AddCategoryMapping("thriller", NewznabStandardCategory.AudioAudiobook, "Thriller");
|
||||
caps.Categories.AddCategoryMapping("true-crime", NewznabStandardCategory.AudioAudiobook, "True Crime");
|
||||
caps.Categories.AddCategoryMapping("tutorial", NewznabStandardCategory.AudioAudiobook, "Tutorial");
|
||||
caps.Categories.AddCategoryMapping("westerns", NewznabStandardCategory.AudioAudiobook, "Westerns");
|
||||
caps.Categories.AddCategoryMapping("zombies", NewznabStandardCategory.AudioAudiobook, "Zombies");
|
||||
|
||||
// Category Modifiers
|
||||
caps.Categories.AddCategoryMapping("anthology", NewznabStandardCategory.AudioAudiobook, "Anthology");
|
||||
caps.Categories.AddCategoryMapping("bestsellers", NewznabStandardCategory.AudioAudiobook, "Bestsellers");
|
||||
caps.Categories.AddCategoryMapping("classic", NewznabStandardCategory.AudioAudiobook, "Classic");
|
||||
caps.Categories.AddCategoryMapping("documentary", NewznabStandardCategory.AudioAudiobook, "Documentary");
|
||||
caps.Categories.AddCategoryMapping("full-cast", NewznabStandardCategory.AudioAudiobook, "Full Cast");
|
||||
caps.Categories.AddCategoryMapping("libertarian", NewznabStandardCategory.AudioAudiobook, "Libertarian");
|
||||
caps.Categories.AddCategoryMapping("military", NewznabStandardCategory.AudioAudiobook, "Military");
|
||||
caps.Categories.AddCategoryMapping("novel", NewznabStandardCategory.AudioAudiobook, "Novel");
|
||||
caps.Categories.AddCategoryMapping("short-story", NewznabStandardCategory.AudioAudiobook, "Short Story");
|
||||
|
||||
return caps;
|
||||
}
|
||||
}
|
||||
|
||||
public class AudioBookBayRequestGenerator : IIndexerRequestGenerator
|
||||
{
|
||||
private readonly NoAuthTorrentBaseSettings _settings;
|
||||
private readonly IndexerCapabilities _capabilities;
|
||||
|
||||
public AudioBookBayRequestGenerator(NoAuthTorrentBaseSettings settings, IndexerCapabilities capabilities)
|
||||
{
|
||||
_settings = settings;
|
||||
_capabilities = capabilities;
|
||||
}
|
||||
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
{
|
||||
return new List<IndexerRequest>();
|
||||
}
|
||||
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
{
|
||||
return new List<IndexerRequest>();
|
||||
}
|
||||
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
{
|
||||
return new List<IndexerRequest>();
|
||||
}
|
||||
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
{
|
||||
return GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}");
|
||||
}
|
||||
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
{
|
||||
return GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}");
|
||||
}
|
||||
|
||||
private IEnumerable<IndexerRequest> GetPagedRequests(string term)
|
||||
{
|
||||
var searchUrl = _settings.BaseUrl;
|
||||
|
||||
var parameters = new NameValueCollection();
|
||||
|
||||
term = Regex.Replace(term, @"[\W]+", " ").Trim();
|
||||
|
||||
if (term.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
parameters.Set("s", term);
|
||||
parameters.Set("tt", "1");
|
||||
}
|
||||
|
||||
if (parameters.Count > 0)
|
||||
{
|
||||
searchUrl += $"?{parameters.GetQueryString()}";
|
||||
}
|
||||
|
||||
yield return new IndexerRequest(new UriBuilder(searchUrl) { Path = "/" }.Uri.AbsoluteUri, HttpAccept.Html);
|
||||
yield return new IndexerRequest(new UriBuilder(searchUrl) { Path = "/page/2/" }.Uri.AbsoluteUri, HttpAccept.Html);
|
||||
yield return new IndexerRequest(new UriBuilder(searchUrl) { Path = "/page/3/" }.Uri.AbsoluteUri, HttpAccept.Html);
|
||||
}
|
||||
|
||||
public Func<IDictionary<string, string>> GetCookies { get; set; }
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
}
|
||||
|
||||
public class AudioBookBayParser : IParseIndexerResponse
|
||||
{
|
||||
private readonly NoAuthTorrentBaseSettings _settings;
|
||||
private readonly IndexerCapabilitiesCategories _categories;
|
||||
|
||||
public AudioBookBayParser(NoAuthTorrentBaseSettings settings, IndexerCapabilitiesCategories categories)
|
||||
{
|
||||
_settings = settings;
|
||||
_categories = categories;
|
||||
}
|
||||
|
||||
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
||||
{
|
||||
var releaseInfos = new List<ReleaseInfo>();
|
||||
|
||||
var doc = ParseHtmlDocument(indexerResponse.Content);
|
||||
|
||||
var rows = doc.QuerySelectorAll("div.post:has(div[class=\"postTitle\"])");
|
||||
foreach (var row in rows)
|
||||
{
|
||||
var infoUrl = _settings.BaseUrl + row.QuerySelector("div.postTitle h2 a")?.GetAttribute("href")?.Trim().TrimStart('/');
|
||||
|
||||
var title = row.QuerySelector("div.postTitle")?.TextContent.Trim();
|
||||
|
||||
var infoString = row.QuerySelector("div.postContent")?.TextContent.Trim() ?? string.Empty;
|
||||
|
||||
var matchFormat = Regex.Match(infoString, @"Format: (.+) \/", RegexOptions.IgnoreCase);
|
||||
if (matchFormat.Groups[1].Success && matchFormat.Groups[1].Value.Length > 0 && matchFormat.Groups[1].Value != "?")
|
||||
{
|
||||
title += $" [{matchFormat.Groups[1].Value.Trim()}]";
|
||||
}
|
||||
|
||||
var matchBitrate = Regex.Match(infoString, @"Bitrate: (.+)File", RegexOptions.IgnoreCase);
|
||||
if (matchBitrate.Groups[1].Success && matchBitrate.Groups[1].Value.Length > 0 && matchBitrate.Groups[1].Value != "?")
|
||||
{
|
||||
title += $" [{matchBitrate.Groups[1].Value.Trim()}]";
|
||||
}
|
||||
|
||||
var matchSize = Regex.Match(infoString, @"File Size: (.+?)s?$", RegexOptions.IgnoreCase);
|
||||
var size = matchSize.Groups[1].Success ? ParseUtil.GetBytes(matchSize.Groups[1].Value) : 0;
|
||||
|
||||
var matchDateAdded = Regex.Match(infoString, @"Posted: (\d{1,2} \D{3} \d{4})", RegexOptions.IgnoreCase);
|
||||
var publishDate = matchDateAdded.Groups[1].Success && DateTime.TryParseExact(matchDateAdded.Groups[1].Value, "d MMM yyyy", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var parsedDate) ? parsedDate : DateTime.Now;
|
||||
|
||||
var postInfo = row.QuerySelector("div.postInfo")?.FirstChild?.TextContent.Trim().Replace("\xA0", ";") ?? string.Empty;
|
||||
var matchCategory = Regex.Match(postInfo, @"Category: (.+)$", RegexOptions.IgnoreCase);
|
||||
var category = matchCategory.Groups[1].Success ? matchCategory.Groups[1].Value.Split(';', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).ToList() : new List<string>();
|
||||
var categories = category.SelectMany(_categories.MapTrackerCatDescToNewznab).Distinct().ToList();
|
||||
|
||||
var release = new TorrentInfo
|
||||
{
|
||||
Guid = infoUrl,
|
||||
InfoUrl = infoUrl,
|
||||
DownloadUrl = infoUrl,
|
||||
Title = CleanTitle(title),
|
||||
Categories = categories,
|
||||
Size = size,
|
||||
Seeders = 1,
|
||||
Peers = 1,
|
||||
PublishDate = publishDate,
|
||||
DownloadVolumeFactor = 0,
|
||||
UploadVolumeFactor = 1
|
||||
};
|
||||
|
||||
var cover = row.QuerySelector("img[src]")?.GetAttribute("src")?.Trim();
|
||||
if (!string.IsNullOrEmpty(cover))
|
||||
{
|
||||
release.PosterUrl = cover.StartsWith("http") ? cover : _settings.BaseUrl + cover;
|
||||
}
|
||||
|
||||
releaseInfos.Add(release);
|
||||
}
|
||||
|
||||
return releaseInfos;
|
||||
}
|
||||
|
||||
private static IHtmlDocument ParseHtmlDocument(string response)
|
||||
{
|
||||
var parser = new HtmlParser();
|
||||
var doc = parser.ParseDocument(response);
|
||||
|
||||
var hidden = doc.QuerySelectorAll("div.post.re-ab");
|
||||
foreach (var element in hidden)
|
||||
{
|
||||
var body = doc.CreateElement("div");
|
||||
body.ClassList.Add("post");
|
||||
body.InnerHtml = Encoding.UTF8.GetString(Convert.FromBase64String(element.TextContent));
|
||||
element.Parent.ReplaceChild(body, element);
|
||||
}
|
||||
|
||||
return doc;
|
||||
}
|
||||
|
||||
private static string CleanTitle(string title)
|
||||
{
|
||||
title = Regex.Replace(title, @"[\u0000-\u0008\u000A-\u001F\u0100-\uFFFF]", string.Empty, RegexOptions.Compiled);
|
||||
title = Regex.Replace(title, @"\s+", " ", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
return title.Trim();
|
||||
}
|
||||
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
}
|
||||
@@ -66,12 +66,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
||||
|
||||
protected override bool CheckIfLoginNeeded(HttpResponse response)
|
||||
{
|
||||
if (response.StatusCode == HttpStatusCode.Unauthorized || response.StatusCode == HttpStatusCode.PreconditionFailed)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return response.StatusCode == HttpStatusCode.Unauthorized || response.StatusCode == HttpStatusCode.PreconditionFailed;
|
||||
}
|
||||
|
||||
protected override void ModifyRequest(IndexerRequest request)
|
||||
@@ -99,14 +94,14 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
||||
{
|
||||
_logger.Warn(ex, "Unable to connect to indexer");
|
||||
|
||||
return new ValidationFailure(string.Empty, "Unable to connect to indexer, check the log for more details");
|
||||
return new ValidationFailure(string.Empty, "Unable to connect to indexer, check the log above the ValidationFailure for more details. " + ex.Message);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warn(ex, "Unable to connect to indexer");
|
||||
|
||||
return new ValidationFailure(string.Empty, "Unable to connect to indexer, check the log for more details");
|
||||
return new ValidationFailure(string.Empty, "Unable to connect to indexer, check the log above the ValidationFailure for more details");
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -116,12 +111,10 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
||||
{
|
||||
var requestBuilder = new HttpRequestBuilder(LoginUrl)
|
||||
{
|
||||
LogResponseContent = true
|
||||
LogResponseContent = true,
|
||||
Method = HttpMethod.Post
|
||||
};
|
||||
|
||||
requestBuilder.Method = HttpMethod.Post;
|
||||
requestBuilder.PostProcess += r => r.RequestTimeout = TimeSpan.FromSeconds(15);
|
||||
|
||||
var authLoginRequest = requestBuilder
|
||||
.AddFormParameter("username", Settings.Username)
|
||||
.AddFormParameter("password", Settings.Password)
|
||||
|
||||
@@ -76,7 +76,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
||||
yield return request;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
{
|
||||
var parameters = GetBasicSearchParameters(searchCriteria.Categories, searchCriteria.Genre);
|
||||
|
||||
@@ -93,23 +93,19 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
||||
parameters.Add("search", GetSearchTerm(searchCriteria.SanitizedSearchTerm).Trim());
|
||||
}
|
||||
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
pageableRequests.Add(GetRequest(parameters));
|
||||
return pageableRequests;
|
||||
return GetRequest(parameters);
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
{
|
||||
var parameters = GetBasicSearchParameters(searchCriteria.Categories, null);
|
||||
|
||||
parameters.Add("search", GetSearchTerm(searchCriteria.SanitizedSearchTerm).Trim());
|
||||
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
pageableRequests.Add(GetRequest(parameters));
|
||||
return pageableRequests;
|
||||
return GetRequest(parameters);
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
{
|
||||
var parameters = GetBasicSearchParameters(searchCriteria.Categories, searchCriteria.Genre);
|
||||
|
||||
@@ -128,12 +124,10 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
||||
parameters.Add("search", GetSearchTerm(searchCriteria.SanitizedTvSearchString).Trim());
|
||||
}
|
||||
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
pageableRequests.Add(GetRequest(parameters));
|
||||
return pageableRequests;
|
||||
return GetRequest(parameters);
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
@@ -141,15 +135,13 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
||||
// hook to adjust the search term
|
||||
protected virtual string GetSearchTerm(string term) => term;
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
{
|
||||
var parameters = GetBasicSearchParameters(searchCriteria.Categories, null);
|
||||
|
||||
parameters.Add("search", GetSearchTerm(searchCriteria.SanitizedSearchTerm).Trim());
|
||||
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
pageableRequests.Add(GetRequest(parameters));
|
||||
return pageableRequests;
|
||||
return GetRequest(parameters);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,8 @@ using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using AngleSharp.Dom;
|
||||
using AngleSharp.Html.Parser;
|
||||
using FluentValidation;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Indexers.Exceptions;
|
||||
using NzbDrone.Core.Indexers.Settings;
|
||||
@@ -18,14 +16,13 @@ using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
public class BB : TorrentIndexerBase<UserPassTorrentBaseSettings>
|
||||
{
|
||||
public override string Name => "BB";
|
||||
public override string[] IndexerUrls => new string[] { StringUtil.FromBase64("aHR0cHM6Ly9iYWNvbmJpdHMub3JnLw==") };
|
||||
public override string[] IndexerUrls => new[] { StringUtil.FromBase64("aHR0cHM6Ly9iYWNvbmJpdHMub3JnLw==") };
|
||||
private string LoginUrl => Settings.BaseUrl + "login.php";
|
||||
public override string Description => "BB is a Private Torrent Tracker for 0DAY / GENERAL";
|
||||
public override string Language => "en-US";
|
||||
@@ -41,7 +38,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
public override IIndexerRequestGenerator GetRequestGenerator()
|
||||
{
|
||||
return new BBRequestGenerator() { Settings = Settings, Capabilities = Capabilities };
|
||||
return new BBRequestGenerator { Settings = Settings, Capabilities = Capabilities };
|
||||
}
|
||||
|
||||
public override IParseIndexerResponse GetParser()
|
||||
@@ -54,30 +51,22 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
var requestBuilder = new HttpRequestBuilder(LoginUrl)
|
||||
{
|
||||
LogResponseContent = true,
|
||||
AllowAutoRedirect = true
|
||||
AllowAutoRedirect = true,
|
||||
Method = HttpMethod.Post
|
||||
};
|
||||
|
||||
requestBuilder.Method = HttpMethod.Post;
|
||||
requestBuilder.PostProcess += r => r.RequestTimeout = TimeSpan.FromSeconds(15);
|
||||
|
||||
var cookies = Cookies;
|
||||
|
||||
Cookies = null;
|
||||
|
||||
var authLoginRequest = requestBuilder
|
||||
.AddFormParameter("username", Settings.Username)
|
||||
.AddFormParameter("password", Settings.Password)
|
||||
.AddFormParameter("keeplogged", "1")
|
||||
.AddFormParameter("login", "Log+In!")
|
||||
.SetHeader("Content-Type", "multipart/form-data")
|
||||
.SetHeader("Content-Type", "application/x-www-form-urlencoded")
|
||||
.SetHeader("Referer", LoginUrl)
|
||||
.Build();
|
||||
|
||||
var headers = new NameValueCollection
|
||||
{
|
||||
{ "Referer", LoginUrl }
|
||||
};
|
||||
|
||||
authLoginRequest.Headers.Add(headers);
|
||||
|
||||
var response = await ExecuteAuth(authLoginRequest);
|
||||
|
||||
if (CheckIfLoginNeeded(response))
|
||||
@@ -98,19 +87,14 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
}
|
||||
|
||||
cookies = response.GetCookies();
|
||||
UpdateCookies(cookies, DateTime.Now + TimeSpan.FromDays(30));
|
||||
UpdateCookies(cookies, DateTime.Now.AddDays(30));
|
||||
|
||||
_logger.Debug("BB authentication succeeded.");
|
||||
}
|
||||
|
||||
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
|
||||
{
|
||||
if (!httpResponse.Content.Contains("logout.php"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return !httpResponse.Content.Contains("logout.php");
|
||||
}
|
||||
|
||||
private IndexerCapabilities SetCapabilities()
|
||||
@@ -118,21 +102,21 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
var caps = new IndexerCapabilities
|
||||
{
|
||||
TvSearchParams = new List<TvSearchParam>
|
||||
{
|
||||
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
|
||||
},
|
||||
{
|
||||
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
|
||||
},
|
||||
MovieSearchParams = new List<MovieSearchParam>
|
||||
{
|
||||
MovieSearchParam.Q
|
||||
},
|
||||
{
|
||||
MovieSearchParam.Q
|
||||
},
|
||||
MusicSearchParams = new List<MusicSearchParam>
|
||||
{
|
||||
MusicSearchParam.Q
|
||||
},
|
||||
{
|
||||
MusicSearchParam.Q
|
||||
},
|
||||
BookSearchParams = new List<BookSearchParam>
|
||||
{
|
||||
BookSearchParam.Q
|
||||
}
|
||||
{
|
||||
BookSearchParam.Q
|
||||
}
|
||||
};
|
||||
|
||||
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.Audio);
|
||||
@@ -163,10 +147,6 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
public UserPassTorrentBaseSettings Settings { get; set; }
|
||||
public IndexerCapabilities Capabilities { get; set; }
|
||||
|
||||
public BBRequestGenerator()
|
||||
{
|
||||
}
|
||||
|
||||
private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories)
|
||||
{
|
||||
var searchUrl = string.Format("{0}/torrents.php", Settings.BaseUrl.TrimEnd('/'));
|
||||
@@ -197,49 +177,29 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
yield return request;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories);
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories);
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedTvSearchString), searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedTvSearchString), searchCriteria.Categories);
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories);
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories);
|
||||
}
|
||||
|
||||
public Func<IDictionary<string, string>> GetCookies { get; set; }
|
||||
|
||||
@@ -8,7 +8,6 @@ using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using AngleSharp.Dom;
|
||||
using AngleSharp.Html.Parser;
|
||||
using FluentValidation;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Annotations;
|
||||
@@ -19,7 +18,6 @@ using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
@@ -27,7 +25,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
public override string Name => "BakaBT";
|
||||
|
||||
public override string[] IndexerUrls => new string[] { "https://bakabt.me/" };
|
||||
public override string[] IndexerUrls => new[] { "https://bakabt.me/" };
|
||||
public override string Description => "Anime Community";
|
||||
private string LoginUrl => Settings.BaseUrl + "login.php";
|
||||
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
|
||||
@@ -41,7 +39,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
public override IIndexerRequestGenerator GetRequestGenerator()
|
||||
{
|
||||
return new BakaBTRequestGenerator() { Settings = Settings, Capabilities = Capabilities };
|
||||
return new BakaBTRequestGenerator { Settings = Settings, Capabilities = Capabilities };
|
||||
}
|
||||
|
||||
public override IParseIndexerResponse GetParser()
|
||||
@@ -52,14 +50,14 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
public override async Task<byte[]> Download(Uri link)
|
||||
{
|
||||
var request = new HttpRequestBuilder(link.ToString())
|
||||
.SetCookies(GetCookies() ?? new Dictionary<string, string>())
|
||||
.Build();
|
||||
.SetCookies(GetCookies() ?? new Dictionary<string, string>())
|
||||
.Build();
|
||||
|
||||
var response = await _httpClient.ExecuteProxiedAsync(request, Definition);
|
||||
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(response.Content);
|
||||
var downloadLink = dom.QuerySelectorAll(".download_link").First().GetAttribute("href");
|
||||
var downloadLink = dom.QuerySelector(".download_link")?.GetAttribute("href");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(downloadLink))
|
||||
{
|
||||
@@ -76,19 +74,12 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
var requestBuilder = new HttpRequestBuilder(LoginUrl)
|
||||
{
|
||||
LogResponseContent = true,
|
||||
AllowAutoRedirect = true
|
||||
AllowAutoRedirect = true,
|
||||
Method = HttpMethod.Post
|
||||
};
|
||||
|
||||
var loginPage = await ExecuteAuth(new HttpRequest(LoginUrl));
|
||||
|
||||
requestBuilder.Method = HttpMethod.Post;
|
||||
requestBuilder.PostProcess += r => r.RequestTimeout = TimeSpan.FromSeconds(15);
|
||||
requestBuilder.SetCookies(loginPage.GetCookies());
|
||||
|
||||
requestBuilder.AddFormParameter("username", Settings.Username);
|
||||
requestBuilder.AddFormParameter("password", Settings.Password);
|
||||
requestBuilder.AddFormParameter("returnto", "/index.php");
|
||||
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(loginPage.Content);
|
||||
var loginKey = dom.QuerySelector("input[name=\"loginKey\"]");
|
||||
@@ -98,14 +89,18 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
}
|
||||
|
||||
var authLoginRequest = requestBuilder
|
||||
.SetHeader("Content-Type", "multipart/form-data")
|
||||
.SetCookies(loginPage.GetCookies())
|
||||
.AddFormParameter("username", Settings.Username)
|
||||
.AddFormParameter("password", Settings.Password)
|
||||
.AddFormParameter("returnto", "/index.php")
|
||||
.SetHeader("Content-Type", "application/x-www-form-urlencoded")
|
||||
.Build();
|
||||
|
||||
var response = await ExecuteAuth(authLoginRequest);
|
||||
|
||||
if (response.Content != null && response.Content.Contains("<a href=\"logout.php\">Logout</a>"))
|
||||
{
|
||||
UpdateCookies(response.GetCookies(), DateTime.Now + TimeSpan.FromDays(30));
|
||||
UpdateCookies(response.GetCookies(), DateTime.Now.AddDays(30));
|
||||
|
||||
_logger.Debug("BakaBT authentication succeeded");
|
||||
}
|
||||
@@ -117,12 +112,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
|
||||
{
|
||||
if (!httpResponse.Content.Contains("<a href=\"logout.php\">Logout</a>"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return !httpResponse.Content.Contains("<a href=\"logout.php\">Logout</a>");
|
||||
}
|
||||
|
||||
private IndexerCapabilities SetCapabilities()
|
||||
@@ -130,21 +120,21 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
var caps = new IndexerCapabilities
|
||||
{
|
||||
TvSearchParams = new List<TvSearchParam>
|
||||
{
|
||||
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
|
||||
},
|
||||
{
|
||||
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
|
||||
},
|
||||
MovieSearchParams = new List<MovieSearchParam>
|
||||
{
|
||||
MovieSearchParam.Q
|
||||
},
|
||||
{
|
||||
MovieSearchParam.Q
|
||||
},
|
||||
MusicSearchParams = new List<MusicSearchParam>
|
||||
{
|
||||
MusicSearchParam.Q
|
||||
},
|
||||
{
|
||||
MusicSearchParam.Q
|
||||
},
|
||||
BookSearchParams = new List<BookSearchParam>
|
||||
{
|
||||
BookSearchParam.Q
|
||||
}
|
||||
{
|
||||
BookSearchParam.Q
|
||||
}
|
||||
};
|
||||
|
||||
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.TVAnime, "Anime Series");
|
||||
@@ -166,10 +156,6 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
public BakaBTSettings Settings { get; set; }
|
||||
public IndexerCapabilities Capabilities { get; set; }
|
||||
|
||||
public BakaBTRequestGenerator()
|
||||
{
|
||||
}
|
||||
|
||||
private IEnumerable<IndexerRequest> GetPagedRequests(string term)
|
||||
{
|
||||
var searchString = term;
|
||||
@@ -192,49 +178,29 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
yield return request;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm)));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm));
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm)));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm));
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm)));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm));
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm)));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm));
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm)));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm));
|
||||
}
|
||||
|
||||
public Func<IDictionary<string, string>> GetCookies { get; set; }
|
||||
@@ -245,7 +211,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
private readonly BakaBTSettings _settings;
|
||||
private readonly IndexerCapabilitiesCategories _categories;
|
||||
private readonly List<IndexerCategory> _defaultCategories = new List<IndexerCategory> { NewznabStandardCategory.TVAnime };
|
||||
private readonly List<IndexerCategory> _defaultCategories = new () { NewznabStandardCategory.TVAnime };
|
||||
|
||||
public BakaBTParser(BakaBTSettings settings, IndexerCapabilitiesCategories categories)
|
||||
{
|
||||
@@ -295,7 +261,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
var stringSeparator = new[] { " | " };
|
||||
var titles = titleSeries.Split(stringSeparator, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
if (titles.Count() > 1 && !_settings.AddRomajiTitle)
|
||||
if (titles.Length > 1 && !_settings.AddRomajiTitle)
|
||||
{
|
||||
titles = titles.Skip(1).ToArray();
|
||||
}
|
||||
@@ -307,7 +273,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
release.Title = (name + releaseInfo).Trim();
|
||||
|
||||
// Ensure the season is defined as this tracker only deals with full seasons
|
||||
if (release.Title.IndexOf("Season") == -1 && _settings.AppendSeason)
|
||||
if (!release.Title.Contains("Season", StringComparison.CurrentCulture) && _settings.AppendSeason)
|
||||
{
|
||||
// Insert before the release info
|
||||
var aidx = release.Title.IndexOf('(');
|
||||
@@ -415,10 +381,6 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
public class BakaBTSettings : UserPassTorrentBaseSettings
|
||||
{
|
||||
public BakaBTSettings()
|
||||
{
|
||||
}
|
||||
|
||||
[FieldDefinition(4, Label = "Add Romaji Title", Type = FieldType.Checkbox, HelpText = "Add releases for Romaji Title")]
|
||||
public bool AddRomajiTitle { get; set; }
|
||||
|
||||
|
||||
@@ -120,49 +120,29 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
yield return indexerRequest;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories, searchCriteria.FullImdbId, searchCriteria.TmdbId.GetValueOrDefault()));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories, searchCriteria.FullImdbId, searchCriteria.TmdbId.GetValueOrDefault());
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories);
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedTvSearchString), searchCriteria.Categories, searchCriteria.FullImdbId));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedTvSearchString), searchCriteria.Categories, searchCriteria.FullImdbId);
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories);
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories);
|
||||
}
|
||||
|
||||
public Func<IDictionary<string, string>> GetCookies { get; set; }
|
||||
|
||||
@@ -90,7 +90,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{ "adv_sort", "date" },
|
||||
{ "q", term },
|
||||
{ "m", searchCriteria.Offset.ToString() },
|
||||
{ "max", searchCriteria.Limit?.ToString() ?? "100" }
|
||||
{ "max", searchCriteria.Limit.ToString() ?? "100" }
|
||||
};
|
||||
|
||||
var searchUrl = string.Format("{0}/?{1}", Settings.BaseUrl.TrimEnd('/'), qc.GetQueryString());
|
||||
@@ -100,49 +100,29 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
yield return request;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria);
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria);
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedTvSearchString), searchCriteria));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedTvSearchString), searchCriteria);
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria);
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria);
|
||||
}
|
||||
|
||||
public Func<IDictionary<string, string>> GetCookies { get; set; }
|
||||
|
||||
@@ -129,49 +129,29 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
}
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories, searchCriteria.FullImdbId));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories, searchCriteria.FullImdbId);
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories);
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedTvSearchString), searchCriteria.Categories, searchCriteria.FullImdbId));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedTvSearchString), searchCriteria.Categories, searchCriteria.FullImdbId);
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories);
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories);
|
||||
}
|
||||
|
||||
public Func<IDictionary<string, string>> GetCookies { get; set; }
|
||||
|
||||
@@ -34,31 +34,29 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
|
||||
yield return new IndexerRequest(builder.Build());
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
{
|
||||
return new IndexerPageableRequestChain();
|
||||
return new List<IndexerRequest>();
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
{
|
||||
return new IndexerPageableRequestChain();
|
||||
return new List<IndexerRequest>();
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
var parameters = new BroadcastheNetTorrentQuery();
|
||||
|
||||
var searchString = searchCriteria.SearchTerm != null ? searchCriteria.SearchTerm : "";
|
||||
|
||||
var btnResults = searchCriteria.Limit.GetValueOrDefault();
|
||||
var btnResults = searchCriteria.Limit;
|
||||
if (btnResults == 0)
|
||||
{
|
||||
btnResults = (int)Capabilities.LimitsDefault;
|
||||
}
|
||||
|
||||
var btnOffset = searchCriteria.Offset.GetValueOrDefault();
|
||||
var btnOffset = searchCriteria.Offset;
|
||||
|
||||
if (searchCriteria.TvdbId > 0)
|
||||
{
|
||||
@@ -93,25 +91,21 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
|
||||
// Neither a season only search nor daily nor standard, fall back to query
|
||||
parameters.Search = searchString.Replace(" ", "%");
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(parameters, btnResults, btnOffset));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests(parameters, btnResults, btnOffset);
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
{
|
||||
return new IndexerPageableRequestChain();
|
||||
return new List<IndexerRequest>();
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
var parameters = new BroadcastheNetTorrentQuery();
|
||||
|
||||
var searchString = searchCriteria.SearchTerm != null ? searchCriteria.SearchTerm : "";
|
||||
|
||||
var btnResults = searchCriteria.Limit.GetValueOrDefault();
|
||||
var btnResults = searchCriteria.Limit;
|
||||
if (btnResults == 0)
|
||||
{
|
||||
btnResults = (int)Capabilities.LimitsDefault;
|
||||
@@ -119,11 +113,9 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
|
||||
|
||||
parameters.Search = searchString.Replace(" ", "%");
|
||||
|
||||
var btnOffset = searchCriteria.Offset.GetValueOrDefault();
|
||||
var btnOffset = searchCriteria.Offset;
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(parameters, btnResults, btnOffset));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests(parameters, btnResults, btnOffset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using FluentValidation.Results;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Exceptions;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.IndexerVersions;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
@@ -29,10 +28,6 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
|
||||
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
|
||||
|
||||
// Page size is different per indexer, setting to 1 ensures we don't break out of paging logic
|
||||
// thinking its a partial page and instead all search_path requests are run for each indexer
|
||||
public override int PageSize => 1;
|
||||
|
||||
public override TimeSpan RateLimit
|
||||
{
|
||||
get
|
||||
@@ -48,12 +43,28 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
}
|
||||
}
|
||||
|
||||
public override int PageSize
|
||||
{
|
||||
get
|
||||
{
|
||||
var definition = _definitionService.GetCachedDefinition(Settings.DefinitionFile);
|
||||
|
||||
if (definition.Search != null && definition.Search.PageSize > 0)
|
||||
{
|
||||
return definition.Search.PageSize;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public override IIndexerRequestGenerator GetRequestGenerator()
|
||||
{
|
||||
var generator = _generatorCache.Get(Settings.DefinitionFile, () =>
|
||||
new CardigannRequestGenerator(_configService,
|
||||
_definitionService.GetCachedDefinition(Settings.DefinitionFile),
|
||||
_logger)
|
||||
_logger,
|
||||
RateLimit)
|
||||
{
|
||||
HttpClient = _httpClient,
|
||||
Definition = Definition,
|
||||
@@ -79,6 +90,37 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
};
|
||||
}
|
||||
|
||||
protected override IList<ReleaseInfo> CleanupReleases(IEnumerable<ReleaseInfo> releases, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
var cleanReleases = base.CleanupReleases(releases, searchCriteria);
|
||||
|
||||
if (_definitionService.GetCachedDefinition(Settings.DefinitionFile).Search?.Rows?.Filters?.Any(x => x.Name == "andmatch") ?? false)
|
||||
{
|
||||
cleanReleases = FilterReleasesByQuery(releases, searchCriteria).ToList();
|
||||
}
|
||||
|
||||
// Only take the request results using Offset and Limit from the search
|
||||
var pageSize = PageSize;
|
||||
|
||||
if (pageSize > 0)
|
||||
{
|
||||
var minPage = searchCriteria.Offset / pageSize;
|
||||
var firstResult = searchCriteria.Offset - (pageSize * minPage);
|
||||
|
||||
cleanReleases = cleanReleases
|
||||
.Skip(firstResult)
|
||||
.Take(searchCriteria.Limit).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
cleanReleases = cleanReleases
|
||||
.Skip(searchCriteria.Offset)
|
||||
.Take(searchCriteria.Limit).ToList();
|
||||
}
|
||||
|
||||
return cleanReleases;
|
||||
}
|
||||
|
||||
protected override IDictionary<string, string> GetCookies()
|
||||
{
|
||||
if (Settings.ExtraFieldData.TryGetValue("cookie", out var cookies))
|
||||
@@ -117,8 +159,8 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
{
|
||||
var defaultSettings = new List<SettingsField>
|
||||
{
|
||||
new SettingsField { Name = "username", Label = "Username", Type = "text" },
|
||||
new SettingsField { Name = "password", Label = "Password", Type = "password" }
|
||||
new () { Name = "username", Label = "Username", Type = "text" },
|
||||
new () { Name = "password", Label = "Password", Type = "password" }
|
||||
};
|
||||
|
||||
var settings = definition.Settings ?? defaultSettings;
|
||||
@@ -180,60 +222,15 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
await generator.DoLogin();
|
||||
}
|
||||
|
||||
public override async Task<byte[]> Download(Uri link)
|
||||
protected override async Task<HttpRequest> GetDownloadRequest(Uri link)
|
||||
{
|
||||
var generator = (CardigannRequestGenerator)GetRequestGenerator();
|
||||
|
||||
var request = await generator.DownloadRequest(link);
|
||||
|
||||
if (request.Url.Scheme == "magnet")
|
||||
{
|
||||
ValidateMagnet(request.Url.FullUri);
|
||||
return Encoding.UTF8.GetBytes(request.Url.FullUri);
|
||||
}
|
||||
|
||||
request.AllowAutoRedirect = true;
|
||||
|
||||
var downloadBytes = Array.Empty<byte>();
|
||||
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.ExecuteProxiedAsync(request, Definition);
|
||||
downloadBytes = response.ResponseData;
|
||||
}
|
||||
catch (HttpException ex)
|
||||
{
|
||||
if (ex.Response.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
_logger.Error(ex, "Downloading torrent file for release failed since it no longer exists ({0})", request.Url.FullUri);
|
||||
throw new ReleaseUnavailableException("Downloading torrent failed", ex);
|
||||
}
|
||||
|
||||
if (ex.Response.StatusCode == HttpStatusCode.TooManyRequests)
|
||||
{
|
||||
_logger.Error("API Grab Limit reached for {0}", request.Url.FullUri);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Error(ex, "Downloading torrent file for release failed ({0})", request.Url.FullUri);
|
||||
}
|
||||
|
||||
throw new ReleaseDownloadException("Downloading torrent failed", ex);
|
||||
}
|
||||
catch (WebException ex)
|
||||
{
|
||||
_logger.Error(ex, "Downloading torrent file for release failed ({0})", request.Url.FullUri);
|
||||
|
||||
throw new ReleaseDownloadException("Downloading torrent failed", ex);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
_indexerStatusService.RecordFailure(Definition.Id);
|
||||
_logger.Error("Downloading torrent failed");
|
||||
throw;
|
||||
}
|
||||
|
||||
return downloadBytes;
|
||||
return request;
|
||||
}
|
||||
|
||||
protected override async Task Test(List<ValidationFailure> failures)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
@@ -300,7 +301,12 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
}
|
||||
else if (setting.Type == "checkbox")
|
||||
{
|
||||
variables[name] = ((bool)value) ? ".True" : null;
|
||||
if (value is string stringValue && bool.TryParse(stringValue, out var result))
|
||||
{
|
||||
value = result;
|
||||
}
|
||||
|
||||
variables[name] = (bool)value ? ".True" : null;
|
||||
}
|
||||
else if (setting.Type == "select")
|
||||
{
|
||||
@@ -328,12 +334,12 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
throw new NotSupportedException($"Type {setting.Type} is not supported.");
|
||||
}
|
||||
|
||||
if (setting.Type != "password" && setting.Name != "apikey" && setting.Name != "rsskey" && indexerLogging)
|
||||
if (setting.Type != "password" && setting.Name != "apikey" && setting.Name != "rsskey" && indexerLogging && variables.ContainsKey(name))
|
||||
{
|
||||
_logger.Debug($"Setting {setting.Name} to {variables[name]}");
|
||||
_logger.Debug($"Setting {setting.Name} to {variables[name].ToJson()}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,11 +350,13 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
|
||||
public string ApplyGoTemplateText(string template, Dictionary<string, object> variables = null, TemplateTextModifier modifier = null)
|
||||
{
|
||||
if (variables == null)
|
||||
if (template.IsNullOrWhiteSpace() || !template.Contains("{{"))
|
||||
{
|
||||
variables = GetBaseTemplateVariables();
|
||||
return template;
|
||||
}
|
||||
|
||||
variables ??= GetBaseTemplateVariables();
|
||||
|
||||
// handle re_replace expression
|
||||
// Example: {{ re_replace .Query.Keywords "[^a-zA-Z0-9]+" "%" }}
|
||||
var reReplaceRegex = new Regex(@"{{\s*re_replace\s+(\..+?)\s+""(.*?)""\s+""(.*?)""\s*}}");
|
||||
@@ -606,10 +614,11 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
case "timeparse":
|
||||
case "dateparse":
|
||||
var layout = (string)filter.Args;
|
||||
|
||||
try
|
||||
{
|
||||
var date = DateTimeUtil.ParseDateTimeGoLang(data, layout);
|
||||
data = date.ToString(DateTimeUtil.Rfc1123ZPattern);
|
||||
data = date.ToString(DateTimeUtil.Rfc1123ZPattern, CultureInfo.InvariantCulture);
|
||||
}
|
||||
catch (InvalidDateException ex)
|
||||
{
|
||||
@@ -650,15 +659,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
break;
|
||||
case "trim":
|
||||
var cutset = (string)filter.Args;
|
||||
if (cutset != null)
|
||||
{
|
||||
data = data.Trim(cutset[0]);
|
||||
}
|
||||
else
|
||||
{
|
||||
data = data.Trim();
|
||||
}
|
||||
|
||||
data = cutset != null ? data.Trim(cutset[0]) : data.Trim();
|
||||
break;
|
||||
case "prepend":
|
||||
var prependstr = (string)filter.Args;
|
||||
@@ -688,10 +689,10 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
break;
|
||||
case "timeago":
|
||||
case "reltime":
|
||||
data = DateTimeUtil.FromTimeAgo(data).ToString(DateTimeUtil.Rfc1123ZPattern);
|
||||
data = DateTimeUtil.FromTimeAgo(data).ToString(DateTimeUtil.Rfc1123ZPattern, CultureInfo.InvariantCulture);
|
||||
break;
|
||||
case "fuzzytime":
|
||||
data = DateTimeUtil.FromUnknown(data).ToString(DateTimeUtil.Rfc1123ZPattern);
|
||||
data = DateTimeUtil.FromUnknown(data).ToString(DateTimeUtil.Rfc1123ZPattern, CultureInfo.InvariantCulture);
|
||||
break;
|
||||
case "validfilename":
|
||||
data = StringUtil.MakeValidFileName(data, '_', false);
|
||||
@@ -739,18 +740,20 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
// for debugging
|
||||
var debugData = data.Replace("\r", "\\r").Replace("\n", "\\n").Replace("\xA0", "\\xA0");
|
||||
var strTag = (string)filter.Args;
|
||||
if (strTag != null)
|
||||
{
|
||||
strTag = string.Format("({0}):", strTag);
|
||||
}
|
||||
else
|
||||
{
|
||||
strTag = ":";
|
||||
}
|
||||
strTag = strTag != null ? $"({strTag}):" : ":";
|
||||
|
||||
_logger.Debug(string.Format("CardigannIndexer ({0}): strdump{1} {2}", _definition.Id, strTag, debugData));
|
||||
_logger.Debug($"CardigannIndexer ({_definition.Id}): strdump{strTag} {debugData}");
|
||||
break;
|
||||
case "validate":
|
||||
char[] delimiters = { ',', ' ', '/', ')', '(', '.', ';', '[', ']', '"', '|', ':' };
|
||||
var args = (string)filter.Args;
|
||||
var argsList = args.ToLower().Split(delimiters, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
|
||||
var validList = argsList.ToList();
|
||||
var validIntersect = validList.Intersect(data.ToLower().Split(delimiters, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)).ToList();
|
||||
data = string.Join(", ", validIntersect);
|
||||
break;
|
||||
default:
|
||||
_logger.Error($"CardigannIndexer ({_definition.Id}): Unsupported field filter: {filter.Name}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -758,8 +761,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
return data;
|
||||
}
|
||||
|
||||
protected Dictionary<string, string> ParseCustomHeaders(Dictionary<string, List<string>> customHeaders,
|
||||
Dictionary<string, object> variables)
|
||||
protected Dictionary<string, string> ParseCustomHeaders(Dictionary<string, List<string>> customHeaders, Dictionary<string, object> variables)
|
||||
{
|
||||
if (customHeaders == null)
|
||||
{
|
||||
|
||||
@@ -44,7 +44,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
public double? RequestDelay { get; set; }
|
||||
public List<string> Links { get; set; }
|
||||
public List<string> Legacylinks { get; set; }
|
||||
public bool Followredirect { get; set; } = false;
|
||||
public bool Followredirect { get; set; }
|
||||
public bool TestLinkTorrent { get; set; } = true;
|
||||
public List<string> Certificates { get; set; }
|
||||
public CapabilitiesBlock Caps { get; set; }
|
||||
@@ -95,13 +95,14 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
public List<string> Cookies { get; set; }
|
||||
public string Method { get; set; }
|
||||
public string Form { get; set; }
|
||||
public bool Selectors { get; set; } = false;
|
||||
public bool Selectors { get; set; }
|
||||
public Dictionary<string, string> Inputs { get; set; }
|
||||
public Dictionary<string, SelectorBlock> Selectorinputs { get; set; }
|
||||
public Dictionary<string, SelectorBlock> Getselectorinputs { get; set; }
|
||||
public List<ErrorBlock> Error { get; set; }
|
||||
public PageTestBlock Test { get; set; }
|
||||
public CaptchaBlock Captcha { get; set; }
|
||||
public Dictionary<string, List<string>> Headers { get; set; }
|
||||
}
|
||||
|
||||
public class ErrorBlock
|
||||
@@ -114,7 +115,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
public class SelectorBlock
|
||||
{
|
||||
public string Selector { get; set; }
|
||||
public bool Optional { get; set; } = false;
|
||||
public bool Optional { get; set; }
|
||||
public string Text { get; set; }
|
||||
public string Attribute { get; set; }
|
||||
public string Remove { get; set; }
|
||||
@@ -141,6 +142,9 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
|
||||
public class SearchBlock
|
||||
{
|
||||
public int PageSize { get; set; }
|
||||
public int FirstPageNumber { get; set; }
|
||||
public bool AllowEmptyInputs { get; set; }
|
||||
public string Path { get; set; }
|
||||
public List<SearchPathBlock> Paths { get; set; }
|
||||
public Dictionary<string, List<string>> Headers { get; set; }
|
||||
@@ -157,7 +161,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
public int After { get; set; }
|
||||
public SelectorBlock Dateheaders { get; set; }
|
||||
public SelectorBlock Count { get; set; }
|
||||
public bool Multiple { get; set; } = false;
|
||||
public bool Multiple { get; set; }
|
||||
}
|
||||
|
||||
public class SearchPathBlock : RequestBlock
|
||||
@@ -182,20 +186,21 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
public string Method { get; set; }
|
||||
public BeforeBlock Before { get; set; }
|
||||
public InfohashBlock Infohash { get; set; }
|
||||
public Dictionary<string, List<string>> Headers { get; set; }
|
||||
}
|
||||
|
||||
public class InfohashBlock
|
||||
{
|
||||
public SelectorField Hash { get; set; }
|
||||
public SelectorField Title { get; set; }
|
||||
public bool UseBeforeResponse { get; set; }
|
||||
public bool Usebeforeresponse { get; set; }
|
||||
}
|
||||
|
||||
public class SelectorField
|
||||
{
|
||||
public string Selector { get; set; }
|
||||
public string Attribute { get; set; }
|
||||
public bool UseBeforeResponse { get; set; }
|
||||
public bool Usebeforeresponse { get; set; }
|
||||
public List<FilterBlock> Filters { get; set; }
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text.RegularExpressions;
|
||||
@@ -40,12 +41,16 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
|
||||
if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK)
|
||||
{
|
||||
// Remove cookie cache
|
||||
if (indexerResponse.HttpResponse.HasHttpRedirect && indexerResponse.HttpResponse.RedirectUrl
|
||||
.ContainsIgnoreCase("login.php"))
|
||||
if (indexerResponse.HttpResponse.HasHttpRedirect)
|
||||
{
|
||||
CookiesUpdater(null, null);
|
||||
throw new IndexerException(indexerResponse, "We are being redirected to the login page. Most likely your session expired or was killed. Recheck your cookie or credentials and try testing the indexer.");
|
||||
if (indexerResponse.HttpResponse.RedirectUrl.ContainsIgnoreCase("login.php"))
|
||||
{
|
||||
// Remove cookie cache
|
||||
CookiesUpdater(null, null);
|
||||
throw new IndexerException(indexerResponse, "We are being redirected to the login page. Most likely your session expired or was killed. Recheck your cookie or credentials and try testing the indexer.");
|
||||
}
|
||||
|
||||
throw new IndexerException(indexerResponse, $"Redirected to {indexerResponse.HttpResponse.RedirectUrl} from API request");
|
||||
}
|
||||
|
||||
throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from API request");
|
||||
@@ -62,12 +67,13 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
{
|
||||
if (request.SearchPath.Response != null &&
|
||||
request.SearchPath.Response.NoResultsMessage != null &&
|
||||
((request.SearchPath.Response.NoResultsMessage != string.Empty && results.Contains(request.SearchPath.Response.NoResultsMessage)) || (request.SearchPath.Response.NoResultsMessage == string.Empty && results == string.Empty)))
|
||||
((request.SearchPath.Response.NoResultsMessage.IsNotNullOrWhiteSpace() && results.Contains(request.SearchPath.Response.NoResultsMessage)) || (request.SearchPath.Response.NoResultsMessage.IsNullOrWhiteSpace() && results.IsNullOrWhiteSpace())))
|
||||
{
|
||||
return releases;
|
||||
}
|
||||
|
||||
var parsedJson = JToken.Parse(results);
|
||||
|
||||
if (parsedJson == null)
|
||||
{
|
||||
throw new IndexerException(indexerResponse, "Error Parsing Json Response");
|
||||
@@ -76,12 +82,10 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
if (search.Rows.Count != null)
|
||||
{
|
||||
var countVal = HandleJsonSelector(search.Rows.Count, parsedJson, variables);
|
||||
if (int.TryParse(countVal, out var count))
|
||||
|
||||
if (int.TryParse(countVal, out var count) && count < 1)
|
||||
{
|
||||
if (count < 1)
|
||||
{
|
||||
return releases;
|
||||
}
|
||||
return releases;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,205 +165,191 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
IHtmlCollection<IElement> rowsDom;
|
||||
|
||||
if (request.SearchPath.Response != null && request.SearchPath.Response.Type.Equals("xml"))
|
||||
{
|
||||
IHtmlCollection<IElement> rowsDom;
|
||||
var searchResultParser = new XmlParser();
|
||||
var searchResultDocument = searchResultParser.ParseDocument(results);
|
||||
|
||||
if (request.SearchPath.Response != null && request.SearchPath.Response.Type.Equals("xml"))
|
||||
if (search.Preprocessingfilters != null)
|
||||
{
|
||||
var searchResultParser = new XmlParser();
|
||||
var searchResultDocument = searchResultParser.ParseDocument(results);
|
||||
|
||||
if (search.Preprocessingfilters != null)
|
||||
{
|
||||
results = ApplyFilters(results, search.Preprocessingfilters, variables);
|
||||
searchResultDocument = searchResultParser.ParseDocument(results);
|
||||
_logger.Trace(string.Format("CardigannIndexer ({0}): result after preprocessingfilters: {1}", _definition.Id, results));
|
||||
}
|
||||
|
||||
var rowsSelector = ApplyGoTemplateText(search.Rows.Selector, variables);
|
||||
rowsDom = searchResultDocument.QuerySelectorAll(rowsSelector);
|
||||
}
|
||||
else
|
||||
{
|
||||
var searchResultParser = new HtmlParser();
|
||||
var searchResultDocument = searchResultParser.ParseDocument(results);
|
||||
|
||||
if (search.Preprocessingfilters != null)
|
||||
{
|
||||
results = ApplyFilters(results, search.Preprocessingfilters, variables);
|
||||
searchResultDocument = searchResultParser.ParseDocument(results);
|
||||
_logger.Trace(string.Format("CardigannIndexer ({0}): result after preprocessingfilters: {1}", _definition.Id, results));
|
||||
}
|
||||
|
||||
var rowsSelector = ApplyGoTemplateText(search.Rows.Selector, variables);
|
||||
rowsDom = searchResultDocument.QuerySelectorAll(rowsSelector);
|
||||
results = ApplyFilters(results, search.Preprocessingfilters, variables);
|
||||
searchResultDocument = searchResultParser.ParseDocument(results);
|
||||
_logger.Trace(string.Format("CardigannIndexer ({0}): result after preprocessingfilters: {1}", _definition.Id, results));
|
||||
}
|
||||
|
||||
var rows = new List<IElement>();
|
||||
foreach (var rowDom in rowsDom)
|
||||
var rowsSelector = ApplyGoTemplateText(search.Rows.Selector, variables);
|
||||
rowsDom = searchResultDocument.QuerySelectorAll(rowsSelector);
|
||||
}
|
||||
else
|
||||
{
|
||||
var searchResultParser = new HtmlParser();
|
||||
var searchResultDocument = searchResultParser.ParseDocument(results);
|
||||
|
||||
if (search.Preprocessingfilters != null)
|
||||
{
|
||||
rows.Add(rowDom);
|
||||
results = ApplyFilters(results, search.Preprocessingfilters, variables);
|
||||
searchResultDocument = searchResultParser.ParseDocument(results);
|
||||
_logger.Trace(string.Format("CardigannIndexer ({0}): result after preprocessingfilters: {1}", _definition.Id, results));
|
||||
}
|
||||
|
||||
// merge following rows for After selector
|
||||
var after = search.Rows.After;
|
||||
if (after > 0)
|
||||
var rowsSelector = ApplyGoTemplateText(search.Rows.Selector, variables);
|
||||
rowsDom = searchResultDocument.QuerySelectorAll(rowsSelector);
|
||||
}
|
||||
|
||||
var rows = new List<IElement>();
|
||||
foreach (var rowDom in rowsDom)
|
||||
{
|
||||
rows.Add(rowDom);
|
||||
}
|
||||
|
||||
// merge following rows for After selector
|
||||
var after = search.Rows.After;
|
||||
if (after > 0)
|
||||
{
|
||||
for (var i = 0; i < rows.Count; i += 1)
|
||||
{
|
||||
for (var i = 0; i < rows.Count; i += 1)
|
||||
var currentRow = rows[i];
|
||||
for (var j = 0; j < after; j += 1)
|
||||
{
|
||||
var currentRow = rows[i];
|
||||
for (var j = 0; j < after; j += 1)
|
||||
var mergeRowIndex = i + j + 1;
|
||||
var mergeRow = rows[mergeRowIndex];
|
||||
var mergeNodes = new List<INode>();
|
||||
foreach (var node in mergeRow.ChildNodes)
|
||||
{
|
||||
var mergeRowIndex = i + j + 1;
|
||||
var mergeRow = rows[mergeRowIndex];
|
||||
var mergeNodes = new List<INode>();
|
||||
foreach (var node in mergeRow.ChildNodes)
|
||||
{
|
||||
mergeNodes.Add(node);
|
||||
}
|
||||
|
||||
currentRow.Append(mergeNodes.ToArray());
|
||||
mergeNodes.Add(node);
|
||||
}
|
||||
|
||||
rows.RemoveRange(i + 1, after);
|
||||
currentRow.Append(mergeNodes.ToArray());
|
||||
}
|
||||
|
||||
rows.RemoveRange(i + 1, after);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var row in rows)
|
||||
foreach (var row in rows)
|
||||
{
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
var release = new TorrentInfo();
|
||||
var release = new TorrentInfo();
|
||||
|
||||
// Parse fields
|
||||
foreach (var field in search.Fields)
|
||||
// Parse fields
|
||||
foreach (var field in search.Fields)
|
||||
{
|
||||
var fieldParts = field.Key.Split('|');
|
||||
var fieldName = fieldParts[0];
|
||||
var fieldModifiers = new List<string>();
|
||||
for (var i = 1; i < fieldParts.Length; i++)
|
||||
{
|
||||
var fieldParts = field.Key.Split('|');
|
||||
var fieldName = fieldParts[0];
|
||||
var fieldModifiers = new List<string>();
|
||||
for (var i = 1; i < fieldParts.Length; i++)
|
||||
fieldModifiers.Add(fieldParts[i]);
|
||||
}
|
||||
|
||||
string value = null;
|
||||
var variablesKey = ".Result." + fieldName;
|
||||
var isOptional = OptionalFields.Contains(field.Key) || fieldModifiers.Contains("optional") || field.Value.Optional;
|
||||
try
|
||||
{
|
||||
value = HandleSelector(field.Value, row, variables, !isOptional);
|
||||
|
||||
if (isOptional && string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
fieldModifiers.Add(fieldParts[i]);
|
||||
variables[variablesKey] = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
string value = null;
|
||||
var variablesKey = ".Result." + fieldName;
|
||||
var isOptional = OptionalFields.Contains(field.Key) || fieldModifiers.Contains("optional") || field.Value.Optional;
|
||||
variables[variablesKey] = ParseFields(value, fieldName, release, fieldModifiers, searchUrlUri);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (!variables.ContainsKey(variablesKey))
|
||||
{
|
||||
variables[variablesKey] = null;
|
||||
}
|
||||
|
||||
if (OptionalFields.Contains(field.Key) || fieldModifiers.Contains("optional") || field.Value.Optional)
|
||||
{
|
||||
variables[variablesKey] = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (indexerLogging)
|
||||
{
|
||||
_logger.Trace("Error while parsing field={0}, selector={1}, value={2}: {3}", field.Key, field.Value.Selector, value == null ? "<null>" : value, ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var filters = search.Rows.Filters;
|
||||
var skipRelease = ParseRowFilters(filters, release, variables, row.ToHtmlPretty());
|
||||
|
||||
if (skipRelease)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// if DateHeaders is set go through the previous rows and look for the header selector
|
||||
var dateHeaders = _definition.Search.Rows.Dateheaders;
|
||||
if (release.PublishDate == DateTime.MinValue && dateHeaders != null)
|
||||
{
|
||||
var prevRow = row.PreviousElementSibling;
|
||||
string value = null;
|
||||
if (prevRow == null)
|
||||
{
|
||||
// continue with parent
|
||||
var parent = row.ParentElement;
|
||||
if (parent != null)
|
||||
{
|
||||
prevRow = parent.PreviousElementSibling;
|
||||
}
|
||||
}
|
||||
|
||||
while (prevRow != null)
|
||||
{
|
||||
var curRow = prevRow;
|
||||
_logger.Debug(prevRow.OuterHtml);
|
||||
try
|
||||
{
|
||||
value = HandleSelector(field.Value, row, variables, !isOptional);
|
||||
|
||||
if (isOptional && string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
variables[variablesKey] = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
variables[variablesKey] = ParseFields(value, fieldName, release, fieldModifiers, searchUrlUri);
|
||||
value = HandleSelector(dateHeaders, curRow);
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (Exception)
|
||||
{
|
||||
if (!variables.ContainsKey(variablesKey))
|
||||
{
|
||||
variables[variablesKey] = null;
|
||||
}
|
||||
|
||||
if (OptionalFields.Contains(field.Key) || fieldModifiers.Contains("optional") || field.Value.Optional)
|
||||
{
|
||||
variables[variablesKey] = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (indexerLogging)
|
||||
{
|
||||
_logger.Trace("Error while parsing field={0}, selector={1}, value={2}: {3}", field.Key, field.Value.Selector, value == null ? "<null>" : value, ex.Message);
|
||||
}
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
var filters = search.Rows.Filters;
|
||||
var skipRelease = ParseRowFilters(filters, release, variables, row.ToHtmlPretty());
|
||||
|
||||
if (skipRelease)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// if DateHeaders is set go through the previous rows and look for the header selector
|
||||
var dateHeaders = _definition.Search.Rows.Dateheaders;
|
||||
if (release.PublishDate == DateTime.MinValue && dateHeaders != null)
|
||||
{
|
||||
var prevRow = row.PreviousElementSibling;
|
||||
string value = null;
|
||||
prevRow = curRow.PreviousElementSibling;
|
||||
if (prevRow == null)
|
||||
{
|
||||
// continue with parent
|
||||
var parent = row.ParentElement;
|
||||
var parent = curRow.ParentElement;
|
||||
if (parent != null)
|
||||
{
|
||||
prevRow = parent.PreviousElementSibling;
|
||||
}
|
||||
}
|
||||
|
||||
while (prevRow != null)
|
||||
{
|
||||
var curRow = prevRow;
|
||||
_logger.Debug(prevRow.OuterHtml);
|
||||
try
|
||||
{
|
||||
value = HandleSelector(dateHeaders, curRow);
|
||||
break;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
|
||||
prevRow = curRow.PreviousElementSibling;
|
||||
if (prevRow == null)
|
||||
{
|
||||
// continue with parent
|
||||
var parent = curRow.ParentElement;
|
||||
if (parent != null)
|
||||
{
|
||||
prevRow = parent.PreviousElementSibling;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (value == null && dateHeaders.Optional == false)
|
||||
{
|
||||
throw new CardigannException(string.Format("No date header row found for {0}", release.ToString()));
|
||||
}
|
||||
|
||||
if (value != null)
|
||||
{
|
||||
release.PublishDate = DateTimeUtil.FromUnknown(value);
|
||||
}
|
||||
}
|
||||
|
||||
releases.Add(release);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "CardigannIndexer ({0}): Error while parsing row '{1}':\n\n{2}", _definition.Id, row.ToHtmlPretty());
|
||||
if (value == null && dateHeaders.Optional == false)
|
||||
{
|
||||
throw new CardigannException(string.Format("No date header row found for {0}", release.ToString()));
|
||||
}
|
||||
|
||||
if (value != null)
|
||||
{
|
||||
release.PublishDate = DateTimeUtil.FromUnknown(value);
|
||||
}
|
||||
}
|
||||
|
||||
releases.Add(release);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "CardigannIndexer ({0}): Error while parsing row '{1}':\n\n{2}", _definition.Id, row.ToHtmlPretty());
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// OnParseError(results, ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
if (query.Limit > 0)
|
||||
{
|
||||
releases = releases.Take(query.Limit).ToList();
|
||||
}*/
|
||||
|
||||
releases.ForEach(c =>
|
||||
{
|
||||
// generate magnet link from info hash (not allowed for private sites)
|
||||
@@ -421,11 +411,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
break;
|
||||
case "comments":
|
||||
var commentsUrl = ResolvePath(value, searchUrlUri);
|
||||
if (release.CommentUrl == null)
|
||||
{
|
||||
release.CommentUrl = commentsUrl.AbsoluteUri;
|
||||
}
|
||||
|
||||
release.CommentUrl ??= commentsUrl.AbsoluteUri;
|
||||
value = commentsUrl.ToString();
|
||||
break;
|
||||
case "title":
|
||||
@@ -518,7 +504,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
break;
|
||||
case "date":
|
||||
release.PublishDate = DateTimeUtil.FromUnknown(value);
|
||||
value = release.PublishDate.ToString(DateTimeUtil.Rfc1123ZPattern);
|
||||
value = release.PublishDate.ToString(DateTimeUtil.Rfc1123ZPattern, CultureInfo.InvariantCulture);
|
||||
break;
|
||||
case "files":
|
||||
release.Files = ParseUtil.CoerceInt(value);
|
||||
@@ -550,38 +536,23 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
value = release.ImdbId.ToString();
|
||||
break;
|
||||
case "tmdbid":
|
||||
var tmdbIDRegEx = new Regex(@"(\d+)", RegexOptions.Compiled);
|
||||
var tmdbIDMatch = tmdbIDRegEx.Match(value);
|
||||
var tmdbID = tmdbIDMatch.Groups[1].Value;
|
||||
release.TmdbId = (int)ParseUtil.CoerceLong(tmdbID);
|
||||
release.TmdbId = (int)ParseUtil.GetLongFromString(value);
|
||||
value = release.TmdbId.ToString();
|
||||
break;
|
||||
case "rageid":
|
||||
var rageIDRegEx = new Regex(@"(\d+)", RegexOptions.Compiled);
|
||||
var rageIDMatch = rageIDRegEx.Match(value);
|
||||
var rageID = rageIDMatch.Groups[1].Value;
|
||||
release.TvRageId = (int)ParseUtil.CoerceLong(rageID);
|
||||
release.TvRageId = (int)ParseUtil.GetLongFromString(value);
|
||||
value = release.TvRageId.ToString();
|
||||
break;
|
||||
case "traktid":
|
||||
var traktIDRegEx = new Regex(@"(\d+)", RegexOptions.Compiled);
|
||||
var traktIDMatch = traktIDRegEx.Match(value);
|
||||
var traktID = traktIDMatch.Groups[1].Value;
|
||||
release.TraktId = (int)ParseUtil.CoerceLong(traktID);
|
||||
release.TraktId = (int)ParseUtil.GetLongFromString(value);
|
||||
value = release.TraktId.ToString();
|
||||
break;
|
||||
case "tvdbid":
|
||||
var tvdbIdRegEx = new Regex(@"(\d+)", RegexOptions.Compiled);
|
||||
var tvdbIdMatch = tvdbIdRegEx.Match(value);
|
||||
var tvdbId = tvdbIdMatch.Groups[1].Value;
|
||||
release.TvdbId = (int)ParseUtil.CoerceLong(tvdbId);
|
||||
release.TvdbId = (int)ParseUtil.GetLongFromString(value);
|
||||
value = release.TvdbId.ToString();
|
||||
break;
|
||||
case "doubanid":
|
||||
var doubanIDRegEx = new Regex(@"(\d+)", RegexOptions.Compiled);
|
||||
var doubanIDMatch = doubanIDRegEx.Match(value);
|
||||
var doubanID = doubanIDMatch.Groups[1].Value;
|
||||
release.DoubanId = (int)ParseUtil.CoerceLong(doubanID);
|
||||
release.DoubanId = (int)ParseUtil.GetLongFromString(value);
|
||||
value = release.DoubanId.ToString();
|
||||
break;
|
||||
case "poster":
|
||||
@@ -594,8 +565,12 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
value = release.PosterUrl;
|
||||
break;
|
||||
case "genre":
|
||||
release.Genres ??= new List<string>();
|
||||
char[] delimiters = { ',', ' ', '/', ')', '(', '.', ';', '[', ']', '"', '|', ':' };
|
||||
release.Genres = release.Genres.Union(value.Split(delimiters, System.StringSplitOptions.RemoveEmptyEntries)).ToList();
|
||||
release.Genres = release.Genres
|
||||
.Union(value.Split(delimiters, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries))
|
||||
.Select(x => x.Replace("_", " "))
|
||||
.ToList();
|
||||
value = string.Join(", ", release.Genres);
|
||||
break;
|
||||
case "year":
|
||||
@@ -641,29 +616,14 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
switch (filter.Name)
|
||||
{
|
||||
case "andmatch":
|
||||
var characterLimit = -1;
|
||||
if (filter.Args != null)
|
||||
{
|
||||
characterLimit = int.Parse(filter.Args);
|
||||
}
|
||||
|
||||
var queryKeywords = variables[".Keywords"] as string;
|
||||
|
||||
// See IndexerBase.FilterReleasesByQuery
|
||||
break;
|
||||
case "strdump":
|
||||
// for debugging
|
||||
_logger.Debug(string.Format("CardigannIndexer ({0}): row strdump: {1}", _definition.Id, row.ToString()));
|
||||
break;
|
||||
case "validate":
|
||||
char[] delimiters = { ',', ' ', '/', ')', '(', '.', ';', '[', ']', '"', '|', ':' };
|
||||
var args = (string)filter.Args;
|
||||
var argsList = args.ToLower().Split(delimiters, StringSplitOptions.RemoveEmptyEntries);
|
||||
var validList = argsList.ToList();
|
||||
var validIntersect = validList.Intersect(row.ToString().ToLower().Split(delimiters, StringSplitOptions.RemoveEmptyEntries)).ToList();
|
||||
row = string.Join(", ", validIntersect);
|
||||
_logger.Debug($"CardigannIndexer ({_definition.Id}): row strdump: {row}");
|
||||
break;
|
||||
default:
|
||||
_logger.Error(string.Format("CardigannIndexer ({0}): Unsupported rows filter: {1}", _definition.Id, filter.Name));
|
||||
_logger.Error($"CardigannIndexer ({_definition.Id}): Unsupported rows filter: {filter.Name}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,12 @@ using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using AngleSharp.Html.Dom;
|
||||
using AngleSharp.Html.Parser;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Indexers.Definitions.Cardigann;
|
||||
@@ -29,22 +29,24 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
protected IHtmlDocument landingResultDocument;
|
||||
protected override string SiteLink => ResolveSiteLink();
|
||||
|
||||
private readonly TimeSpan _rateLimit;
|
||||
|
||||
public CardigannRequestGenerator(IConfigService configService,
|
||||
CardigannDefinition definition,
|
||||
Logger logger)
|
||||
Logger logger,
|
||||
TimeSpan rateLimit)
|
||||
: base(configService, definition, logger)
|
||||
{
|
||||
_rateLimit = rateLimit;
|
||||
}
|
||||
|
||||
public Func<IDictionary<string, string>> GetCookies { get; set; }
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
{
|
||||
_logger.Trace("Getting Movie search");
|
||||
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
var variables = GetQueryVariableDefaults(searchCriteria);
|
||||
|
||||
variables[".Query.Movie"] = null;
|
||||
@@ -56,17 +58,13 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
variables[".Query.TraktID"] = searchCriteria.TraktId?.ToString() ?? null;
|
||||
variables[".Query.DoubanID"] = searchCriteria.DoubanId?.ToString() ?? null;
|
||||
|
||||
pageableRequests.Add(GetRequest(variables));
|
||||
|
||||
return pageableRequests;
|
||||
return GetRequest(variables, searchCriteria);
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
{
|
||||
_logger.Trace("Getting Music search");
|
||||
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
var variables = GetQueryVariableDefaults(searchCriteria);
|
||||
|
||||
variables[".Query.Album"] = searchCriteria.Album;
|
||||
@@ -76,17 +74,13 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
variables[".Query.Year"] = searchCriteria.Year?.ToString() ?? null;
|
||||
variables[".Query.Track"] = searchCriteria.Track;
|
||||
|
||||
pageableRequests.Add(GetRequest(variables));
|
||||
|
||||
return pageableRequests;
|
||||
return GetRequest(variables, searchCriteria);
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
{
|
||||
_logger.Trace("Getting TV search");
|
||||
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
var variables = GetQueryVariableDefaults(searchCriteria);
|
||||
|
||||
variables[".Query.Series"] = null;
|
||||
@@ -104,17 +98,13 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
variables[".Query.DoubanID"] = searchCriteria.DoubanId?.ToString() ?? null;
|
||||
variables[".Query.Episode"] = searchCriteria.EpisodeSearchString;
|
||||
|
||||
pageableRequests.Add(GetRequest(variables));
|
||||
|
||||
return pageableRequests;
|
||||
return GetRequest(variables, searchCriteria);
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
{
|
||||
_logger.Trace("Getting Book search");
|
||||
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
var variables = GetQueryVariableDefaults(searchCriteria);
|
||||
|
||||
variables[".Query.Author"] = searchCriteria.Author;
|
||||
@@ -123,22 +113,16 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
variables[".Query.Publisher"] = searchCriteria.Publisher;
|
||||
variables[".Query.Year"] = searchCriteria.Year?.ToString() ?? null;
|
||||
|
||||
pageableRequests.Add(GetRequest(variables));
|
||||
|
||||
return pageableRequests;
|
||||
return GetRequest(variables, searchCriteria);
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
{
|
||||
_logger.Trace("Getting Basic search");
|
||||
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
var variables = GetQueryVariableDefaults(searchCriteria);
|
||||
|
||||
pageableRequests.Add(GetRequest(variables));
|
||||
|
||||
return pageableRequests;
|
||||
return GetRequest(variables, searchCriteria);
|
||||
}
|
||||
|
||||
private Dictionary<string, object> GetQueryVariableDefaults(SearchCriteriaBase searchCriteria)
|
||||
@@ -148,8 +132,8 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
variables[".Query.Type"] = searchCriteria.SearchType;
|
||||
variables[".Query.Q"] = searchCriteria.SearchTerm;
|
||||
variables[".Query.Categories"] = searchCriteria.Categories;
|
||||
variables[".Query.Limit"] = searchCriteria.Limit?.ToString() ?? null;
|
||||
variables[".Query.Offset"] = searchCriteria.Offset?.ToString() ?? null;
|
||||
variables[".Query.Limit"] = searchCriteria.Limit.ToString() ?? null;
|
||||
variables[".Query.Offset"] = searchCriteria.Offset.ToString() ?? null;
|
||||
variables[".Query.Extended"] = null;
|
||||
variables[".Query.APIKey"] = null;
|
||||
variables[".Query.Genre"] = null;
|
||||
@@ -190,6 +174,9 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
{
|
||||
var login = _definition.Login;
|
||||
|
||||
var variables = GetBaseTemplateVariables();
|
||||
var headers = ParseCustomHeaders(_definition.Login?.Headers ?? _definition.Search?.Headers, variables);
|
||||
|
||||
if (login.Method == "post")
|
||||
{
|
||||
var pairs = new Dictionary<string, string>();
|
||||
@@ -218,15 +205,26 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
requestBuilder.AddFormParameter(pair.Key, pair.Value);
|
||||
}
|
||||
|
||||
requestBuilder.Headers.Add("Referer", SiteLink);
|
||||
Cookies = null;
|
||||
if (login.Cookies != null)
|
||||
{
|
||||
Cookies = CookieUtil.CookieHeaderToDictionary(string.Join("; ", login.Cookies));
|
||||
}
|
||||
|
||||
var response = await HttpClient.ExecuteProxiedAsync(requestBuilder.Build(), Definition);
|
||||
var request = requestBuilder
|
||||
.SetCookies(Cookies ?? new Dictionary<string, string>())
|
||||
.SetHeaders(headers ?? new Dictionary<string, string>())
|
||||
.SetHeader("Referer", SiteLink)
|
||||
.WithRateLimit(_rateLimit.TotalSeconds)
|
||||
.Build();
|
||||
|
||||
var response = await HttpClient.ExecuteProxiedAsync(request, Definition);
|
||||
|
||||
Cookies = response.GetCookies();
|
||||
|
||||
CheckForError(response, login.Error);
|
||||
|
||||
CookiesUpdater(Cookies, DateTime.Now + TimeSpan.FromDays(30));
|
||||
CookiesUpdater(Cookies, DateTime.Now.AddDays(30));
|
||||
}
|
||||
else if (login.Method == "form")
|
||||
{
|
||||
@@ -235,13 +233,9 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
var queryCollection = new NameValueCollection();
|
||||
var pairs = new Dictionary<string, string>();
|
||||
|
||||
var formSelector = login.Form;
|
||||
if (formSelector == null)
|
||||
{
|
||||
formSelector = "form";
|
||||
}
|
||||
var formSelector = login.Form ?? "form";
|
||||
|
||||
// landingResultDocument might not be initiated if the login is caused by a relogin during a query
|
||||
// landingResultDocument might not be initiated if the login is caused by a re-login during a query
|
||||
if (landingResultDocument == null)
|
||||
{
|
||||
await GetConfigurationForSetup(true);
|
||||
@@ -273,11 +267,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
continue;
|
||||
}
|
||||
|
||||
var value = input.GetAttribute("value");
|
||||
if (value == null)
|
||||
{
|
||||
value = "";
|
||||
}
|
||||
var value = input.GetAttribute("value") ?? "";
|
||||
|
||||
pairs[name] = value;
|
||||
}
|
||||
@@ -356,11 +346,14 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
Encoding = _encoding
|
||||
};
|
||||
|
||||
requestBuilder.SetCookies(Cookies);
|
||||
var request = requestBuilder
|
||||
.SetCookies(Cookies)
|
||||
.SetHeaders(headers ?? new Dictionary<string, string>())
|
||||
.SetHeader("Referer", loginUrl)
|
||||
.WithRateLimit(_rateLimit.TotalSeconds)
|
||||
.Build();
|
||||
|
||||
requestBuilder.Headers.Add("Referer", loginUrl);
|
||||
|
||||
var simpleCaptchaResult = await HttpClient.ExecuteProxiedAsync(requestBuilder.Build(), Definition);
|
||||
var simpleCaptchaResult = await HttpClient.ExecuteProxiedAsync(request, Definition);
|
||||
|
||||
var simpleCaptchaJSON = JObject.Parse(simpleCaptchaResult.Content);
|
||||
var captchaSelection = simpleCaptchaJSON["images"][0]["hash"].ToString();
|
||||
@@ -398,7 +391,6 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
var enctype = form.GetAttribute("enctype");
|
||||
if (enctype == "multipart/form-data")
|
||||
{
|
||||
var headers = new Dictionary<string, string>();
|
||||
var boundary = "---------------------------" + DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1)).TotalSeconds.ToString().Replace(".", "");
|
||||
var bodyParts = new List<string>();
|
||||
|
||||
@@ -424,21 +416,18 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
Encoding = _encoding
|
||||
};
|
||||
|
||||
requestBuilder.Headers.Add("Referer", SiteLink);
|
||||
|
||||
requestBuilder.SetCookies(Cookies);
|
||||
|
||||
foreach (var pair in pairs)
|
||||
{
|
||||
requestBuilder.AddFormParameter(pair.Key, pair.Value);
|
||||
}
|
||||
|
||||
foreach (var header in headers)
|
||||
{
|
||||
requestBuilder.SetHeader(header.Key, header.Value);
|
||||
}
|
||||
var request = requestBuilder
|
||||
.SetCookies(Cookies)
|
||||
.SetHeaders(headers ?? new Dictionary<string, string>())
|
||||
.SetHeader("Referer", SiteLink)
|
||||
.WithRateLimit(_rateLimit.TotalSeconds)
|
||||
.Build();
|
||||
|
||||
var request = requestBuilder.Build();
|
||||
request.SetContent(body);
|
||||
|
||||
loginResult = await HttpClient.ExecuteProxiedAsync(request, Definition);
|
||||
@@ -454,26 +443,30 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
Encoding = _encoding
|
||||
};
|
||||
|
||||
requestBuilder.SetCookies(Cookies);
|
||||
requestBuilder.Headers.Add("Referer", loginUrl);
|
||||
|
||||
foreach (var pair in pairs)
|
||||
{
|
||||
requestBuilder.AddFormParameter(pair.Key, pair.Value);
|
||||
}
|
||||
|
||||
loginResult = await HttpClient.ExecuteProxiedAsync(requestBuilder.Build(), Definition);
|
||||
var request = requestBuilder
|
||||
.SetCookies(Cookies)
|
||||
.SetHeaders(headers ?? new Dictionary<string, string>())
|
||||
.SetHeader("Referer", loginUrl)
|
||||
.WithRateLimit(_rateLimit.TotalSeconds)
|
||||
.Build();
|
||||
|
||||
loginResult = await HttpClient.ExecuteProxiedAsync(request, Definition);
|
||||
}
|
||||
|
||||
Cookies = loginResult.GetCookies();
|
||||
CheckForError(loginResult, login.Error);
|
||||
CookiesUpdater(Cookies, DateTime.Now + TimeSpan.FromDays(30));
|
||||
CookiesUpdater(Cookies, DateTime.Now.AddDays(30));
|
||||
}
|
||||
else if (login.Method == "cookie")
|
||||
{
|
||||
CookiesUpdater(null, null);
|
||||
Settings.ExtraFieldData.TryGetValue("cookie", out var cookies);
|
||||
CookiesUpdater(CookieUtil.CookieHeaderToDictionary((string)cookies), DateTime.Now + TimeSpan.FromDays(30));
|
||||
CookiesUpdater(CookieUtil.CookieHeaderToDictionary((string)cookies), DateTime.Now.AddDays(30));
|
||||
}
|
||||
else if (login.Method == "get")
|
||||
{
|
||||
@@ -496,15 +489,19 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
Encoding = _encoding
|
||||
};
|
||||
|
||||
requestBuilder.Headers.Add("Referer", SiteLink);
|
||||
var request = requestBuilder
|
||||
.SetHeaders(headers ?? new Dictionary<string, string>())
|
||||
.SetHeader("Referer", SiteLink)
|
||||
.WithRateLimit(_rateLimit.TotalSeconds)
|
||||
.Build();
|
||||
|
||||
var response = await HttpClient.ExecuteProxiedAsync(requestBuilder.Build(), Definition);
|
||||
var response = await HttpClient.ExecuteProxiedAsync(request, Definition);
|
||||
|
||||
Cookies = response.GetCookies();
|
||||
|
||||
CheckForError(response, login.Error);
|
||||
|
||||
CookiesUpdater(Cookies, DateTime.Now + TimeSpan.FromDays(30));
|
||||
CookiesUpdater(Cookies, DateTime.Now.AddDays(30));
|
||||
}
|
||||
else if (login.Method == "oneurl")
|
||||
{
|
||||
@@ -521,19 +518,23 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
Encoding = _encoding
|
||||
};
|
||||
|
||||
requestBuilder.Headers.Add("Referer", SiteLink);
|
||||
var request = requestBuilder
|
||||
.SetHeaders(headers ?? new Dictionary<string, string>())
|
||||
.SetHeader("Referer", SiteLink)
|
||||
.WithRateLimit(_rateLimit.TotalSeconds)
|
||||
.Build();
|
||||
|
||||
var response = await HttpClient.ExecuteProxiedAsync(requestBuilder.Build(), Definition);
|
||||
var response = await HttpClient.ExecuteProxiedAsync(request, Definition);
|
||||
|
||||
Cookies = response.GetCookies();
|
||||
|
||||
CheckForError(response, login.Error);
|
||||
|
||||
CookiesUpdater(Cookies, DateTime.Now + TimeSpan.FromDays(30));
|
||||
CookiesUpdater(Cookies, DateTime.Now.AddDays(30));
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotImplementedException("Login method " + login.Method + " not implemented");
|
||||
throw new NotImplementedException($"Login method {login.Method} not implemented");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -578,15 +579,11 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
return null;
|
||||
}
|
||||
|
||||
var variables = GetBaseTemplateVariables();
|
||||
var headers = ParseCustomHeaders(_definition.Login?.Headers ?? _definition.Search?.Headers, variables);
|
||||
|
||||
var loginUrl = ResolvePath(login.Path);
|
||||
|
||||
Cookies = null;
|
||||
|
||||
if (login.Cookies != null)
|
||||
{
|
||||
Cookies = CookieUtil.CookieHeaderToDictionary(string.Join("; ", login.Cookies));
|
||||
}
|
||||
|
||||
var requestBuilder = new HttpRequestBuilder(loginUrl.AbsoluteUri)
|
||||
{
|
||||
LogResponseContent = true,
|
||||
@@ -594,14 +591,18 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
Encoding = _encoding
|
||||
};
|
||||
|
||||
requestBuilder.Headers.Add("Referer", SiteLink);
|
||||
|
||||
if (Cookies != null)
|
||||
Cookies = null;
|
||||
if (login.Cookies != null)
|
||||
{
|
||||
requestBuilder.SetCookies(Cookies);
|
||||
Cookies = CookieUtil.CookieHeaderToDictionary(string.Join("; ", login.Cookies));
|
||||
}
|
||||
|
||||
var request = requestBuilder.Build();
|
||||
var request = requestBuilder
|
||||
.SetCookies(Cookies ?? new Dictionary<string, string>())
|
||||
.SetHeaders(headers ?? new Dictionary<string, string>())
|
||||
.SetHeader("Referer", SiteLink)
|
||||
.WithRateLimit(_rateLimit.TotalSeconds)
|
||||
.Build();
|
||||
|
||||
landingResult = await HttpClient.ExecuteProxiedAsync(request, Definition);
|
||||
|
||||
@@ -634,6 +635,9 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
{
|
||||
var captcha = login.Captcha;
|
||||
|
||||
var variables = GetBaseTemplateVariables();
|
||||
var headers = ParseCustomHeaders(_definition.Login?.Headers ?? _definition.Search?.Headers, variables);
|
||||
|
||||
if (captcha.Type == "image")
|
||||
{
|
||||
var captchaElement = landingResultDocument.QuerySelector(captcha.Selector);
|
||||
@@ -644,8 +648,10 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
|
||||
var request = new HttpRequestBuilder(captchaUrl.ToString())
|
||||
.SetCookies(landingResult.GetCookies())
|
||||
.SetHeaders(headers ?? new Dictionary<string, string>())
|
||||
.SetHeader("Referer", loginUrl.AbsoluteUri)
|
||||
.SetEncoding(_encoding)
|
||||
.WithRateLimit(_rateLimit.TotalSeconds)
|
||||
.Build();
|
||||
|
||||
var response = await HttpClient.ExecuteProxiedAsync(request, Definition);
|
||||
@@ -656,10 +662,8 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
ImageData = response.ResponseData
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Debug("CardigannIndexer ({0}): No captcha image found", _definition.Id);
|
||||
}
|
||||
|
||||
_logger.Debug("CardigannIndexer ({0}): No captcha image found", _definition.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -689,8 +693,6 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
{
|
||||
var requestLinkStr = ResolvePath(ApplyGoTemplateText(request.Path, variables)).ToString();
|
||||
|
||||
_logger.Debug("CardigannIndexer ({0}): handleRequest() requestLinkStr= {1}", _definition.Id, requestLinkStr);
|
||||
|
||||
Dictionary<string, string> pairs = null;
|
||||
var queryCollection = new NameValueCollection();
|
||||
|
||||
@@ -724,25 +726,36 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
requestLinkStr += queryCollection.GetQueryString(_encoding, separator: request.Queryseparator);
|
||||
}
|
||||
|
||||
var httpRequest = new HttpRequestBuilder(requestLinkStr)
|
||||
.SetCookies(Cookies ?? new Dictionary<string, string>())
|
||||
.SetEncoding(_encoding)
|
||||
.SetHeader("Referer", referer);
|
||||
|
||||
httpRequest.Method = method;
|
||||
var httpRequestBuilder = new HttpRequestBuilder(requestLinkStr)
|
||||
{
|
||||
Method = method,
|
||||
Encoding = _encoding
|
||||
};
|
||||
|
||||
// Add form data for POST requests
|
||||
if (method == HttpMethod.Post)
|
||||
{
|
||||
foreach (var param in pairs)
|
||||
{
|
||||
httpRequest.AddFormParameter(param.Key, param.Value);
|
||||
httpRequestBuilder.AddFormParameter(param.Key, param.Value);
|
||||
}
|
||||
}
|
||||
|
||||
var response = await HttpClient.ExecuteProxiedAsync(httpRequest.Build(), Definition);
|
||||
var headers = ParseCustomHeaders(_definition.Download?.Headers ?? _definition.Search?.Headers, variables);
|
||||
|
||||
var httpRequest = httpRequestBuilder
|
||||
.SetCookies(Cookies ?? new Dictionary<string, string>())
|
||||
.SetHeaders(headers ?? new Dictionary<string, string>())
|
||||
.SetHeader("Referer", referer)
|
||||
.WithRateLimit(_rateLimit.TotalSeconds)
|
||||
.Build();
|
||||
|
||||
_logger.Debug("CardigannIndexer ({0}): handleRequest() httpRequest={1}", _definition.Id, httpRequest);
|
||||
|
||||
var response = await HttpClient.ExecuteProxiedAsync(httpRequest, Definition);
|
||||
|
||||
_logger.Debug("CardigannIndexer ({0}): handleRequest() remote server returned {1}", _definition.Id, response.StatusCode);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -750,11 +763,10 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
{
|
||||
Cookies = GetCookies();
|
||||
var method = HttpMethod.Get;
|
||||
var headers = new Dictionary<string, string>();
|
||||
|
||||
var variables = GetBaseTemplateVariables();
|
||||
AddTemplateVariablesFromUri(variables, link, ".DownloadUri");
|
||||
headers = ParseCustomHeaders(_definition.Search?.Headers, variables);
|
||||
var headers = ParseCustomHeaders(_definition.Download?.Headers ?? _definition.Search?.Headers, variables);
|
||||
|
||||
if (_definition.Download != null)
|
||||
{
|
||||
@@ -766,6 +778,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
.SetCookies(Cookies ?? new Dictionary<string, string>())
|
||||
.SetHeaders(headers ?? new Dictionary<string, string>())
|
||||
.SetEncoding(_encoding)
|
||||
.WithRateLimit(_rateLimit.TotalSeconds)
|
||||
.Build();
|
||||
|
||||
request.AllowAutoRedirect = true;
|
||||
@@ -791,7 +804,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!download.Infohash.UseBeforeResponse || download.Before == null || response == null)
|
||||
if (!download.Infohash.Usebeforeresponse || download.Before == null || response == null)
|
||||
{
|
||||
response = await HttpClient.ExecuteProxiedAsync(request, Definition);
|
||||
}
|
||||
@@ -799,13 +812,13 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
var hash = MatchSelector(response, download.Infohash.Hash, variables);
|
||||
if (hash == null)
|
||||
{
|
||||
throw new CardigannException($"InfoHash selectors didn't match");
|
||||
throw new CardigannException("InfoHash selectors didn't match hash.");
|
||||
}
|
||||
|
||||
var title = MatchSelector(response, download.Infohash.Title, variables);
|
||||
if (title == null)
|
||||
{
|
||||
throw new CardigannException($"InfoHash selectors didn't match");
|
||||
throw new CardigannException("InfoHash selectors didn't match title.");
|
||||
}
|
||||
|
||||
var magnet = MagnetLinkBuilder.BuildPublicMagnetLink(hash, title);
|
||||
@@ -837,7 +850,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
|
||||
try
|
||||
{
|
||||
if (!selector.UseBeforeResponse || download.Before == null || response == null)
|
||||
if (!selector.Usebeforeresponse || download.Before == null || response == null)
|
||||
{
|
||||
response = await HttpClient.ExecuteProxiedAsync(request, Definition);
|
||||
}
|
||||
@@ -856,6 +869,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
.SetCookies(Cookies ?? new Dictionary<string, string>())
|
||||
.SetHeaders(headers ?? new Dictionary<string, string>())
|
||||
.SetEncoding(_encoding)
|
||||
.WithRateLimit(_rateLimit.TotalSeconds)
|
||||
.Build();
|
||||
|
||||
response = await HttpClient.ExecuteProxiedAsync(testLinkRequest, Definition);
|
||||
@@ -875,6 +889,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
.SetCookies(Cookies ?? new Dictionary<string, string>())
|
||||
.SetHeaders(headers ?? new Dictionary<string, string>())
|
||||
.SetEncoding(_encoding)
|
||||
.WithRateLimit(_rateLimit.TotalSeconds)
|
||||
.Build();
|
||||
|
||||
selectorDownloadRequest.Method = method;
|
||||
@@ -895,6 +910,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
.SetCookies(Cookies ?? new Dictionary<string, string>())
|
||||
.SetHeaders(headers ?? new Dictionary<string, string>())
|
||||
.SetEncoding(_encoding)
|
||||
.WithRateLimit(_rateLimit.TotalSeconds)
|
||||
.Build();
|
||||
|
||||
downloadRequest.Method = method;
|
||||
@@ -907,8 +923,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
var selectorText = ApplyGoTemplateText(selector.Selector, variables);
|
||||
var parser = new HtmlParser();
|
||||
|
||||
var results = response.Content;
|
||||
var resultDocument = parser.ParseDocument(results);
|
||||
var resultDocument = parser.ParseDocument(response.Content);
|
||||
|
||||
var element = resultDocument.QuerySelector(selectorText);
|
||||
if (element == null)
|
||||
@@ -981,7 +996,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
return false;
|
||||
}
|
||||
|
||||
private IEnumerable<IndexerRequest> GetRequest(Dictionary<string, object> variables)
|
||||
private IEnumerable<IndexerRequest> GetRequest(Dictionary<string, object> variables, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
var search = _definition.Search;
|
||||
|
||||
@@ -1012,115 +1027,154 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
variables[".Query.Keywords"] = string.Join(" ", keywordTokens);
|
||||
variables[".Keywords"] = ApplyFilters((string)variables[".Query.Keywords"], search.Keywordsfilters, variables);
|
||||
|
||||
var pageSize = search.PageSize;
|
||||
var minPage = 0;
|
||||
var maxPage = 0;
|
||||
|
||||
if (pageSize > 0)
|
||||
{
|
||||
variables[".PageSize"] = pageSize;
|
||||
minPage = (searchCriteria.Offset / pageSize) + search.FirstPageNumber;
|
||||
maxPage = ((searchCriteria.Offset + searchCriteria.Limit - 1) / pageSize) + search.FirstPageNumber;
|
||||
}
|
||||
|
||||
if (pageSize == 0 && searchCriteria.Offset >= 100)
|
||||
{
|
||||
// Indexer doesn't support pagination
|
||||
yield break;
|
||||
}
|
||||
|
||||
// TODO: prepare queries first and then send them parallel
|
||||
var searchPaths = search.Paths;
|
||||
foreach (var searchPath in searchPaths)
|
||||
|
||||
// Grab all pages we will need to return user requested limit and offset
|
||||
for (var page = minPage; page <= maxPage; page++)
|
||||
{
|
||||
// skip path if categories don't match
|
||||
if (searchPath.Categories != null && mappedCategories.Count > 0)
|
||||
variables[".Query.Page"] = page;
|
||||
|
||||
foreach (var searchPath in searchPaths)
|
||||
{
|
||||
var invertMatch = searchPath.Categories[0] == "!";
|
||||
var hasIntersect = mappedCategories.Intersect(searchPath.Categories).Any();
|
||||
if (invertMatch)
|
||||
// skip path if categories don't match
|
||||
if (searchPath.Categories != null && mappedCategories.Count > 0)
|
||||
{
|
||||
hasIntersect = !hasIntersect;
|
||||
}
|
||||
|
||||
if (!hasIntersect)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// build search URL
|
||||
// HttpUtility.UrlPathEncode seems to only encode spaces, we use UrlEncode and replace + with %20 as a workaround
|
||||
var searchUrl = ResolvePath(ApplyGoTemplateText(searchPath.Path, variables, WebUtility.UrlEncode).Replace("+", "%20")).AbsoluteUri;
|
||||
var queryCollection = new List<KeyValuePair<string, string>>();
|
||||
var method = HttpMethod.Get;
|
||||
|
||||
if (string.Equals(searchPath.Method, "post", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
method = HttpMethod.Post;
|
||||
}
|
||||
|
||||
var inputsList = new List<Dictionary<string, string>>();
|
||||
if (searchPath.Inheritinputs)
|
||||
{
|
||||
inputsList.Add(search.Inputs);
|
||||
}
|
||||
|
||||
inputsList.Add(searchPath.Inputs);
|
||||
|
||||
foreach (var inputs in inputsList)
|
||||
{
|
||||
if (inputs != null)
|
||||
{
|
||||
foreach (var input in inputs)
|
||||
var invertMatch = searchPath.Categories[0] == "!";
|
||||
var hasIntersect = mappedCategories.Intersect(searchPath.Categories).Any();
|
||||
if (invertMatch)
|
||||
{
|
||||
if (input.Key == "$raw")
|
||||
hasIntersect = !hasIntersect;
|
||||
}
|
||||
|
||||
if (!hasIntersect)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// build search URL
|
||||
// HttpUtility.UrlPathEncode seems to only encode spaces, we use UrlEncode and replace + with %20 as a workaround
|
||||
var searchUrl = ResolvePath(ApplyGoTemplateText(searchPath.Path, variables, WebUtility.UrlEncode).Replace("+", "%20")).AbsoluteUri;
|
||||
var queryCollection = new List<KeyValuePair<string, string>>();
|
||||
var method = HttpMethod.Get;
|
||||
|
||||
if (string.Equals(searchPath.Method, "post", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
method = HttpMethod.Post;
|
||||
}
|
||||
|
||||
var inputsList = new List<Dictionary<string, string>>();
|
||||
if (searchPath.Inheritinputs)
|
||||
{
|
||||
inputsList.Add(search.Inputs);
|
||||
}
|
||||
|
||||
inputsList.Add(searchPath.Inputs);
|
||||
|
||||
foreach (var inputs in inputsList)
|
||||
{
|
||||
if (inputs != null)
|
||||
{
|
||||
foreach (var input in inputs)
|
||||
{
|
||||
var rawStr = ApplyGoTemplateText(input.Value, variables, WebUtility.UrlEncode);
|
||||
foreach (var part in rawStr.Split('&'))
|
||||
if (input.Key == "$raw")
|
||||
{
|
||||
var parts = part.Split(new char[] { '=' }, 2);
|
||||
var key = parts[0];
|
||||
if (key.Length == 0)
|
||||
var rawStr = ApplyGoTemplateText(input.Value, variables, WebUtility.UrlEncode);
|
||||
foreach (var part in rawStr.Split('&'))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var parts = part.Split(new char[] { '=' }, 2);
|
||||
var key = parts[0];
|
||||
if (key.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var value = "";
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
value = parts[1];
|
||||
}
|
||||
var value = "";
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
value = parts[1];
|
||||
}
|
||||
|
||||
queryCollection.Add(key, value);
|
||||
queryCollection.Add(key, value);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var inputValue = ApplyGoTemplateText(input.Value, variables);
|
||||
|
||||
if (inputValue.IsNotNullOrWhiteSpace() || search.AllowEmptyInputs)
|
||||
{
|
||||
queryCollection.Add(input.Key, inputValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
queryCollection.Add(input.Key, ApplyGoTemplateText(input.Value, variables));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (method == HttpMethod.Get)
|
||||
{
|
||||
if (queryCollection.Count > 0)
|
||||
if (method == HttpMethod.Get)
|
||||
{
|
||||
searchUrl += "?" + queryCollection.GetQueryString(_encoding);
|
||||
if (queryCollection.Count > 0)
|
||||
{
|
||||
searchUrl += "?" + queryCollection.GetQueryString(_encoding);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Info($"Adding request: {searchUrl}");
|
||||
_logger.Info($"Adding request: {searchUrl}");
|
||||
|
||||
var requestbuilder = new HttpRequestBuilder(searchUrl);
|
||||
|
||||
requestbuilder.Method = method;
|
||||
|
||||
// Add FormData for searchs that POST
|
||||
if (method == HttpMethod.Post)
|
||||
{
|
||||
foreach (var param in queryCollection)
|
||||
var requestBuilder = new HttpRequestBuilder(searchUrl)
|
||||
{
|
||||
requestbuilder.AddFormParameter(param.Key, param.Value);
|
||||
Method = method,
|
||||
Encoding = _encoding
|
||||
};
|
||||
|
||||
// Add FormData for searchs that POST
|
||||
if (method == HttpMethod.Post)
|
||||
{
|
||||
foreach (var param in queryCollection)
|
||||
{
|
||||
requestBuilder.AddFormParameter(param.Key, param.Value);
|
||||
}
|
||||
}
|
||||
|
||||
// send HTTP request
|
||||
if (search.Headers != null)
|
||||
{
|
||||
var headers = ParseCustomHeaders(search.Headers, variables);
|
||||
requestBuilder.SetHeaders(headers ?? new Dictionary<string, string>());
|
||||
}
|
||||
|
||||
var request = requestBuilder
|
||||
.WithRateLimit(_rateLimit.TotalSeconds)
|
||||
.Build();
|
||||
|
||||
var cardigannRequest = new CardigannRequest(request, variables, searchPath)
|
||||
{
|
||||
HttpRequest =
|
||||
{
|
||||
AllowAutoRedirect = searchPath.Followredirect
|
||||
}
|
||||
};
|
||||
|
||||
yield return cardigannRequest;
|
||||
}
|
||||
|
||||
// send HTTP request
|
||||
if (search.Headers != null)
|
||||
{
|
||||
var headers = ParseCustomHeaders(search.Headers, variables);
|
||||
requestbuilder.SetHeaders(headers ?? new Dictionary<string, string>());
|
||||
}
|
||||
|
||||
var request = new CardigannRequest(requestbuilder.SetEncoding(_encoding).Build(), variables, searchPath);
|
||||
|
||||
request.HttpRequest.AllowAutoRedirect = searchPath.Followredirect;
|
||||
|
||||
yield return request;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
@@ -15,139 +16,128 @@ public class FileListRequestGenerator : IIndexerRequestGenerator
|
||||
public Func<IDictionary<string, string>> GetCookies { get; set; }
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
var parameters = GetDefaultParameters();
|
||||
var parameters = new NameValueCollection();
|
||||
|
||||
if (searchCriteria.ImdbId.IsNotNullOrWhiteSpace() || searchCriteria.SearchTerm.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
parameters.Add("action", "search-torrents");
|
||||
parameters.Set("action", "search-torrents");
|
||||
|
||||
if (searchCriteria.ImdbId.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
parameters.Add("type", "imdb");
|
||||
parameters.Add("query", searchCriteria.FullImdbId);
|
||||
parameters.Set("type", "imdb");
|
||||
parameters.Set("query", searchCriteria.FullImdbId);
|
||||
}
|
||||
else if (searchCriteria.SearchTerm.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
parameters.Add("type", "name");
|
||||
parameters.Add("query", searchCriteria.SanitizedSearchTerm.Trim());
|
||||
parameters.Set("type", "name");
|
||||
parameters.Set("query", searchCriteria.SanitizedSearchTerm.Trim());
|
||||
}
|
||||
|
||||
if (searchCriteria.Season.HasValue)
|
||||
{
|
||||
parameters.Add("season", searchCriteria.Season.ToString());
|
||||
parameters.Add("episode", searchCriteria.Episode);
|
||||
parameters.Set("season", searchCriteria.Season.ToString());
|
||||
}
|
||||
|
||||
if (searchCriteria.Episode.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
parameters.Set("episode", searchCriteria.Episode);
|
||||
}
|
||||
}
|
||||
|
||||
pageableRequests.Add(GetRequest(searchCriteria, parameters));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests(searchCriteria, parameters);
|
||||
}
|
||||
|
||||
public virtual IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
var parameters = GetDefaultParameters();
|
||||
var parameters = new NameValueCollection();
|
||||
|
||||
if (searchCriteria.ImdbId.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
parameters.Add("action", "search-torrents");
|
||||
parameters.Add("type", "imdb");
|
||||
parameters.Add("query", searchCriteria.FullImdbId);
|
||||
parameters.Set("action", "search-torrents");
|
||||
parameters.Set("type", "imdb");
|
||||
parameters.Set("query", searchCriteria.FullImdbId);
|
||||
}
|
||||
else if (searchCriteria.SearchTerm.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
parameters.Add("action", "search-torrents");
|
||||
parameters.Add("type", "name");
|
||||
parameters.Add("query", searchCriteria.SanitizedSearchTerm.Trim());
|
||||
parameters.Set("action", "search-torrents");
|
||||
parameters.Set("type", "name");
|
||||
parameters.Set("query", searchCriteria.SanitizedSearchTerm.Trim());
|
||||
}
|
||||
|
||||
pageableRequests.Add(GetRequest(searchCriteria, parameters));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests(searchCriteria, parameters);
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
var parameters = GetDefaultParameters();
|
||||
var parameters = new NameValueCollection();
|
||||
|
||||
if (searchCriteria.SearchTerm.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
parameters.Add("action", "search-torrents");
|
||||
parameters.Add("type", "name");
|
||||
parameters.Add("query", searchCriteria.SanitizedSearchTerm.Trim());
|
||||
parameters.Set("action", "search-torrents");
|
||||
parameters.Set("type", "name");
|
||||
parameters.Set("query", searchCriteria.SanitizedSearchTerm.Trim());
|
||||
}
|
||||
|
||||
pageableRequests.Add(GetRequest(searchCriteria, parameters));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests(searchCriteria, parameters);
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
var parameters = GetDefaultParameters();
|
||||
var parameters = new NameValueCollection();
|
||||
|
||||
if (searchCriteria.SearchTerm.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
parameters.Add("action", "search-torrents");
|
||||
parameters.Add("type", "name");
|
||||
parameters.Add("query", searchCriteria.SanitizedSearchTerm.Trim());
|
||||
parameters.Set("action", "search-torrents");
|
||||
parameters.Set("type", "name");
|
||||
parameters.Set("query", searchCriteria.SanitizedSearchTerm.Trim());
|
||||
}
|
||||
|
||||
pageableRequests.Add(GetRequest(searchCriteria, parameters));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests(searchCriteria, parameters);
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
var parameters = GetDefaultParameters();
|
||||
var parameters = new NameValueCollection();
|
||||
|
||||
if (searchCriteria.SearchTerm.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
parameters.Add("action", "search-torrents");
|
||||
parameters.Add("type", "name");
|
||||
parameters.Add("query", searchCriteria.SanitizedSearchTerm.Trim());
|
||||
parameters.Set("action", "search-torrents");
|
||||
parameters.Set("type", "name");
|
||||
parameters.Set("query", searchCriteria.SanitizedSearchTerm.Trim());
|
||||
}
|
||||
|
||||
pageableRequests.Add(GetRequest(searchCriteria, parameters));
|
||||
|
||||
return pageableRequests;
|
||||
return GetPagedRequests(searchCriteria, parameters);
|
||||
}
|
||||
|
||||
private IEnumerable<IndexerRequest> GetRequest(SearchCriteriaBase searchCriteria, NameValueCollection parameters)
|
||||
private IEnumerable<IndexerRequest> GetPagedRequests(SearchCriteriaBase searchCriteria, NameValueCollection parameters)
|
||||
{
|
||||
if (parameters.Get("action") is null)
|
||||
{
|
||||
parameters.Add("action", "latest-torrents");
|
||||
parameters.Set("action", "latest-torrents");
|
||||
}
|
||||
|
||||
parameters.Add("category", string.Join(",", Capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories)));
|
||||
|
||||
var searchUrl = $"{Settings.BaseUrl.TrimEnd('/')}/api.php?{parameters.GetQueryString()}";
|
||||
|
||||
yield return new IndexerRequest(searchUrl, HttpAccept.Json);
|
||||
}
|
||||
|
||||
private NameValueCollection GetDefaultParameters()
|
||||
{
|
||||
var parameters = new NameValueCollection
|
||||
if (searchCriteria.Categories != null && searchCriteria.Categories.Any())
|
||||
{
|
||||
{ "username", Settings.Username.Trim() },
|
||||
{ "passkey", Settings.Passkey.Trim() }
|
||||
};
|
||||
parameters.Set("category", string.Join(",", Capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories)));
|
||||
}
|
||||
|
||||
if (Settings.FreeleechOnly)
|
||||
{
|
||||
parameters.Add("freeleech", "1");
|
||||
parameters.Set("freeleech", "1");
|
||||
}
|
||||
|
||||
return parameters;
|
||||
var searchUrl = $"{Settings.BaseUrl.TrimEnd('/')}/api.php?{parameters.GetQueryString()}";
|
||||
|
||||
var request = new IndexerRequest(searchUrl, HttpAccept.Json)
|
||||
{
|
||||
HttpRequest =
|
||||
{
|
||||
Credentials = new BasicNetworkCredential(Settings.Username.Trim(), Settings.Passkey.Trim())
|
||||
}
|
||||
};
|
||||
|
||||
yield return request;
|
||||
}
|
||||
}
|
||||
|
||||
323
src/NzbDrone.Core/Indexers/Definitions/FunFile.cs
Normal file
323
src/NzbDrone.Core/Indexers/Definitions/FunFile.cs
Normal file
@@ -0,0 +1,323 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using AngleSharp.Html.Parser;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Indexers.Exceptions;
|
||||
using NzbDrone.Core.Indexers.Settings;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions;
|
||||
|
||||
public class FunFile : TorrentIndexerBase<UserPassTorrentBaseSettings>
|
||||
{
|
||||
public override string Name => "FunFile";
|
||||
public override string[] IndexerUrls => new[] { "https://www.funfile.org/" };
|
||||
public override string Description => "FunFile is a general tracker";
|
||||
public override string Language => "en-US";
|
||||
public override Encoding Encoding => Encoding.GetEncoding("iso-8859-1");
|
||||
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
|
||||
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
|
||||
public override IndexerCapabilities Capabilities => SetCapabilities();
|
||||
|
||||
public FunFile(IIndexerHttpClient httpClient,
|
||||
IEventAggregator eventAggregator,
|
||||
IIndexerStatusService indexerStatusService,
|
||||
IConfigService configService,
|
||||
Logger logger)
|
||||
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public override IIndexerRequestGenerator GetRequestGenerator()
|
||||
{
|
||||
return new FunFileRequestGenerator(Settings, Capabilities);
|
||||
}
|
||||
|
||||
public override IParseIndexerResponse GetParser()
|
||||
{
|
||||
return new FunFileParser(Settings, Capabilities.Categories);
|
||||
}
|
||||
|
||||
protected override async Task DoLogin()
|
||||
{
|
||||
var requestBuilder = new HttpRequestBuilder(Settings.BaseUrl + "takelogin.php")
|
||||
{
|
||||
LogResponseContent = true,
|
||||
AllowAutoRedirect = true,
|
||||
Method = HttpMethod.Post
|
||||
};
|
||||
|
||||
var authLoginRequest = requestBuilder
|
||||
.AddFormParameter("username", Settings.Username)
|
||||
.AddFormParameter("password", Settings.Password)
|
||||
.AddFormParameter("returnto", "")
|
||||
.AddFormParameter("login", "Login")
|
||||
.SetHeader("Content-Type", "application/x-www-form-urlencoded")
|
||||
.Build();
|
||||
|
||||
var response = await ExecuteAuth(authLoginRequest);
|
||||
|
||||
if (CheckIfLoginNeeded(response))
|
||||
{
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(response.Content);
|
||||
var errorMessage = dom.QuerySelector("td.mf_content")?.TextContent.Trim();
|
||||
|
||||
throw new IndexerAuthException(errorMessage ?? "Unknown error message, please report.");
|
||||
}
|
||||
|
||||
var cookies = response.GetCookies();
|
||||
UpdateCookies(cookies, DateTime.Now.AddDays(30));
|
||||
|
||||
_logger.Debug("Authentication succeeded.");
|
||||
}
|
||||
|
||||
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
|
||||
{
|
||||
return !httpResponse.Content.Contains("logout.php");
|
||||
}
|
||||
|
||||
private IndexerCapabilities SetCapabilities()
|
||||
{
|
||||
var caps = new IndexerCapabilities
|
||||
{
|
||||
TvSearchParams = new List<TvSearchParam>
|
||||
{
|
||||
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep, TvSearchParam.ImdbId
|
||||
},
|
||||
MovieSearchParams = new List<MovieSearchParam>
|
||||
{
|
||||
MovieSearchParam.Q, MovieSearchParam.ImdbId
|
||||
},
|
||||
MusicSearchParams = new List<MusicSearchParam>
|
||||
{
|
||||
MusicSearchParam.Q
|
||||
},
|
||||
BookSearchParams = new List<BookSearchParam>
|
||||
{
|
||||
BookSearchParam.Q
|
||||
}
|
||||
};
|
||||
|
||||
caps.Categories.AddCategoryMapping(44, NewznabStandardCategory.TVAnime, "Anime");
|
||||
caps.Categories.AddCategoryMapping(22, NewznabStandardCategory.PC, "Applications");
|
||||
caps.Categories.AddCategoryMapping(43, NewznabStandardCategory.AudioAudiobook, "Audio Books");
|
||||
caps.Categories.AddCategoryMapping(27, NewznabStandardCategory.Books, "Ebook");
|
||||
caps.Categories.AddCategoryMapping(4, NewznabStandardCategory.PCGames, "Games");
|
||||
caps.Categories.AddCategoryMapping(40, NewznabStandardCategory.OtherMisc, "Miscellaneous");
|
||||
caps.Categories.AddCategoryMapping(19, NewznabStandardCategory.Movies, "Movies");
|
||||
caps.Categories.AddCategoryMapping(6, NewznabStandardCategory.Audio, "Music");
|
||||
caps.Categories.AddCategoryMapping(31, NewznabStandardCategory.PCMobileOther, "Portable");
|
||||
caps.Categories.AddCategoryMapping(49, NewznabStandardCategory.Other, "Tutorials");
|
||||
caps.Categories.AddCategoryMapping(7, NewznabStandardCategory.TV, "TV");
|
||||
|
||||
return caps;
|
||||
}
|
||||
}
|
||||
|
||||
public class FunFileRequestGenerator : IIndexerRequestGenerator
|
||||
{
|
||||
private readonly UserPassTorrentBaseSettings _settings;
|
||||
private readonly IndexerCapabilities _capabilities;
|
||||
|
||||
public FunFileRequestGenerator(UserPassTorrentBaseSettings settings, IndexerCapabilities capabilities)
|
||||
{
|
||||
_settings = settings;
|
||||
_capabilities = capabilities;
|
||||
}
|
||||
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
{
|
||||
return GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories, searchCriteria.FullImdbId);
|
||||
}
|
||||
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
{
|
||||
return GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories);
|
||||
}
|
||||
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
{
|
||||
return GetPagedRequests($"{searchCriteria.SanitizedTvSearchString}", searchCriteria.Categories, searchCriteria.FullImdbId);
|
||||
}
|
||||
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
{
|
||||
return GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories);
|
||||
}
|
||||
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
{
|
||||
return GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories);
|
||||
}
|
||||
|
||||
private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories, string imdbId = null)
|
||||
{
|
||||
var parameters = new NameValueCollection
|
||||
{
|
||||
{ "cat", "0" },
|
||||
{ "incldead", "1" },
|
||||
{ "showspam", "1" },
|
||||
{ "s_title", "1" }
|
||||
};
|
||||
|
||||
if (imdbId.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
parameters.Set("search", imdbId);
|
||||
parameters.Set("s_desc", "1");
|
||||
}
|
||||
else if (term.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
parameters.Set("search", term);
|
||||
}
|
||||
|
||||
var queryCats = _capabilities.Categories.MapTorznabCapsToTrackers(categories);
|
||||
if (queryCats.Any())
|
||||
{
|
||||
queryCats.ForEach(cat => parameters.Set($"c{cat}", "1"));
|
||||
}
|
||||
|
||||
var searchUrl = _settings.BaseUrl + "browse.php";
|
||||
|
||||
if (parameters.Count > 0)
|
||||
{
|
||||
searchUrl += $"?{parameters.GetQueryString()}";
|
||||
}
|
||||
|
||||
var request = new IndexerRequest(searchUrl, HttpAccept.Html);
|
||||
|
||||
yield return request;
|
||||
}
|
||||
|
||||
public Func<IDictionary<string, string>> GetCookies { get; set; }
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
}
|
||||
|
||||
public class FunFileParser : IParseIndexerResponse
|
||||
{
|
||||
private readonly UserPassTorrentBaseSettings _settings;
|
||||
private readonly IndexerCapabilitiesCategories _categories;
|
||||
|
||||
private readonly List<string> _validTagList = new ()
|
||||
{
|
||||
"action",
|
||||
"adventure",
|
||||
"animation",
|
||||
"biography",
|
||||
"comedy",
|
||||
"crime",
|
||||
"documentary",
|
||||
"drama",
|
||||
"family",
|
||||
"fantasy",
|
||||
"game-show",
|
||||
"history",
|
||||
"home_&_garden",
|
||||
"home_and_garden",
|
||||
"horror",
|
||||
"music",
|
||||
"musical",
|
||||
"mystery",
|
||||
"news",
|
||||
"reality",
|
||||
"reality-tv",
|
||||
"romance",
|
||||
"sci-fi",
|
||||
"science-fiction",
|
||||
"short",
|
||||
"sport",
|
||||
"talk-show",
|
||||
"thriller",
|
||||
"travel",
|
||||
"war",
|
||||
"western"
|
||||
};
|
||||
|
||||
private readonly char[] _delimiters = { ',', ' ', '/', ')', '(', '.', ';', '[', ']', '"', '|', ':' };
|
||||
|
||||
public FunFileParser(UserPassTorrentBaseSettings settings, IndexerCapabilitiesCategories categories)
|
||||
{
|
||||
_settings = settings;
|
||||
_categories = categories;
|
||||
}
|
||||
|
||||
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
||||
{
|
||||
var releaseInfos = new List<TorrentInfo>();
|
||||
|
||||
var parser = new HtmlParser();
|
||||
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)
|
||||
{
|
||||
var qDownloadLink = row.QuerySelector("a[href^=\"download.php\"]");
|
||||
if (qDownloadLink == null)
|
||||
{
|
||||
throw new Exception("Download links not found. Make sure you can download from the website.");
|
||||
}
|
||||
|
||||
var downloadUrl = _settings.BaseUrl + qDownloadLink.GetAttribute("href");
|
||||
|
||||
var qDetailsLink = row.QuerySelector("a[href^=\"details.php?id=\"]");
|
||||
var title = qDetailsLink?.GetAttribute("title")?.Trim();
|
||||
var infoUrl = _settings.BaseUrl + qDetailsLink?.GetAttribute("href")?.Replace("&hit=1", "");
|
||||
|
||||
var categoryLink = row.QuerySelector("a[href^=\"browse.php?cat=\"]")?.GetAttribute("href");
|
||||
var cat = ParseUtil.GetArgumentFromQueryString(categoryLink, "cat");
|
||||
|
||||
var seeders = ParseUtil.CoerceInt(row.Children[9].TextContent);
|
||||
var leechers = ParseUtil.CoerceInt(row.Children[10].TextContent);
|
||||
|
||||
var release = new TorrentInfo
|
||||
{
|
||||
Guid = infoUrl,
|
||||
InfoUrl = infoUrl,
|
||||
DownloadUrl = downloadUrl,
|
||||
Title = title,
|
||||
Categories = _categories.MapTrackerCatToNewznab(cat),
|
||||
Size = ParseUtil.GetBytes(row.Children[7].TextContent),
|
||||
Files = ParseUtil.CoerceInt(row.Children[3].TextContent),
|
||||
Grabs = ParseUtil.CoerceInt(row.Children[8].TextContent),
|
||||
Seeders = seeders,
|
||||
Peers = leechers + seeders,
|
||||
PublishDate = DateTimeUtil.FromTimeAgo(row.Children[5].TextContent),
|
||||
DownloadVolumeFactor = 1,
|
||||
UploadVolumeFactor = 1,
|
||||
MinimumRatio = 1,
|
||||
MinimumSeedTime = 172800 // 48 hours
|
||||
};
|
||||
|
||||
var nextRow = row.NextElementSibling;
|
||||
if (nextRow != null)
|
||||
{
|
||||
var qStats = nextRow.QuerySelector("table > tbody > tr:nth-child(3)");
|
||||
release.UploadVolumeFactor = ParseUtil.CoerceDouble(qStats?.Children[0].TextContent.Replace("X", ""));
|
||||
release.DownloadVolumeFactor = ParseUtil.CoerceDouble(qStats?.Children[1].TextContent.Replace("X", ""));
|
||||
|
||||
release.Description = nextRow.QuerySelector("span[style=\"float:left\"]")?.TextContent.Trim();
|
||||
var genres = release.Description.ToLower().Replace(" & ", "_&_").Replace(" and ", "_and_");
|
||||
|
||||
var releaseGenres = _validTagList.Intersect(genres.Split(_delimiters, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries));
|
||||
release.Genres = releaseGenres.Select(x => x.Replace("_", " ")).ToList();
|
||||
}
|
||||
|
||||
releaseInfos.Add(release);
|
||||
}
|
||||
|
||||
return releaseInfos.ToArray();
|
||||
}
|
||||
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
@@ -56,7 +57,7 @@ public abstract class GazelleBase<TSettings> : TorrentIndexerBase<TSettings>
|
||||
CheckForLoginError(response);
|
||||
|
||||
cookies = response.GetCookies();
|
||||
UpdateCookies(cookies, DateTime.Now + TimeSpan.FromDays(30));
|
||||
UpdateCookies(cookies, DateTime.Now.AddDays(30));
|
||||
|
||||
_logger.Debug("Gazelle authentication succeeded.");
|
||||
}
|
||||
@@ -68,7 +69,6 @@ public abstract class GazelleBase<TSettings> : TorrentIndexerBase<TSettings>
|
||||
LogResponseContent = true,
|
||||
Method = HttpMethod.Post
|
||||
};
|
||||
requestBuilder.PostProcess += r => r.RequestTimeout = TimeSpan.FromSeconds(15);
|
||||
|
||||
var authLoginRequestBuilder = requestBuilder
|
||||
.AddFormParameter("username", Settings.Username)
|
||||
@@ -107,8 +107,23 @@ public abstract class GazelleBase<TSettings> : TorrentIndexerBase<TSettings>
|
||||
return response;
|
||||
}
|
||||
|
||||
protected override IDictionary<string, string> GetCookies()
|
||||
{
|
||||
if (Settings is GazelleUserPassOrCookieSettings cookieSettings && !string.IsNullOrWhiteSpace(cookieSettings.Cookie))
|
||||
{
|
||||
return CookieUtil.CookieHeaderToDictionary(cookieSettings.Cookie);
|
||||
}
|
||||
|
||||
return base.GetCookies();
|
||||
}
|
||||
|
||||
protected override bool CheckIfLoginNeeded(HttpResponse response)
|
||||
{
|
||||
if (Settings is GazelleUserPassOrCookieSettings cookieSettings && !string.IsNullOrWhiteSpace(cookieSettings.Cookie))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var invalidResponses = new[] { "\"bad credentials\"", "\"groupName\":\"wrong-creds\"" };
|
||||
|
||||
return response.HasHttpRedirect || (response.Content != null && invalidResponses.Any(response.Content.Contains));
|
||||
|
||||
@@ -42,29 +42,23 @@ public class GazelleRequestGenerator : IIndexerRequestGenerator
|
||||
yield return request;
|
||||
}
|
||||
|
||||
public virtual IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
public virtual IEnumerable<IndexerRequest> GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
var parameters = GetBasicSearchParameters(searchCriteria.SearchTerm, searchCriteria.Categories);
|
||||
var parameters = GetBasicSearchParameters(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories);
|
||||
|
||||
if (searchCriteria.ImdbId != null)
|
||||
{
|
||||
parameters.Set(ImdbInTags ? "taglist" : "cataloguenumber", searchCriteria.FullImdbId);
|
||||
}
|
||||
|
||||
pageableRequests.Add(GetRequest(parameters));
|
||||
|
||||
return pageableRequests;
|
||||
return GetRequest(parameters);
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
var parameters = GetBasicSearchParameters(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories);
|
||||
|
||||
var parameters = GetBasicSearchParameters(searchCriteria.SearchTerm, searchCriteria.Categories);
|
||||
|
||||
if (searchCriteria.Artist.IsNotNullOrWhiteSpace())
|
||||
if (searchCriteria.Artist.IsNotNullOrWhiteSpace() && searchCriteria.Artist != "VA")
|
||||
{
|
||||
parameters.Set("artistname", searchCriteria.Artist);
|
||||
}
|
||||
@@ -79,15 +73,11 @@ public class GazelleRequestGenerator : IIndexerRequestGenerator
|
||||
parameters.Set("recordlabel", searchCriteria.Label);
|
||||
}
|
||||
|
||||
pageableRequests.Add(GetRequest(parameters));
|
||||
|
||||
return pageableRequests;
|
||||
return GetRequest(parameters);
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
var parameters = GetBasicSearchParameters(searchCriteria.SanitizedTvSearchString, searchCriteria.Categories);
|
||||
|
||||
if (searchCriteria.ImdbId != null)
|
||||
@@ -95,29 +85,21 @@ public class GazelleRequestGenerator : IIndexerRequestGenerator
|
||||
parameters.Set(ImdbInTags ? "taglist" : "cataloguenumber", searchCriteria.FullImdbId);
|
||||
}
|
||||
|
||||
pageableRequests.Add(GetRequest(parameters));
|
||||
|
||||
return pageableRequests;
|
||||
return GetRequest(parameters);
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
var parameters = GetBasicSearchParameters(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories);
|
||||
|
||||
var parameters = GetBasicSearchParameters(searchCriteria.SearchTerm, searchCriteria.Categories);
|
||||
pageableRequests.Add(GetRequest(parameters));
|
||||
|
||||
return pageableRequests;
|
||||
return GetRequest(parameters);
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
public IEnumerable<IndexerRequest> GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
var parameters = GetBasicSearchParameters(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories);
|
||||
|
||||
var parameters = GetBasicSearchParameters(searchCriteria.SearchTerm, searchCriteria.Categories);
|
||||
pageableRequests.Add(GetRequest(parameters));
|
||||
|
||||
return pageableRequests;
|
||||
return GetRequest(parameters);
|
||||
}
|
||||
|
||||
// hook to adjust the search term
|
||||
|
||||
@@ -4,13 +4,14 @@ using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions.Gazelle;
|
||||
|
||||
public class GazelleSettingsValidator : UserPassBaseSettingsValidator<GazelleSettings>
|
||||
public class GazelleSettingsValidator<T> : UserPassBaseSettingsValidator<T>
|
||||
where T : GazelleSettings
|
||||
{
|
||||
}
|
||||
|
||||
public class GazelleSettings : UserPassTorrentBaseSettings
|
||||
{
|
||||
private static readonly GazelleSettingsValidator Validator = new ();
|
||||
private static readonly GazelleSettingsValidator<GazelleSettings> Validator = new ();
|
||||
|
||||
public string AuthKey { get; set; }
|
||||
public string PassKey { get; set; }
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
using FluentValidation;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Indexers.Settings;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions.Gazelle;
|
||||
|
||||
public class GazelleUserPassOrCookieValidator<T> : NoAuthSettingsValidator<T>
|
||||
where T : GazelleUserPassOrCookieSettings
|
||||
{
|
||||
public GazelleUserPassOrCookieValidator()
|
||||
{
|
||||
RuleFor(c => c.Username).NotEmpty().When(c => c.Cookie.IsNullOrWhiteSpace());
|
||||
RuleFor(c => c.Password).NotEmpty().When(c => c.Cookie.IsNullOrWhiteSpace());
|
||||
RuleFor(c => c.Cookie).NotEmpty().When(c => c.Username.IsNullOrWhiteSpace() && c.Password.IsNullOrWhiteSpace());
|
||||
}
|
||||
}
|
||||
|
||||
public class GazelleUserPassOrCookieSettings : GazelleSettings
|
||||
{
|
||||
private static readonly GazelleUserPassOrCookieValidator<GazelleUserPassOrCookieSettings> Validator = new ();
|
||||
|
||||
[FieldDefinition(4, Label = "Cookie", HelpText = "Use the Cookie field only if 2FA is enabled for your account, leave it empty otherwise.", Privacy = PrivacyLevel.Password)]
|
||||
public string Cookie { get; set; }
|
||||
|
||||
public override NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user