mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2026-03-05 13:40:08 -05:00
Compare commits
92 Commits
v1.1.2.245
...
v1.3.0.275
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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.0'
|
||||
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,7 @@ namespace NzbDrone.Common.Extensions
|
||||
|
||||
public static string ToUrlHost(this string input)
|
||||
{
|
||||
return input.Contains(":") ? $"[{input}]" : input;
|
||||
return input.Contains(':') ? $"[{input}]" : input;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
@@ -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.GetAllTiers().Should().HaveCount(1);
|
||||
|
||||
var page = results.GetAllTiers().First().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.GetAllTiers().Should().HaveCount(1);
|
||||
|
||||
var page = results.GetAllTiers().First().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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,9 +7,8 @@ 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; }
|
||||
@@ -21,58 +20,26 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
|
||||
public string Source { get; set; }
|
||||
public string Host { get; set; }
|
||||
|
||||
public virtual string SearchQuery
|
||||
public override string ToString() => $"{SearchQuery}, Offset: {Offset ?? 0}, Limit: {Limit ?? 0}, 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
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -31,11 +31,12 @@ namespace NzbDrone.Core.IndexerVersions
|
||||
private const string DEFINITION_BRANCH = "master";
|
||||
private const int DEFINITION_VERSION = 8;
|
||||
|
||||
//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)
|
||||
|
||||
@@ -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);
|
||||
@@ -207,7 +198,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
|
||||
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}"));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
@@ -216,7 +207,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedTvSearchString), searchCriteria.Categories));
|
||||
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedTvSearchString}"));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
@@ -225,7 +216,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
|
||||
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}"));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
@@ -234,7 +225,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
|
||||
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}"));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
@@ -243,7 +234,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
|
||||
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}"));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
@@ -256,33 +247,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 +322,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 +340,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 +392,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 +441,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 +467,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));
|
||||
|
||||
@@ -425,7 +425,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;
|
||||
|
||||
@@ -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,18 +93,17 @@ 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)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
|
||||
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}"));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
@@ -114,7 +112,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedTvSearchString), searchCriteria.Categories));
|
||||
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedTvSearchString}"));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
@@ -123,7 +121,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
|
||||
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}"));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
@@ -148,6 +146,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 +156,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 +184,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 +193,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 +257,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 +292,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 +304,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('/'));
|
||||
|
||||
@@ -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)
|
||||
|
||||
358
src/NzbDrone.Core/Indexers/Definitions/AudioBookBay.cs
Normal file
358
src/NzbDrone.Core/Indexers/Definitions/AudioBookBay.cs
Normal file
@@ -0,0 +1,358 @@
|
||||
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 IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
{
|
||||
return new IndexerPageableRequestChain();
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
{
|
||||
return new IndexerPageableRequestChain();
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
{
|
||||
return new IndexerPageableRequestChain();
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}"));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}"));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
@@ -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('/'));
|
||||
|
||||
@@ -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;
|
||||
@@ -245,7 +231,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 +281,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 +293,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 +401,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; }
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -53,7 +52,8 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
var generator = _generatorCache.Get(Settings.DefinitionFile, () =>
|
||||
new CardigannRequestGenerator(_configService,
|
||||
_definitionService.GetCachedDefinition(Settings.DefinitionFile),
|
||||
_logger)
|
||||
_logger,
|
||||
RateLimit)
|
||||
{
|
||||
HttpClient = _httpClient,
|
||||
Definition = Definition,
|
||||
@@ -79,6 +79,18 @@ 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();
|
||||
}
|
||||
|
||||
return cleanReleases;
|
||||
}
|
||||
|
||||
protected override IDictionary<string, string> GetCookies()
|
||||
{
|
||||
if (Settings.ExtraFieldData.TryGetValue("cookie", out var cookies))
|
||||
@@ -180,60 +192,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)
|
||||
|
||||
@@ -300,7 +300,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 +333,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()}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -758,8 +763,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; }
|
||||
@@ -157,7 +158,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 +183,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; }
|
||||
}
|
||||
|
||||
|
||||
@@ -40,12 +40,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,7 +66,7 @@ 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;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ 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;
|
||||
@@ -29,11 +28,15 @@ 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; }
|
||||
@@ -190,6 +193,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 +224,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 +252,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 +286,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 +365,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 +410,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 +435,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 +462,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 +508,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 +537,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 +598,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 +610,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 +654,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 +667,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 +681,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 +712,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 +745,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 +782,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 +797,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 +823,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 +831,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 +869,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 +888,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 +908,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 +929,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 +942,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)
|
||||
@@ -1096,16 +1130,18 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
|
||||
_logger.Info($"Adding request: {searchUrl}");
|
||||
|
||||
var requestbuilder = new HttpRequestBuilder(searchUrl);
|
||||
|
||||
requestbuilder.Method = method;
|
||||
var requestBuilder = new HttpRequestBuilder(searchUrl)
|
||||
{
|
||||
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);
|
||||
requestBuilder.AddFormParameter(param.Key, param.Value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1113,14 +1149,22 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
if (search.Headers != null)
|
||||
{
|
||||
var headers = ParseCustomHeaders(search.Headers, variables);
|
||||
requestbuilder.SetHeaders(headers ?? new Dictionary<string, string>());
|
||||
requestBuilder.SetHeaders(headers ?? new Dictionary<string, string>());
|
||||
}
|
||||
|
||||
var request = new CardigannRequest(requestbuilder.SetEncoding(_encoding).Build(), variables, searchPath);
|
||||
var request = requestBuilder
|
||||
.WithRateLimit(_rateLimit.TotalSeconds)
|
||||
.Build();
|
||||
|
||||
request.HttpRequest.AllowAutoRedirect = searchPath.Followredirect;
|
||||
var cardigannRequest = new CardigannRequest(request, variables, searchPath)
|
||||
{
|
||||
HttpRequest =
|
||||
{
|
||||
AllowAutoRedirect = searchPath.Followredirect
|
||||
}
|
||||
};
|
||||
|
||||
yield return request;
|
||||
yield return cardigannRequest;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -18,54 +19,58 @@ public class FileListRequestGenerator : IIndexerRequestGenerator
|
||||
public IndexerPageableRequestChain 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));
|
||||
pageableRequests.Add(GetPagedRequests(searchCriteria, parameters));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public virtual IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
public IndexerPageableRequestChain 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));
|
||||
pageableRequests.Add(GetPagedRequests(searchCriteria, parameters));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
@@ -73,16 +78,16 @@ public class FileListRequestGenerator : IIndexerRequestGenerator
|
||||
public IndexerPageableRequestChain 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));
|
||||
pageableRequests.Add(GetPagedRequests(searchCriteria, parameters));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
@@ -90,16 +95,16 @@ public class FileListRequestGenerator : IIndexerRequestGenerator
|
||||
public IndexerPageableRequestChain 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));
|
||||
pageableRequests.Add(GetPagedRequests(searchCriteria, parameters));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
@@ -107,47 +112,47 @@ public class FileListRequestGenerator : IIndexerRequestGenerator
|
||||
public IndexerPageableRequestChain 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));
|
||||
pageableRequests.Add(GetPagedRequests(searchCriteria, parameters));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
343
src/NzbDrone.Core/Indexers/Definitions/FunFile.cs
Normal file
343
src/NzbDrone.Core/Indexers/Definitions/FunFile.cs
Normal file
@@ -0,0 +1,343 @@
|
||||
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 IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories, searchCriteria.FullImdbId));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedTvSearchString}", searchCriteria.Categories, searchCriteria.FullImdbId));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
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; }
|
||||
}
|
||||
@@ -56,7 +56,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 +68,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)
|
||||
|
||||
@@ -46,7 +46,7 @@ public class GazelleRequestGenerator : IIndexerRequestGenerator
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
var parameters = GetBasicSearchParameters(searchCriteria.SearchTerm, searchCriteria.Categories);
|
||||
var parameters = GetBasicSearchParameters(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories);
|
||||
|
||||
if (searchCriteria.ImdbId != null)
|
||||
{
|
||||
@@ -62,9 +62,9 @@ public class GazelleRequestGenerator : IIndexerRequestGenerator
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
var parameters = GetBasicSearchParameters(searchCriteria.SearchTerm, searchCriteria.Categories);
|
||||
var parameters = GetBasicSearchParameters(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories);
|
||||
|
||||
if (searchCriteria.Artist.IsNotNullOrWhiteSpace())
|
||||
if (searchCriteria.Artist.IsNotNullOrWhiteSpace() && searchCriteria.Artist != "VA")
|
||||
{
|
||||
parameters.Set("artistname", searchCriteria.Artist);
|
||||
}
|
||||
@@ -104,7 +104,7 @@ public class GazelleRequestGenerator : IIndexerRequestGenerator
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
var parameters = GetBasicSearchParameters(searchCriteria.SearchTerm, searchCriteria.Categories);
|
||||
var parameters = GetBasicSearchParameters(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories);
|
||||
pageableRequests.Add(GetRequest(parameters));
|
||||
|
||||
return pageableRequests;
|
||||
@@ -114,7 +114,7 @@ public class GazelleRequestGenerator : IIndexerRequestGenerator
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
var parameters = GetBasicSearchParameters(searchCriteria.SearchTerm, searchCriteria.Categories);
|
||||
var parameters = GetBasicSearchParameters(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories);
|
||||
pageableRequests.Add(GetRequest(parameters));
|
||||
|
||||
return pageableRequests;
|
||||
|
||||
@@ -104,6 +104,7 @@ public class GreatPosterWallRequestGenerator : GazelleRequestGenerator
|
||||
public class GreatPosterWallParser : GazelleParser
|
||||
{
|
||||
private readonly GreatPosterWallSettings _settings;
|
||||
private readonly HashSet<string> _hdResolutions = new () { "1080p", "1080i", "720p" };
|
||||
|
||||
public GreatPosterWallParser(GreatPosterWallSettings settings, IndexerCapabilities capabilities)
|
||||
: base(settings, capabilities)
|
||||
@@ -113,21 +114,27 @@ public class GreatPosterWallParser : GazelleParser
|
||||
|
||||
public override IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
||||
{
|
||||
var torrentInfos = new List<ReleaseInfo>();
|
||||
var releaseInfos = new List<ReleaseInfo>();
|
||||
|
||||
if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK)
|
||||
{
|
||||
// Remove cookie cache
|
||||
CookiesUpdater(null, null);
|
||||
if (indexerResponse.HttpResponse.HasHttpRedirect)
|
||||
{
|
||||
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");
|
||||
}
|
||||
|
||||
if (!indexerResponse.HttpResponse.Headers.ContentType.Contains(HttpAccept.Json.Value))
|
||||
{
|
||||
// Remove cookie cache
|
||||
CookiesUpdater(null, null);
|
||||
|
||||
throw new IndexerException(indexerResponse, $"Unexpected response header {indexerResponse.HttpResponse.Headers.ContentType} from API request, expected {HttpAccept.Json.Value}");
|
||||
}
|
||||
|
||||
@@ -136,7 +143,7 @@ public class GreatPosterWallParser : GazelleParser
|
||||
jsonResponse.Resource.Status.IsNullOrWhiteSpace() ||
|
||||
jsonResponse.Resource.Response == null)
|
||||
{
|
||||
return torrentInfos;
|
||||
return releaseInfos;
|
||||
}
|
||||
|
||||
foreach (var result in jsonResponse.Resource.Response.Results)
|
||||
@@ -148,15 +155,13 @@ public class GreatPosterWallParser : GazelleParser
|
||||
|
||||
var release = new GazelleInfo
|
||||
{
|
||||
MinimumRatio = 1,
|
||||
MinimumSeedTime = 172800,
|
||||
Title = torrent.FileName,
|
||||
InfoUrl = infoUrl,
|
||||
Title = WebUtility.HtmlDecode(torrent.FileName).Trim(),
|
||||
Guid = infoUrl,
|
||||
InfoUrl = infoUrl,
|
||||
PosterUrl = GetPosterUrl(result.Cover),
|
||||
DownloadUrl = GetDownloadUrl(torrent.TorrentId, torrent.CanUseToken),
|
||||
PublishDate = new DateTimeOffset(time, TimeSpan.FromHours(8)).UtcDateTime, // Time is Chinese Time, add 8 hours difference from UTC
|
||||
Categories = new List<IndexerCategory> { NewznabStandardCategory.Movies },
|
||||
Categories = ParseCategories(torrent),
|
||||
Size = torrent.Size,
|
||||
Seeders = torrent.Seeders,
|
||||
Peers = torrent.Seeders + torrent.Leechers,
|
||||
@@ -164,11 +169,12 @@ public class GreatPosterWallParser : GazelleParser
|
||||
Files = torrent.FileCount,
|
||||
Scene = torrent.Scene,
|
||||
DownloadVolumeFactor = torrent.IsFreeleech || torrent.IsNeutralLeech || torrent.IsPersonalFreeleech ? 0 : 1,
|
||||
UploadVolumeFactor = torrent.IsNeutralLeech ? 0 : 1
|
||||
UploadVolumeFactor = torrent.IsNeutralLeech ? 0 : 1,
|
||||
MinimumRatio = 1,
|
||||
MinimumSeedTime = 172800 // 48 hours
|
||||
};
|
||||
|
||||
var imdbId = ParseUtil.GetImdbID(result.ImdbId);
|
||||
|
||||
if (imdbId != null)
|
||||
{
|
||||
release.ImdbId = (int)imdbId;
|
||||
@@ -194,11 +200,11 @@ public class GreatPosterWallParser : GazelleParser
|
||||
break;
|
||||
}
|
||||
|
||||
torrentInfos.Add(release);
|
||||
releaseInfos.Add(release);
|
||||
}
|
||||
}
|
||||
|
||||
return torrentInfos
|
||||
return releaseInfos
|
||||
.OrderByDescending(o => o.PublishDate)
|
||||
.ToArray();
|
||||
}
|
||||
@@ -213,6 +219,22 @@ public class GreatPosterWallParser : GazelleParser
|
||||
|
||||
return url.FullUri;
|
||||
}
|
||||
|
||||
private List<IndexerCategory> ParseCategories(GreatPosterWallTorrent torrent)
|
||||
{
|
||||
var cats = new List<IndexerCategory>
|
||||
{
|
||||
NewznabStandardCategory.Movies,
|
||||
torrent.Resolution switch
|
||||
{
|
||||
var res when _hdResolutions.Contains(res) => NewznabStandardCategory.MoviesHD,
|
||||
"2160p" => NewznabStandardCategory.MoviesUHD,
|
||||
_ => NewznabStandardCategory.MoviesSD
|
||||
}
|
||||
};
|
||||
|
||||
return cats;
|
||||
}
|
||||
}
|
||||
|
||||
public class GreatPosterWallSettings : GazelleSettings
|
||||
@@ -223,180 +245,76 @@ public class GreatPosterWallSettings : GazelleSettings
|
||||
|
||||
public class GreatPosterWallResponse
|
||||
{
|
||||
[JsonProperty("status")]
|
||||
public string Status { get; set; }
|
||||
|
||||
[JsonProperty("response")]
|
||||
public Response Response { get; set; }
|
||||
public GreatPosterWallResponseWithResults Response { get; set; }
|
||||
}
|
||||
|
||||
public class Response
|
||||
public class GreatPosterWallResponseWithResults
|
||||
{
|
||||
[JsonProperty("currentPage")]
|
||||
public int CurrentPage { get; set; }
|
||||
public int CurrentPage { get; set; }
|
||||
public int Pages { get; set; }
|
||||
|
||||
[JsonProperty("pages")]
|
||||
public int Pages { get; set; }
|
||||
|
||||
[JsonProperty("results")]
|
||||
public List<Result> Results { get; set; }
|
||||
[JsonProperty("results")]
|
||||
public List<GreatPosterWallResult> Results { get; set; }
|
||||
}
|
||||
|
||||
public class Result
|
||||
public class GreatPosterWallResult
|
||||
{
|
||||
[JsonProperty("groupId")]
|
||||
public int GroupId { get; set; }
|
||||
|
||||
[JsonProperty("groupName")]
|
||||
public string GroupName { get; set; }
|
||||
|
||||
[JsonProperty("groupSubName")]
|
||||
public string GroupSubName { get; set; }
|
||||
|
||||
[JsonProperty("cover")]
|
||||
public string Cover { get; set; }
|
||||
|
||||
[JsonProperty("tags")]
|
||||
public List<string> Tags { get; set; }
|
||||
|
||||
[JsonProperty("bookmarked")]
|
||||
public bool Bookmarked { get; set; }
|
||||
|
||||
[JsonProperty("groupYear")]
|
||||
public int GroupYear { get; set; }
|
||||
|
||||
[JsonProperty("releaseType")]
|
||||
public string ReleaseType { get; set; }
|
||||
|
||||
[JsonProperty("groupTime")]
|
||||
public string GroupTime { get; set; }
|
||||
|
||||
[JsonProperty("maxSize")]
|
||||
public object MaxSize { get; set; }
|
||||
|
||||
[JsonProperty("totalSnatched")]
|
||||
public int TotalSnatched { get; set; }
|
||||
|
||||
[JsonProperty("totalSeeders")]
|
||||
public int TotalSeeders { get; set; }
|
||||
|
||||
[JsonProperty("totalLeechers")]
|
||||
public int TotalLeechers { get; set; }
|
||||
|
||||
[JsonProperty("imdbId")]
|
||||
public string ImdbId { get; set; }
|
||||
|
||||
[JsonProperty("imdbRating")]
|
||||
public string ImdbRating { get; set; }
|
||||
|
||||
[JsonProperty("imdbVote")]
|
||||
public string ImdbVote { get; set; }
|
||||
|
||||
[JsonProperty("doubanId")]
|
||||
public string DoubanId { get; set; }
|
||||
|
||||
[JsonProperty("doubanRating")]
|
||||
public string DoubanRating { get; set; }
|
||||
|
||||
[JsonProperty("doubanVote")]
|
||||
public string DoubanVote { get; set; }
|
||||
|
||||
[JsonProperty("rtRating")]
|
||||
public string RtRating { get; set; }
|
||||
|
||||
[JsonProperty("region")]
|
||||
public string Region { get; set; }
|
||||
|
||||
[JsonProperty("torrents")]
|
||||
public List<GreatPosterWallTorrent> Torrents { get; set; }
|
||||
public int GroupId { get; set; }
|
||||
public string GroupName { get; set; }
|
||||
public string GroupSubName { get; set; }
|
||||
public string Cover { get; set; }
|
||||
public List<string> Tags { get; set; }
|
||||
public bool Bookmarked { get; set; }
|
||||
public int GroupYear { get; set; }
|
||||
public string ReleaseType { get; set; }
|
||||
public string GroupTime { get; set; }
|
||||
public object MaxSize { get; set; }
|
||||
public int TotalSnatched { get; set; }
|
||||
public int TotalSeeders { get; set; }
|
||||
public int TotalLeechers { get; set; }
|
||||
public string ImdbId { get; set; }
|
||||
public string ImdbRating { get; set; }
|
||||
public string ImdbVote { get; set; }
|
||||
public string DoubanId { get; set; }
|
||||
public string DoubanRating { get; set; }
|
||||
public string DoubanVote { get; set; }
|
||||
public string RtRating { get; set; }
|
||||
public string Region { get; set; }
|
||||
[JsonProperty("torrents")]
|
||||
public List<GreatPosterWallTorrent> Torrents { get; set; }
|
||||
}
|
||||
|
||||
public class GreatPosterWallTorrent
|
||||
{
|
||||
[JsonProperty("torrentId")]
|
||||
public int TorrentId { get; set; }
|
||||
|
||||
[JsonProperty("editionId")]
|
||||
public int EditionId { get; set; }
|
||||
|
||||
[JsonProperty("remasterYear")]
|
||||
public int RemasterYear { get; set; }
|
||||
|
||||
[JsonProperty("remasterTitle")]
|
||||
public string RemasterTitle { get; set; }
|
||||
|
||||
[JsonProperty("remasterCustomTitle")]
|
||||
public string RemasterCustomTitle { get; set; }
|
||||
|
||||
[JsonProperty("scene")]
|
||||
public bool Scene { get; set; }
|
||||
|
||||
[JsonProperty("jinzhuan")]
|
||||
public bool Jinzhuan { get; set; }
|
||||
|
||||
[JsonProperty("fileCount")]
|
||||
public int FileCount { get; set; }
|
||||
|
||||
[JsonProperty("time")]
|
||||
public DateTime Time { get; set; }
|
||||
|
||||
[JsonProperty("size")]
|
||||
public long Size { get; set; }
|
||||
|
||||
[JsonProperty("snatches")]
|
||||
public int Snatches { get; set; }
|
||||
|
||||
[JsonProperty("seeders")]
|
||||
public int Seeders { get; set; }
|
||||
|
||||
[JsonProperty("leechers")]
|
||||
public int Leechers { get; set; }
|
||||
|
||||
[JsonProperty("isFreeleech")]
|
||||
public bool IsFreeleech { get; set; }
|
||||
|
||||
[JsonProperty("isNeutralLeech")]
|
||||
public bool IsNeutralLeech { get; set; }
|
||||
|
||||
[JsonProperty("freeType")]
|
||||
public string FreeType { get; set; }
|
||||
|
||||
[JsonProperty("isPersonalFreeleech")]
|
||||
public bool IsPersonalFreeleech { get; set; }
|
||||
|
||||
[JsonProperty("canUseToken")]
|
||||
public bool CanUseToken { get; set; }
|
||||
|
||||
[JsonProperty("hasSnatched")]
|
||||
public bool HasSnatched { get; set; }
|
||||
|
||||
[JsonProperty("resolution")]
|
||||
public string Resolution { get; set; }
|
||||
|
||||
[JsonProperty("source")]
|
||||
public string Source { get; set; }
|
||||
|
||||
[JsonProperty("codec")]
|
||||
public string Codec { get; set; }
|
||||
|
||||
[JsonProperty("container")]
|
||||
public string Container { get; set; }
|
||||
|
||||
[JsonProperty("processing")]
|
||||
public string Processing { get; set; }
|
||||
|
||||
[JsonProperty("chineseDubbed")]
|
||||
public string ChineseDubbed { get; set; }
|
||||
|
||||
[JsonProperty("specialSub")]
|
||||
public string SpecialSub { get; set; }
|
||||
|
||||
[JsonProperty("subtitles")]
|
||||
public string Subtitles { get; set; }
|
||||
|
||||
[JsonProperty("fileName")]
|
||||
public string FileName { get; set; }
|
||||
|
||||
[JsonProperty("releaseGroup")]
|
||||
public string ReleaseGroup { get; set; }
|
||||
public int TorrentId { get; set; }
|
||||
public int EditionId { get; set; }
|
||||
public int RemasterYear { get; set; }
|
||||
public string RemasterTitle { get; set; }
|
||||
public string RemasterCustomTitle { get; set; }
|
||||
public bool Scene { get; set; }
|
||||
public bool Jinzhuan { get; set; }
|
||||
public int FileCount { get; set; }
|
||||
public DateTime Time { get; set; }
|
||||
public long Size { get; set; }
|
||||
public int Snatches { get; set; }
|
||||
public int Seeders { get; set; }
|
||||
public int Leechers { get; set; }
|
||||
public bool IsFreeleech { get; set; }
|
||||
public bool IsNeutralLeech { get; set; }
|
||||
public string FreeType { get; set; }
|
||||
public bool IsPersonalFreeleech { get; set; }
|
||||
public bool CanUseToken { get; set; }
|
||||
public bool HasSnatched { get; set; }
|
||||
public string Resolution { get; set; }
|
||||
public string Source { get; set; }
|
||||
public string Codec { get; set; }
|
||||
public string Container { get; set; }
|
||||
public string Processing { get; set; }
|
||||
public string ChineseDubbed { get; set; }
|
||||
public string SpecialSub { get; set; }
|
||||
public string Subtitles { get; set; }
|
||||
public string FileName { get; set; }
|
||||
public string ReleaseGroup { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
using System.Collections.Generic;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.HDBits
|
||||
namespace NzbDrone.Core.Indexers.Definitions.HDBits
|
||||
{
|
||||
public class HDBits : TorrentIndexerBase<HDBitsSettings>
|
||||
{
|
||||
public override string Name => "HDBits";
|
||||
public override string[] IndexerUrls => new string[] { "https://hdbits.org" };
|
||||
public override string[] IndexerUrls => new[] { "https://hdbits.org/" };
|
||||
public override string[] LegacyUrls => new[] { "https://hdbits.org" };
|
||||
public override string Description => "Best HD Tracker";
|
||||
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
|
||||
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
|
||||
public override IndexerCapabilities Capabilities => SetCapabilities();
|
||||
public override bool SupportsRedirect => true;
|
||||
|
||||
public override int PageSize => 30;
|
||||
|
||||
public HDBits(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
|
||||
@@ -25,7 +24,7 @@ namespace NzbDrone.Core.Indexers.HDBits
|
||||
|
||||
public override IIndexerRequestGenerator GetRequestGenerator()
|
||||
{
|
||||
return new HDBitsRequestGenerator() { Settings = Settings, Capabilities = Capabilities };
|
||||
return new HDBitsRequestGenerator { Settings = Settings, Capabilities = Capabilities };
|
||||
}
|
||||
|
||||
public override IParseIndexerResponse GetParser()
|
||||
@@ -38,13 +37,13 @@ namespace NzbDrone.Core.Indexers.HDBits
|
||||
var caps = new IndexerCapabilities
|
||||
{
|
||||
TvSearchParams = new List<TvSearchParam>
|
||||
{
|
||||
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep, TvSearchParam.TvdbId
|
||||
},
|
||||
{
|
||||
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep, TvSearchParam.TvdbId
|
||||
},
|
||||
MovieSearchParams = new List<MovieSearchParam>
|
||||
{
|
||||
MovieSearchParam.Q, MovieSearchParam.ImdbId
|
||||
}
|
||||
{
|
||||
MovieSearchParam.Q, MovieSearchParam.ImdbId
|
||||
}
|
||||
};
|
||||
|
||||
caps.Categories.AddCategoryMapping(6, NewznabStandardCategory.Audio, "Audio Track");
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.HDBits
|
||||
namespace NzbDrone.Core.Indexers.Definitions.HDBits
|
||||
{
|
||||
public class TorrentQuery
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.HDBits
|
||||
namespace NzbDrone.Core.Indexers.Definitions.HDBits
|
||||
{
|
||||
public class HDBitsInfo : TorrentInfo
|
||||
{
|
||||
|
||||
@@ -7,7 +7,7 @@ using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Indexers.Exceptions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.HDBits
|
||||
namespace NzbDrone.Core.Indexers.Definitions.HDBits
|
||||
{
|
||||
public class HDBitsParser : IParseIndexerResponse
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using NzbDrone.Common.Extensions;
|
||||
@@ -8,7 +9,7 @@ using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.HDBits
|
||||
namespace NzbDrone.Core.Indexers.Definitions.HDBits
|
||||
{
|
||||
public class HDBitsRequestGenerator : IIndexerRequestGenerator
|
||||
{
|
||||
@@ -33,7 +34,7 @@ namespace NzbDrone.Core.Indexers.HDBits
|
||||
|
||||
if (imdbId != 0)
|
||||
{
|
||||
query.ImdbInfo = query.ImdbInfo ?? new ImdbInfo();
|
||||
query.ImdbInfo ??= new ImdbInfo();
|
||||
query.ImdbInfo.Id = imdbId;
|
||||
}
|
||||
|
||||
@@ -91,15 +92,23 @@ namespace NzbDrone.Core.Indexers.HDBits
|
||||
|
||||
if (tvdbId != 0)
|
||||
{
|
||||
query.TvdbInfo = query.TvdbInfo ?? new TvdbInfo();
|
||||
query.TvdbInfo ??= new TvdbInfo();
|
||||
query.TvdbInfo.Id = tvdbId;
|
||||
query.TvdbInfo.Season = searchCriteria.Season;
|
||||
query.TvdbInfo.Episode = searchCriteria.Episode;
|
||||
|
||||
if (DateTime.TryParseExact($"{searchCriteria.Season} {searchCriteria.Episode}", "yyyy MM/dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var showDate))
|
||||
{
|
||||
query.Search = showDate.ToString("yyyy-MM-dd");
|
||||
}
|
||||
else
|
||||
{
|
||||
query.TvdbInfo.Season = searchCriteria.Season;
|
||||
query.TvdbInfo.Episode = searchCriteria.Episode;
|
||||
}
|
||||
}
|
||||
|
||||
if (imdbId != 0)
|
||||
{
|
||||
query.ImdbInfo = query.ImdbInfo ?? new ImdbInfo();
|
||||
query.ImdbInfo ??= new ImdbInfo();
|
||||
query.ImdbInfo.Id = imdbId;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Indexers.Settings;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.HDBits
|
||||
namespace NzbDrone.Core.Indexers.Definitions.HDBits
|
||||
{
|
||||
public class HDBitsSettingsValidator : NoAuthSettingsValidator<HDBitsSettings>
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
@@ -58,10 +59,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
Method = HttpMethod.Post
|
||||
};
|
||||
|
||||
requestBuilder.PostProcess += r => r.RequestTimeout = TimeSpan.FromSeconds(15);
|
||||
|
||||
var cookies = Cookies;
|
||||
|
||||
Cookies = null;
|
||||
|
||||
var authLoginRequest = requestBuilder
|
||||
@@ -86,19 +84,14 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
}
|
||||
|
||||
cookies = response.GetCookies();
|
||||
UpdateCookies(cookies, DateTime.Now + TimeSpan.FromDays(30));
|
||||
UpdateCookies(cookies, DateTime.Now.AddDays(30));
|
||||
|
||||
_logger.Debug("HDSpace 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()
|
||||
@@ -155,26 +148,30 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories, string imdb = null)
|
||||
{
|
||||
var searchUrl = string.Format("{0}/index.php?page=torrents&", Settings.BaseUrl.TrimEnd('/'));
|
||||
|
||||
var queryCollection = new NameValueCollection
|
||||
{
|
||||
{ "page", "torrents" },
|
||||
{ "active", "0" },
|
||||
{ "category", string.Join(";", Capabilities.Categories.MapTorznabCapsToTrackers(categories)) }
|
||||
};
|
||||
|
||||
if (imdb != null)
|
||||
var catList = Capabilities.Categories.MapTorznabCapsToTrackers(categories);
|
||||
if (catList.Any())
|
||||
{
|
||||
queryCollection.Add("options", "2");
|
||||
queryCollection.Add("search", imdb);
|
||||
queryCollection.Set("category", string.Join(";", catList));
|
||||
}
|
||||
|
||||
if (imdb.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
queryCollection.Set("options", "2");
|
||||
queryCollection.Set("search", imdb);
|
||||
}
|
||||
else
|
||||
{
|
||||
queryCollection.Add("options", "0");
|
||||
queryCollection.Add("search", term.Replace(".", " "));
|
||||
queryCollection.Set("options", "0");
|
||||
queryCollection.Set("search", term.Replace(".", " "));
|
||||
}
|
||||
|
||||
searchUrl += queryCollection.GetQueryString();
|
||||
var searchUrl = $"{Settings.BaseUrl.TrimEnd('/')}/index.php?{queryCollection.GetQueryString()}";
|
||||
|
||||
var request = new IndexerRequest(searchUrl, HttpAccept.Html);
|
||||
|
||||
@@ -267,15 +264,10 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
release.DownloadUrl = _settings.BaseUrl + downloadUrl;
|
||||
|
||||
// Use the torrent filename as release title
|
||||
var torrentTitle = ParseUtil.GetArgumentFromQueryString(downloadUrl, "f")?
|
||||
.Replace("&", "&")
|
||||
.Replace("'", "'")
|
||||
.Replace(".torrent", "")
|
||||
.Trim();
|
||||
|
||||
var torrentTitle = ParseUtil.GetArgumentFromQueryString(downloadUrl, "f")?.Replace(".torrent", "").Trim();
|
||||
if (torrentTitle.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
release.Title = torrentTitle;
|
||||
release.Title = WebUtility.HtmlDecode(torrentTitle);
|
||||
}
|
||||
|
||||
var qGenres = row.QuerySelector("td:nth-child(2) span[style=\"color: #000000 \"]");
|
||||
@@ -318,8 +310,9 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
}
|
||||
|
||||
release.UploadVolumeFactor = 1;
|
||||
var qCat = row.QuerySelector("a[href^=\"index.php?page=torrents&category=\"]");
|
||||
var cat = qCat.GetAttribute("href").Split('=')[2];
|
||||
|
||||
var categoryLink = row.QuerySelector("a[href^=\"index.php?page=torrents&category=\"]").GetAttribute("href");
|
||||
var cat = ParseUtil.GetArgumentFromQueryString(categoryLink, "category");
|
||||
release.Categories = _categories.MapTrackerCatToNewznab(cat);
|
||||
|
||||
torrentInfos.Add(release);
|
||||
|
||||
@@ -58,11 +58,9 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
Method = HttpMethod.Post
|
||||
};
|
||||
|
||||
requestBuilder.PostProcess += r => r.RequestTimeout = TimeSpan.FromSeconds(15);
|
||||
|
||||
var cookies = Cookies;
|
||||
|
||||
Cookies = null;
|
||||
|
||||
var authLoginRequest = requestBuilder
|
||||
.AddFormParameter("uid", Settings.Username)
|
||||
.AddFormParameter("pwd", Settings.Password)
|
||||
@@ -73,7 +71,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
var response = await ExecuteAuth(authLoginRequest);
|
||||
|
||||
cookies = response.GetCookies();
|
||||
UpdateCookies(cookies, DateTime.Now + TimeSpan.FromDays(30));
|
||||
UpdateCookies(cookies, DateTime.Now.AddDays(30));
|
||||
|
||||
_logger.Debug("HDTorrents authentication succeeded.");
|
||||
}
|
||||
@@ -222,7 +220,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
private readonly UserPassTorrentBaseSettings _settings;
|
||||
private readonly IndexerCapabilitiesCategories _categories;
|
||||
|
||||
private readonly Regex _posterRegex = new Regex(@"src=\\'./([^']+)\\'", RegexOptions.IgnoreCase);
|
||||
private readonly Regex _posterRegex = new (@"src=\\'./([^']+)\\'", RegexOptions.IgnoreCase);
|
||||
private readonly HashSet<string> _freeleechRanks = new (StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"VIP",
|
||||
@@ -263,15 +261,14 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
: null;
|
||||
|
||||
var link = new Uri(_settings.BaseUrl + row.Children[4].FirstElementChild.GetAttribute("href"));
|
||||
var description = row.Children[2].QuerySelector("span").TextContent;
|
||||
var description = row.Children[2].QuerySelector("span")?.TextContent.Trim();
|
||||
var size = ParseUtil.GetBytes(row.Children[7].TextContent);
|
||||
|
||||
var dateTag = row.Children[6].FirstElementChild;
|
||||
var dateString = string.Join(" ", dateTag.Attributes.Select(attr => attr.Name));
|
||||
var publishDate = DateTime.ParseExact(dateString, "dd MMM yyyy HH:mm:ss zz00", CultureInfo.InvariantCulture).ToLocalTime();
|
||||
var dateAdded = string.Join(" ", row.Children[6].FirstElementChild.Attributes.Select(a => a.Name).Take(4));
|
||||
var publishDate = DateTime.ParseExact(dateAdded, "dd MMM yyyy HH:mm:ss", CultureInfo.InvariantCulture);
|
||||
|
||||
var catStr = row.FirstElementChild.FirstElementChild.GetAttribute("href").Split('=')[1];
|
||||
var cat = _categories.MapTrackerCatToNewznab(catStr);
|
||||
var categoryLink = row.FirstElementChild.FirstElementChild.GetAttribute("href");
|
||||
var cat = ParseUtil.GetArgumentFromQueryString(categoryLink, "category");
|
||||
|
||||
// Sometimes the uploader column is missing, so seeders, leechers, and grabs may be at a different index.
|
||||
// There's room for improvement, but this works for now.
|
||||
@@ -340,12 +337,13 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
var release = new TorrentInfo
|
||||
{
|
||||
Title = title,
|
||||
Description = description,
|
||||
Guid = details.AbsoluteUri,
|
||||
DownloadUrl = link.AbsoluteUri,
|
||||
InfoUrl = details.AbsoluteUri,
|
||||
PosterUrl = poster,
|
||||
PublishDate = publishDate,
|
||||
Categories = cat,
|
||||
Categories = _categories.MapTrackerCatToNewznab(cat),
|
||||
ImdbId = imdb ?? 0,
|
||||
Size = size,
|
||||
Grabs = grabs,
|
||||
|
||||
@@ -28,6 +28,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
|
||||
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
|
||||
public override IndexerCapabilities Capabilities => SetCapabilities();
|
||||
public override TimeSpan RateLimit => TimeSpan.FromSeconds(5);
|
||||
private string LoginUrl => Settings.BaseUrl + "takelogin.php";
|
||||
|
||||
public ImmortalSeed(IIndexerHttpClient httpClient,
|
||||
@@ -57,7 +58,6 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
AllowAutoRedirect = true,
|
||||
Method = HttpMethod.Post
|
||||
};
|
||||
requestBuilder.PostProcess += r => r.RequestTimeout = TimeSpan.FromSeconds(15);
|
||||
|
||||
var authLoginRequest = requestBuilder
|
||||
.AddFormParameter("username", Settings.Username)
|
||||
@@ -73,7 +73,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
}
|
||||
|
||||
var cookies = response.GetCookies();
|
||||
UpdateCookies(cookies, DateTime.Now + TimeSpan.FromDays(30));
|
||||
UpdateCookies(cookies, DateTime.Now.AddDays(30));
|
||||
|
||||
_logger.Debug("ImmortalSeed authentication succeeded.");
|
||||
}
|
||||
@@ -213,7 +213,13 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories)
|
||||
{
|
||||
var parameters = new NameValueCollection();
|
||||
var parameters = new NameValueCollection
|
||||
{
|
||||
{ "category", "0" },
|
||||
{ "include_dead_torrents", "yes" },
|
||||
{ "sort", "added" },
|
||||
{ "order", "desc" }
|
||||
};
|
||||
|
||||
term = Regex.Replace(term, @"[ -._]+", " ").Trim();
|
||||
|
||||
@@ -222,12 +228,9 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
parameters.Set("do", "search");
|
||||
parameters.Set("keywords", term);
|
||||
parameters.Set("search_type", "t_name");
|
||||
parameters.Set("category", "0");
|
||||
parameters.Set("include_dead_torrents", "no");
|
||||
}
|
||||
|
||||
var queryCats = _capabilities.Categories.MapTorznabCapsToTrackers(categories);
|
||||
|
||||
if (queryCats.Any())
|
||||
{
|
||||
parameters.Set("selectedcats2", string.Join(",", queryCats));
|
||||
@@ -309,7 +312,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
release.PublishDate = DateTime.ParseExact(dateAddedMatch.Value, "yyyy-MM-dd hh:mm tt", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
if (row.QuerySelector("img[title^=\"Free Torrent\"]") != null)
|
||||
if (row.QuerySelector("img[title^=\"Free Torrent\"], img[title^=\"Sitewide Free Torrent\"]") != null)
|
||||
{
|
||||
release.DownloadVolumeFactor = 0;
|
||||
}
|
||||
|
||||
@@ -3,11 +3,11 @@ using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using AngleSharp.Dom;
|
||||
using AngleSharp.Html.Parser;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
@@ -21,352 +21,348 @@ using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions
|
||||
namespace NzbDrone.Core.Indexers.Definitions;
|
||||
|
||||
public class Libble : TorrentIndexerBase<LibbleSettings>
|
||||
{
|
||||
internal class Libble : TorrentIndexerBase<LibbleSettings>
|
||||
public override string Name => "Libble";
|
||||
public override string[] IndexerUrls => new[] { "https://libble.me/" };
|
||||
public override string Description => "Libble is a Private Torrent Tracker for MUSIC";
|
||||
private string LoginUrl => Settings.BaseUrl + "login.php";
|
||||
public override string Language => "en-US";
|
||||
public override Encoding Encoding => Encoding.UTF8;
|
||||
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
|
||||
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
|
||||
public override int PageSize => 50;
|
||||
public override IndexerCapabilities Capabilities => SetCapabilities();
|
||||
|
||||
public Libble(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
|
||||
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
|
||||
{
|
||||
public override string Name => "Libble";
|
||||
public override string[] IndexerUrls => new string[] { "https://libble.me/" };
|
||||
public override string Description => "Libble is a Private Torrent Tracker for MUSIC";
|
||||
private string LoginUrl => Settings.BaseUrl + "login.php";
|
||||
public override string Language => "en-US";
|
||||
public override Encoding Encoding => Encoding.UTF8;
|
||||
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
|
||||
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
|
||||
public override int PageSize => 50;
|
||||
public override IndexerCapabilities Capabilities => SetCapabilities();
|
||||
|
||||
public Libble(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
|
||||
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public override IIndexerRequestGenerator GetRequestGenerator()
|
||||
{
|
||||
return new LibbleRequestGenerator() { Settings = Settings, Capabilities = Capabilities };
|
||||
}
|
||||
|
||||
public override IParseIndexerResponse GetParser()
|
||||
{
|
||||
return new LibbleParser(Settings, Capabilities.Categories);
|
||||
}
|
||||
|
||||
protected override async Task DoLogin()
|
||||
{
|
||||
var requestBuilder = new HttpRequestBuilder(LoginUrl)
|
||||
{
|
||||
Method = HttpMethod.Post,
|
||||
AllowAutoRedirect = true
|
||||
};
|
||||
|
||||
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("code", Settings.TwoFactorAuthCode)
|
||||
.AddFormParameter("keeplogged", "1")
|
||||
.AddFormParameter("login", "Login")
|
||||
.SetHeader("Content-Type", "multipart/form-data")
|
||||
.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("#loginform > .warning")?.TextContent.Trim();
|
||||
|
||||
throw new IndexerAuthException($"Libble authentication failed. Error: \"{errorMessage}\"");
|
||||
}
|
||||
|
||||
cookies = response.GetCookies();
|
||||
UpdateCookies(cookies, DateTime.Now + TimeSpan.FromDays(30));
|
||||
|
||||
_logger.Debug("Libble authentication succeeded.");
|
||||
}
|
||||
|
||||
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
|
||||
{
|
||||
return !httpResponse.Content.Contains("logout.php");
|
||||
}
|
||||
|
||||
private IndexerCapabilities SetCapabilities()
|
||||
{
|
||||
var caps = new IndexerCapabilities
|
||||
{
|
||||
MusicSearchParams = new List<MusicSearchParam>
|
||||
{
|
||||
MusicSearchParam.Q, MusicSearchParam.Artist, MusicSearchParam.Album, MusicSearchParam.Label, MusicSearchParam.Year, MusicSearchParam.Genre
|
||||
}
|
||||
};
|
||||
|
||||
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.Audio);
|
||||
caps.Categories.AddCategoryMapping(2, NewznabStandardCategory.Audio);
|
||||
caps.Categories.AddCategoryMapping(7, NewznabStandardCategory.AudioVideo);
|
||||
|
||||
return caps;
|
||||
}
|
||||
}
|
||||
|
||||
public class LibbleRequestGenerator : IIndexerRequestGenerator
|
||||
public override IIndexerRequestGenerator GetRequestGenerator()
|
||||
{
|
||||
public LibbleSettings Settings { get; set; }
|
||||
public IndexerCapabilities Capabilities { get; set; }
|
||||
|
||||
public LibbleRequestGenerator()
|
||||
{
|
||||
}
|
||||
|
||||
private IEnumerable<IndexerRequest> GetPagedRequests(SearchCriteriaBase searchCriteria, NameValueCollection parameters)
|
||||
{
|
||||
var term = searchCriteria.SanitizedSearchTerm.Trim();
|
||||
|
||||
parameters.Add("order_by", "time");
|
||||
parameters.Add("order_way", "desc");
|
||||
parameters.Add("searchstr", term);
|
||||
|
||||
var queryCats = Capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories);
|
||||
|
||||
if (queryCats.Count > 0)
|
||||
{
|
||||
foreach (var cat in queryCats)
|
||||
{
|
||||
parameters.Add($"filter_cat[{cat}]", "1");
|
||||
}
|
||||
}
|
||||
|
||||
if (searchCriteria.Offset.HasValue && searchCriteria.Limit.HasValue && searchCriteria.Offset > 0 && searchCriteria.Limit > 0)
|
||||
{
|
||||
var page = (int)(searchCriteria.Offset / searchCriteria.Limit) + 1;
|
||||
parameters.Add("page", page.ToString());
|
||||
}
|
||||
|
||||
var searchUrl = string.Format("{0}/torrents.php?{1}", Settings.BaseUrl.TrimEnd('/'), parameters.GetQueryString());
|
||||
|
||||
var request = new IndexerRequest(searchUrl, HttpAccept.Html);
|
||||
|
||||
yield return request;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
var parameters = new NameValueCollection();
|
||||
|
||||
if (searchCriteria.Artist.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
parameters.Add("artistname", searchCriteria.Artist);
|
||||
}
|
||||
|
||||
if (searchCriteria.Album.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
parameters.Add("groupname", searchCriteria.Album);
|
||||
}
|
||||
|
||||
if (searchCriteria.Label.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
parameters.Add("recordlabel", searchCriteria.Label);
|
||||
}
|
||||
|
||||
if (searchCriteria.Year.HasValue)
|
||||
{
|
||||
parameters.Add("year", searchCriteria.Year.ToString());
|
||||
}
|
||||
|
||||
if (searchCriteria.Genre.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
parameters.Add("taglist", searchCriteria.Genre);
|
||||
}
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(searchCriteria, parameters));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
var parameters = new NameValueCollection();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(searchCriteria, parameters));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
{
|
||||
return new IndexerPageableRequestChain();
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
{
|
||||
return new IndexerPageableRequestChain();
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
{
|
||||
return new IndexerPageableRequestChain();
|
||||
}
|
||||
|
||||
public Func<IDictionary<string, string>> GetCookies { get; set; }
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
return new LibbleRequestGenerator(Settings, Capabilities);
|
||||
}
|
||||
|
||||
public class LibbleParser : IParseIndexerResponse
|
||||
public override IParseIndexerResponse GetParser()
|
||||
{
|
||||
private readonly LibbleSettings _settings;
|
||||
private readonly IndexerCapabilitiesCategories _categories;
|
||||
return new LibbleParser(Settings);
|
||||
}
|
||||
|
||||
public LibbleParser(LibbleSettings settings, IndexerCapabilitiesCategories categories)
|
||||
protected override async Task DoLogin()
|
||||
{
|
||||
var requestBuilder = new HttpRequestBuilder(LoginUrl)
|
||||
{
|
||||
_settings = settings;
|
||||
_categories = categories;
|
||||
}
|
||||
AllowAutoRedirect = true,
|
||||
Method = HttpMethod.Post
|
||||
};
|
||||
|
||||
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
||||
var cookies = Cookies;
|
||||
Cookies = null;
|
||||
|
||||
var authLoginRequest = requestBuilder
|
||||
.AddFormParameter("username", Settings.Username)
|
||||
.AddFormParameter("password", Settings.Password)
|
||||
.AddFormParameter("code", Settings.TwoFactorAuthCode)
|
||||
.AddFormParameter("keeplogged", "1")
|
||||
.AddFormParameter("login", "Login")
|
||||
.SetHeader("Content-Type", "application/x-www-form-urlencoded")
|
||||
.SetHeader("Referer", LoginUrl)
|
||||
.Build();
|
||||
|
||||
var response = await ExecuteAuth(authLoginRequest);
|
||||
|
||||
if (CheckIfLoginNeeded(response))
|
||||
{
|
||||
var torrentInfos = new List<ReleaseInfo>();
|
||||
|
||||
var parser = new HtmlParser();
|
||||
var doc = parser.ParseDocument(indexerResponse.Content);
|
||||
var rows = doc.QuerySelectorAll("table#torrent_table > tbody > tr.group:has(strong > a[href*=\"torrents.php?id=\"])");
|
||||
var dom = parser.ParseDocument(response.Content);
|
||||
var errorMessage = dom.QuerySelector("#loginform > .warning")?.TextContent.Trim();
|
||||
|
||||
var releaseYearRegex = new Regex(@"\[(\d{4})\]$");
|
||||
|
||||
foreach (var row in rows)
|
||||
{
|
||||
var albumLinkNode = row.QuerySelector("strong > a[href*=\"torrents.php?id=\"]");
|
||||
var groupId = ParseUtil.GetArgumentFromQueryString(albumLinkNode.GetAttribute("href"), "id");
|
||||
|
||||
var artistsNodes = row.QuerySelectorAll("strong > a[href*=\"artist.php?id=\"]");
|
||||
|
||||
var releaseArtist = "Various Artists";
|
||||
if (artistsNodes.Count() > 0)
|
||||
{
|
||||
releaseArtist = artistsNodes.Select(artist => artist.TextContent.Trim()).ToList().Join(", ");
|
||||
}
|
||||
|
||||
var releaseAlbumName = row.QuerySelector("strong > a[href*=\"torrents.php?id=\"]")?.TextContent.Trim();
|
||||
|
||||
var title = row.QuerySelector("td:nth-child(4) > strong")?.TextContent.Trim();
|
||||
var releaseAlbumYear = releaseYearRegex.Match(title);
|
||||
|
||||
var releaseDescription = row.QuerySelector("div.tags")?.TextContent.Trim();
|
||||
var releaseThumbnailUrl = row.QuerySelector(".thumbnail")?.GetAttribute("title").Trim();
|
||||
|
||||
var releaseGenres = new List<string>();
|
||||
if (!string.IsNullOrEmpty(releaseDescription))
|
||||
{
|
||||
releaseGenres = releaseGenres.Union(releaseDescription.Split(',').Select(tag => tag.Trim()).ToList()).ToList();
|
||||
}
|
||||
|
||||
var cat = row.QuerySelector("td.cats_col div.cat_icon")?.GetAttribute("class").Trim();
|
||||
|
||||
var matchCategory = Regex.Match(cat, @"\bcats_(.*?)\b");
|
||||
if (matchCategory.Success)
|
||||
{
|
||||
cat = matchCategory.Groups[1].Value.Trim();
|
||||
}
|
||||
|
||||
var category = new List<IndexerCategory>
|
||||
{
|
||||
cat switch
|
||||
{
|
||||
"music" => NewznabStandardCategory.Audio,
|
||||
"libblemixtapes" => NewznabStandardCategory.Audio,
|
||||
"musicvideos" => NewznabStandardCategory.AudioVideo,
|
||||
_ => NewznabStandardCategory.Other,
|
||||
}
|
||||
};
|
||||
|
||||
var releaseRows = doc.QuerySelectorAll(string.Format("table#torrent_table > tbody > tr.group_torrent.groupid_{0}:has(a[href*=\"torrents.php?id=\"])", groupId));
|
||||
|
||||
foreach (var releaseRow in releaseRows)
|
||||
{
|
||||
var release = new TorrentInfo();
|
||||
|
||||
var detailsNode = releaseRow.QuerySelector("a[href^=\"torrents.php?id=\"]");
|
||||
var downloadLink = _settings.BaseUrl + releaseRow.QuerySelector("a[href^=\"torrents.php?action=download&id=\"]").GetAttribute("href").Trim();
|
||||
|
||||
var releaseTags = detailsNode.FirstChild.TextContent.Trim(' ', '/');
|
||||
|
||||
release.Title = string.Format("{0} - {1} {2} {3}", releaseArtist, releaseAlbumName, releaseAlbumYear, releaseTags).Trim();
|
||||
release.Categories = category;
|
||||
release.Description = releaseDescription;
|
||||
release.Genres = releaseGenres;
|
||||
release.PosterUrl = releaseThumbnailUrl;
|
||||
|
||||
release.InfoUrl = _settings.BaseUrl + detailsNode.GetAttribute("href").Trim();
|
||||
release.DownloadUrl = downloadLink;
|
||||
release.Guid = release.InfoUrl;
|
||||
|
||||
release.Size = ParseUtil.GetBytes(releaseRow.QuerySelector("td:nth-child(4)").TextContent.Trim());
|
||||
release.Files = ParseUtil.CoerceInt(releaseRow.QuerySelector("td:nth-child(2)").TextContent);
|
||||
release.Grabs = ParseUtil.CoerceInt(releaseRow.QuerySelector("td:nth-child(5)").TextContent);
|
||||
release.Seeders = ParseUtil.CoerceInt(releaseRow.QuerySelector("td:nth-child(6)").TextContent);
|
||||
release.Peers = release.Seeders + ParseUtil.CoerceInt(releaseRow.QuerySelector("td:nth-child(7)").TextContent);
|
||||
|
||||
release.MinimumRatio = 1;
|
||||
release.MinimumSeedTime = 259200; // 72 hours
|
||||
|
||||
try
|
||||
{
|
||||
release.PublishDate = DateTime.ParseExact(
|
||||
releaseRow.QuerySelector("td:nth-child(3) > span[title]").GetAttribute("title").Trim(),
|
||||
"MMM dd yyyy, HH:mm",
|
||||
CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.AssumeUniversal);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
|
||||
switch (releaseRow.QuerySelector("a[href^=\"torrents.php?id=\"] strong")?.TextContent.Trim())
|
||||
{
|
||||
case "Neutral!":
|
||||
release.DownloadVolumeFactor = 0;
|
||||
release.UploadVolumeFactor = 0;
|
||||
break;
|
||||
case "Freeleech!":
|
||||
release.DownloadVolumeFactor = 0;
|
||||
release.UploadVolumeFactor = 1;
|
||||
break;
|
||||
default:
|
||||
release.DownloadVolumeFactor = 1;
|
||||
release.UploadVolumeFactor = 1;
|
||||
break;
|
||||
}
|
||||
|
||||
torrentInfos.Add(release);
|
||||
}
|
||||
}
|
||||
|
||||
return torrentInfos.ToArray();
|
||||
throw new IndexerAuthException(errorMessage ?? "Unknown error message, please report.");
|
||||
}
|
||||
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
cookies = response.GetCookies();
|
||||
UpdateCookies(cookies, DateTime.Now.AddDays(30));
|
||||
|
||||
_logger.Debug("Authentication succeeded.");
|
||||
}
|
||||
|
||||
public class LibbleSettings : UserPassTorrentBaseSettings
|
||||
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
|
||||
{
|
||||
public LibbleSettings()
|
||||
{
|
||||
TwoFactorAuthCode = "";
|
||||
}
|
||||
return !httpResponse.Content.Contains("logout.php");
|
||||
}
|
||||
|
||||
[FieldDefinition(4, Label = "2FA code", Type = FieldType.Textbox, HelpText = "Only fill in the <b>2FA code</b> box if you have enabled <b>2FA</b> on the Libble Web Site. Otherwise just leave it empty.")]
|
||||
public string TwoFactorAuthCode { get; set; }
|
||||
private IndexerCapabilities SetCapabilities()
|
||||
{
|
||||
var caps = new IndexerCapabilities
|
||||
{
|
||||
MusicSearchParams = new List<MusicSearchParam>
|
||||
{
|
||||
MusicSearchParam.Q, MusicSearchParam.Artist, MusicSearchParam.Album, MusicSearchParam.Label, MusicSearchParam.Year, MusicSearchParam.Genre
|
||||
}
|
||||
};
|
||||
|
||||
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.Audio, "Music");
|
||||
caps.Categories.AddCategoryMapping(2, NewznabStandardCategory.Audio, "Libble Mixtapes");
|
||||
caps.Categories.AddCategoryMapping(7, NewznabStandardCategory.AudioVideo, "Music Videos");
|
||||
|
||||
return caps;
|
||||
}
|
||||
}
|
||||
|
||||
public class LibbleRequestGenerator : IIndexerRequestGenerator
|
||||
{
|
||||
private readonly LibbleSettings _settings;
|
||||
private readonly IndexerCapabilities _capabilities;
|
||||
|
||||
public LibbleRequestGenerator(LibbleSettings settings, IndexerCapabilities capabilities)
|
||||
{
|
||||
_settings = settings;
|
||||
_capabilities = capabilities;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
var parameters = new NameValueCollection();
|
||||
|
||||
if (searchCriteria.Artist.IsNotNullOrWhiteSpace() && searchCriteria.Artist != "VA")
|
||||
{
|
||||
parameters.Set("artistname", searchCriteria.Artist);
|
||||
}
|
||||
|
||||
if (searchCriteria.Album.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
// Remove year
|
||||
var album = Regex.Replace(searchCriteria.Album, @"(.+)\b\d{4}$", "$1");
|
||||
|
||||
parameters.Set("groupname", album.Trim());
|
||||
}
|
||||
|
||||
if (searchCriteria.Label.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
parameters.Set("recordlabel", searchCriteria.Label);
|
||||
}
|
||||
|
||||
if (searchCriteria.Year.HasValue)
|
||||
{
|
||||
parameters.Set("year", searchCriteria.Year.ToString());
|
||||
}
|
||||
|
||||
if (searchCriteria.Genre.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
parameters.Set("taglist", searchCriteria.Genre);
|
||||
parameters.Set("tags_type", "0");
|
||||
}
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(searchCriteria, parameters));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
var parameters = new NameValueCollection();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(searchCriteria, parameters));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
{
|
||||
return new IndexerPageableRequestChain();
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
{
|
||||
return new IndexerPageableRequestChain();
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
{
|
||||
return new IndexerPageableRequestChain();
|
||||
}
|
||||
|
||||
private IEnumerable<IndexerRequest> GetPagedRequests(SearchCriteriaBase searchCriteria, NameValueCollection parameters)
|
||||
{
|
||||
var term = searchCriteria.SanitizedSearchTerm.Trim();
|
||||
|
||||
parameters.Set("action", "advanced");
|
||||
parameters.Set("order_by", "time");
|
||||
parameters.Set("order_way", "desc");
|
||||
|
||||
if (term.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
parameters.Set("searchstr", term);
|
||||
}
|
||||
|
||||
var queryCats = _capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories);
|
||||
if (queryCats.Any())
|
||||
{
|
||||
queryCats.ForEach(cat => parameters.Set($"filter_cat[{cat}]", "1"));
|
||||
}
|
||||
|
||||
if (searchCriteria.Offset.HasValue && searchCriteria.Limit.HasValue && searchCriteria.Offset > 0 && searchCriteria.Limit > 0)
|
||||
{
|
||||
var page = (int)(searchCriteria.Offset / searchCriteria.Limit) + 1;
|
||||
parameters.Set("page", page.ToString());
|
||||
}
|
||||
|
||||
var searchUrl = $"{_settings.BaseUrl.TrimEnd('/')}/torrents.php?{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 LibbleParser : IParseIndexerResponse
|
||||
{
|
||||
private readonly LibbleSettings _settings;
|
||||
private static Regex ReleaseYearRegex => new (@"\[(\d{4})\]$", RegexOptions.Compiled);
|
||||
|
||||
public LibbleParser(LibbleSettings settings)
|
||||
{
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
||||
{
|
||||
var releaseInfos = new List<ReleaseInfo>();
|
||||
|
||||
var parser = new HtmlParser();
|
||||
var doc = parser.ParseDocument(indexerResponse.Content);
|
||||
|
||||
var groups = doc.QuerySelectorAll("table#torrent_table > tbody > tr.group:has(strong > a[href*=\"torrents.php?id=\"])");
|
||||
foreach (var group in groups)
|
||||
{
|
||||
var albumLinkNode = group.QuerySelector("strong > a[href*=\"torrents.php?id=\"]");
|
||||
var groupId = ParseUtil.GetArgumentFromQueryString(albumLinkNode.GetAttribute("href"), "id");
|
||||
|
||||
var artistsNodes = group.QuerySelectorAll("strong > a[href*=\"artist.php?id=\"]");
|
||||
|
||||
var releaseArtist = "Various Artists";
|
||||
if (artistsNodes.Any())
|
||||
{
|
||||
releaseArtist = artistsNodes.Select(artist => artist.TextContent.Trim()).ToList().Join(", ");
|
||||
}
|
||||
|
||||
var releaseAlbumName = group.QuerySelector("strong > a[href*=\"torrents.php?id=\"]")?.TextContent.Trim();
|
||||
|
||||
var title = group.QuerySelector("td:nth-child(4) > strong")?.TextContent.Trim();
|
||||
var releaseAlbumYear = ReleaseYearRegex.Match(title);
|
||||
|
||||
var releaseDescription = group.QuerySelector("div.tags")?.TextContent.Trim();
|
||||
var releaseThumbnailUrl = group.QuerySelector(".thumbnail")?.GetAttribute("title")?.Trim();
|
||||
|
||||
var releaseGenres = new List<string>();
|
||||
if (!string.IsNullOrEmpty(releaseDescription))
|
||||
{
|
||||
releaseGenres = releaseDescription.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).ToList();
|
||||
}
|
||||
|
||||
var rows = doc.QuerySelectorAll($"table#torrent_table > tbody > tr.group_torrent.groupid_{groupId}:has(a[href*=\"torrents.php?id=\"])");
|
||||
foreach (var row in rows)
|
||||
{
|
||||
var detailsNode = row.QuerySelector("a[href^=\"torrents.php?id=\"]");
|
||||
|
||||
var infoUrl = _settings.BaseUrl + detailsNode.GetAttribute("href").Trim();
|
||||
var downloadLink = _settings.BaseUrl + row.QuerySelector("a[href^=\"torrents.php?action=download&id=\"]").GetAttribute("href").Trim();
|
||||
|
||||
var releaseTags = detailsNode.FirstChild?.TextContent.Trim(' ', '/');
|
||||
var seeders = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(6)").TextContent);
|
||||
|
||||
var release = new TorrentInfo
|
||||
{
|
||||
Guid = infoUrl,
|
||||
InfoUrl = infoUrl,
|
||||
DownloadUrl = downloadLink,
|
||||
Title = $"{releaseArtist} - {releaseAlbumName} {releaseAlbumYear.Value} {releaseTags}".Trim(' ', '-'),
|
||||
Artist = releaseArtist,
|
||||
Album = releaseAlbumName,
|
||||
Categories = ParseCategories(group),
|
||||
Description = releaseDescription,
|
||||
Size = ParseUtil.GetBytes(row.QuerySelector("td:nth-child(4)").TextContent.Trim()),
|
||||
Files = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(2)").TextContent),
|
||||
Grabs = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(5)").TextContent),
|
||||
Seeders = seeders,
|
||||
Peers = seeders + ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(7)").TextContent),
|
||||
DownloadVolumeFactor = 1,
|
||||
UploadVolumeFactor = 1,
|
||||
MinimumRatio = 1,
|
||||
MinimumSeedTime = 259200, // 72 hours,
|
||||
Genres = releaseGenres,
|
||||
PosterUrl = releaseThumbnailUrl,
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var dateAdded = row.QuerySelector("td:nth-child(3) > span[title]").GetAttribute("title").Trim();
|
||||
release.PublishDate = DateTime.ParseExact(dateAdded, "MMM dd yyyy, HH:mm", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
release.PublishDate = DateTimeUtil.FromTimeAgo(row.QuerySelector("td:nth-child(3)")?.TextContent.Trim());
|
||||
}
|
||||
|
||||
switch (row.QuerySelector("a[href^=\"torrents.php?id=\"] strong")?.TextContent.ToLower().Trim(' ', '!'))
|
||||
{
|
||||
case "neutral":
|
||||
release.DownloadVolumeFactor = 0;
|
||||
release.UploadVolumeFactor = 0;
|
||||
break;
|
||||
case "freeleech":
|
||||
release.DownloadVolumeFactor = 0;
|
||||
release.UploadVolumeFactor = 1;
|
||||
break;
|
||||
}
|
||||
|
||||
releaseInfos.Add(release);
|
||||
}
|
||||
}
|
||||
|
||||
return releaseInfos.ToArray();
|
||||
}
|
||||
|
||||
private IList<IndexerCategory> ParseCategories(IElement group)
|
||||
{
|
||||
var cat = group.QuerySelector("td.cats_col div.cat_icon")?.GetAttribute("class")?.Trim();
|
||||
|
||||
var matchCategory = Regex.Match(cat, @"\bcats_(.*?)\b");
|
||||
if (matchCategory.Success)
|
||||
{
|
||||
cat = matchCategory.Groups[1].Value.Trim();
|
||||
}
|
||||
|
||||
return new List<IndexerCategory>
|
||||
{
|
||||
cat switch
|
||||
{
|
||||
"music" => NewznabStandardCategory.Audio,
|
||||
"libblemixtapes" => NewznabStandardCategory.Audio,
|
||||
"musicvideos" => NewznabStandardCategory.AudioVideo,
|
||||
_ => NewznabStandardCategory.Other,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
}
|
||||
|
||||
public class LibbleSettings : UserPassTorrentBaseSettings
|
||||
{
|
||||
public LibbleSettings()
|
||||
{
|
||||
TwoFactorAuthCode = "";
|
||||
}
|
||||
|
||||
[FieldDefinition(4, Label = "2FA code", Type = FieldType.Textbox, HelpText = "Only fill in the <b>2FA code</b> box if you have enabled <b>2FA</b> on the Libble Web Site. Otherwise just leave it empty.")]
|
||||
public string TwoFactorAuthCode { get; set; }
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
@@ -27,6 +28,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
public override Encoding Encoding => Encoding.UTF8;
|
||||
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
|
||||
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
|
||||
public override bool SupportsRedirect => true;
|
||||
public override IndexerCapabilities Capabilities => SetCapabilities();
|
||||
|
||||
public Nebulance(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
|
||||
@@ -44,6 +46,19 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
return new NebulanceParser(Settings);
|
||||
}
|
||||
|
||||
protected override Task<HttpRequest> GetDownloadRequest(Uri link)
|
||||
{
|
||||
// Avoid using cookies to prevent redirects to login page
|
||||
var requestBuilder = new HttpRequestBuilder(link.AbsoluteUri)
|
||||
{
|
||||
AllowAutoRedirect = FollowRedirect
|
||||
};
|
||||
|
||||
var request = requestBuilder.Build();
|
||||
|
||||
return Task.FromResult(request);
|
||||
}
|
||||
|
||||
private IndexerCapabilities SetCapabilities()
|
||||
{
|
||||
var caps = new IndexerCapabilities
|
||||
|
||||
@@ -211,7 +211,7 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
{
|
||||
_logger.Warn(ex, "Unable to connect to indexer: " + ex.Message);
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user