mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2026-04-17 21:44:48 -04:00
Compare commits
129 Commits
v1.3.2.298
...
v1.5.0.330
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1fd188fe7a | ||
|
|
5e5699fbbe | ||
|
|
d61275e6db | ||
|
|
dca3e939f0 | ||
|
|
26ac66c0e1 | ||
|
|
649b301444 | ||
|
|
78ed2a1af0 | ||
|
|
a4854b7b5f | ||
|
|
97edf495bd | ||
|
|
d10bdf4676 | ||
|
|
03647143e3 | ||
|
|
8090dc9983 | ||
|
|
5bc1f345c0 | ||
|
|
4ef01f5640 | ||
|
|
f13d5c5a14 | ||
|
|
dc8773cf79 | ||
|
|
cad774e250 | ||
|
|
b28eee578a | ||
|
|
5b8c7d0b79 | ||
|
|
8bdc7a6db7 | ||
|
|
cb189b8f61 | ||
|
|
24468db376 | ||
|
|
9b10cea556 | ||
|
|
d8fb71d501 | ||
|
|
fc39a11ece | ||
|
|
40dc4de47d | ||
|
|
a0e2f3324c | ||
|
|
1bcc3b426e | ||
|
|
66f5fd2a26 | ||
|
|
b5e5701791 | ||
|
|
1a9b202afe | ||
|
|
309f42bac5 | ||
|
|
ed330ea657 | ||
|
|
fc6a31ea78 | ||
|
|
25ba9195cf | ||
|
|
681f06e321 | ||
|
|
af7fb442d2 | ||
|
|
2061b9142f | ||
|
|
b97f6f8ddf | ||
|
|
f31c0bb1de | ||
|
|
65e6aa05c3 | ||
|
|
fb20b3e61b | ||
|
|
b8a77830aa | ||
|
|
d2ba52cdce | ||
|
|
43f881c442 | ||
|
|
4a5e923999 | ||
|
|
57e1b6b4a0 | ||
|
|
9cc60760c3 | ||
|
|
2811feb14e | ||
|
|
46af9223bc | ||
|
|
025156978b | ||
|
|
d3ca861aea | ||
|
|
c9249ed583 | ||
|
|
94cc56d0f6 | ||
|
|
c8addc0d62 | ||
|
|
2015156061 | ||
|
|
742c680014 | ||
|
|
b1add3f649 | ||
|
|
65d6d518d7 | ||
|
|
bc8ba5ca02 | ||
|
|
6aebc4ee01 | ||
|
|
9bbe51253b | ||
|
|
88fbc30be2 | ||
|
|
5fdc6ee25d | ||
|
|
4eb5a2d613 | ||
|
|
122883053a | ||
|
|
28d09cd384 | ||
|
|
17be8bb68a | ||
|
|
c5baded3d6 | ||
|
|
349cfacdca | ||
|
|
788fa6d96a | ||
|
|
fbea5bbc06 | ||
|
|
d667c7d853 | ||
|
|
a9e1204a9b | ||
|
|
88e3f86262 | ||
|
|
1c173fc984 | ||
|
|
6e8f3d814a | ||
|
|
14e105e37e | ||
|
|
9e0deb8f74 | ||
|
|
245e573089 | ||
|
|
5e8bfa2ffb | ||
|
|
555c924e50 | ||
|
|
8404b85624 | ||
|
|
dc5e6d29e1 | ||
|
|
8c42b7a69b | ||
|
|
3a6ebdef8a | ||
|
|
5f57957462 | ||
|
|
12526c1bb3 | ||
|
|
29f049f766 | ||
|
|
40f4e1b82a | ||
|
|
065fbb30bf | ||
|
|
ea24a81ef7 | ||
|
|
451f60319f | ||
|
|
c6ed5d65e0 | ||
|
|
4e5cd05bbd | ||
|
|
6b2b953686 | ||
|
|
31c05be9de | ||
|
|
bc852c0b55 | ||
|
|
18651d8be1 | ||
|
|
1608095345 | ||
|
|
7700014ceb | ||
|
|
3fbc2912f0 | ||
|
|
3192990874 | ||
|
|
fb908e8e19 | ||
|
|
8e60c707b2 | ||
|
|
a184bb0784 | ||
|
|
e5ccbaaf24 | ||
|
|
362e0acad1 | ||
|
|
54d06460d0 | ||
|
|
c11bcf4c41 | ||
|
|
2e58583263 | ||
|
|
bf7f769f13 | ||
|
|
7820a83a5d | ||
|
|
d937bdac69 | ||
|
|
ebca32af46 | ||
|
|
21bda07510 | ||
|
|
f638cf34d1 | ||
|
|
b7fcdb5356 | ||
|
|
2e4fa9d06d | ||
|
|
9b50fc40ca | ||
|
|
3c60159df0 | ||
|
|
e075003c8b | ||
|
|
b19202d9f5 | ||
|
|
2784ee8ce6 | ||
|
|
5aa4a5faaa | ||
|
|
1d00b40f90 | ||
|
|
93dd378ade | ||
|
|
534ca73bf8 | ||
|
|
bceebc34c1 |
7
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
7
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -71,3 +71,10 @@ body:
|
||||
Additionally, any additional info? Screenshots? References? Anything that will give us more context about the issue you are encountering!
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Trace Logs have been provided as applicable. Reports may be closed if the required logs are not provided.
|
||||
description: Trace logs are generally required for all bug reports
|
||||
options:
|
||||
- label: I have followed the steps in the wiki link above and provided the required trace logs that are relevant and show this issue.
|
||||
required: true
|
||||
|
||||
9
.github/label-actions.yml
vendored
9
.github/label-actions.yml
vendored
@@ -13,4 +13,11 @@
|
||||
:wave: @{issue-author}, we use the issue tracker exclusively
|
||||
for bug reports and feature requests. However, this issue appears
|
||||
to be a indexer request. Please use our Indexer request [site](https://requests.prowlarr.com/)
|
||||
close: true
|
||||
close: true
|
||||
|
||||
'Status: Logs Needed':
|
||||
comment: >
|
||||
:wave: @{issue-author}, in order to help you further we'll need to see logs.
|
||||
You'll need to enable trace logging and replicate the problem that you encountered.
|
||||
Guidance on how to enable trace logging can be found in
|
||||
our [troubleshooting guide](https://wiki.servarr.com/prowlarr/troubleshooting#logging-and-log-files).
|
||||
@@ -9,13 +9,13 @@ variables:
|
||||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '1.3.2'
|
||||
majorVersion: '1.5.0'
|
||||
minorVersion: $[counter('minorVersion', 1)]
|
||||
prowlarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
|
||||
sentryOrg: 'servarr'
|
||||
sentryUrl: 'https://sentry.servarr.com'
|
||||
dotnetVersion: '6.0.405'
|
||||
dotnetVersion: '6.0.408'
|
||||
innoVersion: '6.2.0'
|
||||
nodeVersion: '16.x'
|
||||
windowsImage: 'windows-2022'
|
||||
|
||||
@@ -50,7 +50,7 @@ module.exports = (env) => {
|
||||
'node_modules'
|
||||
],
|
||||
alias: {
|
||||
jquery: 'jquery/src/jquery'
|
||||
jquery: 'jquery/dist/jquery.min'
|
||||
},
|
||||
fallback: {
|
||||
buffer: false,
|
||||
|
||||
@@ -17,7 +17,7 @@ class DescriptionListItem extends Component {
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<span>
|
||||
<div>
|
||||
<DescriptionListItemTitle
|
||||
className={titleClassName}
|
||||
>
|
||||
@@ -29,7 +29,7 @@ class DescriptionListItem extends Component {
|
||||
>
|
||||
{data}
|
||||
</DescriptionListItemDescription>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import Alert from 'Components/Alert';
|
||||
import PathInput from 'Components/Form/PathInput';
|
||||
import Button from 'Components/Link/Button';
|
||||
@@ -39,7 +38,7 @@ class FileBrowserModalContent extends Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this._scrollerNode = null;
|
||||
this._scrollerRef = React.createRef();
|
||||
|
||||
this.state = {
|
||||
isFileBrowserModalOpen: false,
|
||||
@@ -57,21 +56,10 @@ class FileBrowserModalContent extends Component {
|
||||
currentPath !== prevState.currentPath
|
||||
) {
|
||||
this.setState({ currentPath });
|
||||
this._scrollerNode.scrollTop = 0;
|
||||
this._scrollerRef.current.scrollTop = 0;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
setScrollerRef = (ref) => {
|
||||
if (ref) {
|
||||
this._scrollerNode = ReactDOM.findDOMNode(ref);
|
||||
} else {
|
||||
this._scrollerNode = null;
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
@@ -145,7 +133,7 @@ class FileBrowserModalContent extends Component {
|
||||
/>
|
||||
|
||||
<Scroller
|
||||
ref={this.setScrollerRef}
|
||||
ref={this._scrollerRef}
|
||||
className={styles.scroller}
|
||||
scrollDirection={scrollDirections.BOTH}
|
||||
>
|
||||
|
||||
@@ -112,6 +112,12 @@ class TextInput extends Component {
|
||||
this._isMouseTarget = false;
|
||||
};
|
||||
|
||||
onWheel = () => {
|
||||
if (this.props.type === 'number') {
|
||||
this._input.blur();
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -161,6 +167,7 @@ class TextInput extends Component {
|
||||
onKeyUp={this.onKeyUp}
|
||||
onMouseDown={this.onMouseDown}
|
||||
onMouseUp={this.onMouseUp}
|
||||
onWheel={this.onWheel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ function IconButton(props) {
|
||||
className,
|
||||
isDisabled && styles.isDisabled
|
||||
)}
|
||||
aria-label="Table Options Button"
|
||||
isDisabled={isDisabled}
|
||||
{...otherProps}
|
||||
>
|
||||
|
||||
@@ -56,6 +56,7 @@ class PageHeader extends Component {
|
||||
<img
|
||||
className={styles.logo}
|
||||
src={`${window.Prowlarr.urlBase}/Content/Images/logo.png`}
|
||||
alt="Prowlarr Logo"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
@@ -74,6 +75,7 @@ class PageHeader extends Component {
|
||||
<IconButton
|
||||
className={styles.donate}
|
||||
name={icons.HEART}
|
||||
aria-label="Donate"
|
||||
to="https://prowlarr.com/donate"
|
||||
size={14}
|
||||
/>
|
||||
|
||||
@@ -21,7 +21,7 @@ function PageHeaderActionsMenu(props) {
|
||||
return (
|
||||
<div>
|
||||
<Menu alignMenu={align.RIGHT}>
|
||||
<MenuButton className={styles.menuButton}>
|
||||
<MenuButton className={styles.menuButton} aria-label="Menu Button">
|
||||
<Icon
|
||||
name={icons.INTERACTIVE}
|
||||
/>
|
||||
|
||||
@@ -56,7 +56,9 @@ function ProgressBar(props) {
|
||||
styles[kind],
|
||||
enableColorImpairedMode && 'colorImpaired'
|
||||
)}
|
||||
aria-valuenow={progress}
|
||||
role="meter"
|
||||
aria-label={`Progress Bar at ${progress.toFixed(0)}%`}
|
||||
aria-valuenow={progress.toFixed(0)}
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
style={{ width: progressPercent }}
|
||||
|
||||
@@ -6,6 +6,7 @@ import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import CapabilitiesLabel from 'Indexer/Index/Table/CapabilitiesLabel';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import HistoryDetailsModal from './Details/HistoryDetailsModal';
|
||||
import HistoryEventTypeCell from './HistoryEventTypeCell';
|
||||
import HistoryRowParameter from './HistoryRowParameter';
|
||||
@@ -193,7 +194,7 @@ class HistoryRow extends Component {
|
||||
{
|
||||
data.season ?
|
||||
<HistoryRowParameter
|
||||
title='Season'
|
||||
title={translate('Season')}
|
||||
value={data.season}
|
||||
/> :
|
||||
null
|
||||
@@ -202,7 +203,7 @@ class HistoryRow extends Component {
|
||||
{
|
||||
data.episode ?
|
||||
<HistoryRowParameter
|
||||
title='Episode'
|
||||
title={translate('Episode')}
|
||||
value={data.episode}
|
||||
/> :
|
||||
null
|
||||
@@ -211,7 +212,7 @@ class HistoryRow extends Component {
|
||||
{
|
||||
data.artist ?
|
||||
<HistoryRowParameter
|
||||
title='Artist'
|
||||
title={translate('Artist')}
|
||||
value={data.artist}
|
||||
/> :
|
||||
null
|
||||
@@ -220,7 +221,7 @@ class HistoryRow extends Component {
|
||||
{
|
||||
data.album ?
|
||||
<HistoryRowParameter
|
||||
title='Album'
|
||||
title={translate('Album')}
|
||||
value={data.album}
|
||||
/> :
|
||||
null
|
||||
@@ -229,7 +230,7 @@ class HistoryRow extends Component {
|
||||
{
|
||||
data.label ?
|
||||
<HistoryRowParameter
|
||||
title='Label'
|
||||
title={translate('Label')}
|
||||
value={data.label}
|
||||
/> :
|
||||
null
|
||||
@@ -238,7 +239,7 @@ class HistoryRow extends Component {
|
||||
{
|
||||
data.track ?
|
||||
<HistoryRowParameter
|
||||
title='Track'
|
||||
title={translate('Track')}
|
||||
value={data.track}
|
||||
/> :
|
||||
null
|
||||
@@ -247,7 +248,7 @@ class HistoryRow extends Component {
|
||||
{
|
||||
data.year ?
|
||||
<HistoryRowParameter
|
||||
title='Year'
|
||||
title={translate('Year')}
|
||||
value={data.year}
|
||||
/> :
|
||||
null
|
||||
@@ -256,7 +257,7 @@ class HistoryRow extends Component {
|
||||
{
|
||||
data.genre ?
|
||||
<HistoryRowParameter
|
||||
title='Genre'
|
||||
title={translate('Genre')}
|
||||
value={data.genre}
|
||||
/> :
|
||||
null
|
||||
@@ -265,7 +266,7 @@ class HistoryRow extends Component {
|
||||
{
|
||||
data.author ?
|
||||
<HistoryRowParameter
|
||||
title='Author'
|
||||
title={translate('Author')}
|
||||
value={data.author}
|
||||
/> :
|
||||
null
|
||||
@@ -274,7 +275,7 @@ class HistoryRow extends Component {
|
||||
{
|
||||
data.bookTitle ?
|
||||
<HistoryRowParameter
|
||||
title='Book'
|
||||
title={translate('Book')}
|
||||
value={data.bookTitle}
|
||||
/> :
|
||||
null
|
||||
@@ -283,7 +284,7 @@ class HistoryRow extends Component {
|
||||
{
|
||||
data.publisher ?
|
||||
<HistoryRowParameter
|
||||
title='Publisher'
|
||||
title={translate('Publisher')}
|
||||
value={data.publisher}
|
||||
/> :
|
||||
null
|
||||
@@ -351,6 +352,11 @@ class HistoryRow extends Component {
|
||||
`${data.elapsedTime}ms` :
|
||||
null
|
||||
}
|
||||
{
|
||||
data.cached === '1' ?
|
||||
' (cached)' :
|
||||
null
|
||||
}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
@@ -376,14 +382,14 @@ class HistoryRow extends Component {
|
||||
<IconButton
|
||||
name={icons.SEARCH}
|
||||
onPress={this.onSearchPress}
|
||||
title='Repeat Search'
|
||||
title={translate('RepeatSearch')}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
<IconButton
|
||||
name={icons.INFO}
|
||||
onPress={this.onDetailsPress}
|
||||
title='History Details'
|
||||
title={translate('HistoryDetails')}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
|
||||
@@ -222,7 +222,11 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<IndexerIndexSelectModeButton
|
||||
label={isSelectMode ? 'Stop Selecting' : 'Select Indexer'}
|
||||
label={
|
||||
isSelectMode
|
||||
? translate('StopSelecting')
|
||||
: translate('SelectIndexer')
|
||||
}
|
||||
iconName={isSelectMode ? icons.SERIES_ENDED : icons.CHECK}
|
||||
isSelectMode={isSelectMode}
|
||||
overflowComponent={IndexerIndexSelectModeMenuItem}
|
||||
@@ -230,7 +234,7 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
|
||||
/>
|
||||
|
||||
<IndexerIndexSelectAllButton
|
||||
label="SelectAll"
|
||||
label={translate('SelectAll')}
|
||||
isSelectMode={isSelectMode}
|
||||
overflowComponent={IndexerIndexSelectAllMenuItem}
|
||||
/>
|
||||
@@ -245,7 +249,10 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
|
||||
optionsComponent={IndexerIndexTableOptions}
|
||||
onTableOptionChange={onTableOptionChange}
|
||||
>
|
||||
<PageToolbarButton label="Options" iconName={icons.TABLE} />
|
||||
<PageToolbarButton
|
||||
label={translate('Options')}
|
||||
iconName={icons.TABLE}
|
||||
/>
|
||||
</TableOptionsModalWrapper>
|
||||
|
||||
<PageToolbarSeparator />
|
||||
@@ -276,7 +283,9 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
|
||||
>
|
||||
{isFetching && !isPopulated ? <LoadingIndicator /> : null}
|
||||
|
||||
{!isFetching && !!error ? <div>Unable to load indexers</div> : null}
|
||||
{!isFetching && !!error ? (
|
||||
<div>{translate('UnableToLoadIndexers')}</div>
|
||||
) : null}
|
||||
|
||||
{isLoaded ? (
|
||||
<div className={styles.contentBodyContainer}>
|
||||
@@ -295,7 +304,10 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
|
||||
) : null}
|
||||
|
||||
{!error && isPopulated && !items.length ? (
|
||||
<NoIndexer totalItems={totalItems} />
|
||||
<NoIndexer
|
||||
totalItems={totalItems}
|
||||
onAddIndexerPress={onAddIndexerPress}
|
||||
/>
|
||||
) : null}
|
||||
</PageContentBody>
|
||||
{isLoaded && !!jumpBarItems.order.length ? (
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useCallback } from 'react';
|
||||
import { SelectActionType, useSelect } from 'App/SelectContext';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
interface IndexerIndexSelectAllButtonProps {
|
||||
label: string;
|
||||
@@ -32,7 +33,7 @@ function IndexerIndexSelectAllButton(props: IndexerIndexSelectAllButtonProps) {
|
||||
|
||||
return isSelectMode ? (
|
||||
<PageToolbarButton
|
||||
label={allSelected ? 'Unselect All' : 'Select All'}
|
||||
label={allSelected ? translate('UnselectAll') : translate('SelectAll')}
|
||||
iconName={icon}
|
||||
onPress={onPress}
|
||||
/>
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useCallback } from 'react';
|
||||
import { SelectActionType, useSelect } from 'App/SelectContext';
|
||||
import PageToolbarOverflowMenuItem from 'Components/Page/Toolbar/PageToolbarOverflowMenuItem';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
interface IndexerIndexSelectAllMenuItemProps {
|
||||
label: string;
|
||||
@@ -33,7 +34,7 @@ function IndexerIndexSelectAllMenuItem(
|
||||
|
||||
return isSelectMode ? (
|
||||
<PageToolbarOverflowMenuItem
|
||||
label={allSelected ? 'Unselect All' : 'Select All'}
|
||||
label={allSelected ? translate('UnselectAll') : translate('SelectAll')}
|
||||
iconName={iconName}
|
||||
onPress={onPressWrapper}
|
||||
/>
|
||||
|
||||
@@ -14,6 +14,7 @@ import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||
import createAllIndexersSelector from 'Store/Selectors/createAllIndexersSelector';
|
||||
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './TagsModalContent.css';
|
||||
|
||||
interface TagsModalContentProps {
|
||||
@@ -70,7 +71,7 @@ function TagsModalContent(props: TagsModalContentProps) {
|
||||
<ModalBody>
|
||||
<Form>
|
||||
<FormGroup>
|
||||
<FormLabel>Tags</FormLabel>
|
||||
<FormLabel>{translate('Tags')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TAG}
|
||||
@@ -81,7 +82,7 @@ function TagsModalContent(props: TagsModalContentProps) {
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Apply Tags</FormLabel>
|
||||
<FormLabel>{translate('ApplyTags')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
@@ -89,17 +90,17 @@ function TagsModalContent(props: TagsModalContentProps) {
|
||||
value={applyTags}
|
||||
values={applyTagsOptions}
|
||||
helpTexts={[
|
||||
'How to apply tags to the selected series',
|
||||
'Add: Add the tags the existing list of tags',
|
||||
'Remove: Remove the entered tags',
|
||||
'Replace: Replace the tags with the entered tags (enter no tags to clear all tags)',
|
||||
translate('ApplyTagsHelpTexts1'),
|
||||
translate('ApplyTagsHelpTexts2'),
|
||||
translate('ApplyTagsHelpTexts3'),
|
||||
translate('ApplyTagsHelpTexts4'),
|
||||
]}
|
||||
onChange={onApplyTagsChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Result</FormLabel>
|
||||
<FormLabel>{translate('Result')}</FormLabel>
|
||||
|
||||
<div className={styles.result}>
|
||||
{indexerTags.map((id) => {
|
||||
@@ -116,7 +117,11 @@ function TagsModalContent(props: TagsModalContentProps) {
|
||||
return (
|
||||
<Label
|
||||
key={tag.id}
|
||||
title={removeTag ? 'Removing tag' : 'Existing tag'}
|
||||
title={
|
||||
removeTag
|
||||
? translate('RemoveTagRemovingTag')
|
||||
: translate('RemoveTagExistingTag')
|
||||
}
|
||||
kind={removeTag ? kinds.INVERSE : kinds.INFO}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
@@ -140,7 +145,7 @@ function TagsModalContent(props: TagsModalContentProps) {
|
||||
return (
|
||||
<Label
|
||||
key={tag.id}
|
||||
title={'Adding tag'}
|
||||
title={translate('AddingTag')}
|
||||
kind={kinds.SUCCESS}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
|
||||
@@ -4,6 +4,7 @@ import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import selectTableOptions from './selectTableOptions';
|
||||
|
||||
interface IndexerIndexTableOptionsProps {
|
||||
@@ -32,13 +33,13 @@ function IndexerIndexTableOptions(props: IndexerIndexTableOptionsProps) {
|
||||
return (
|
||||
<Fragment>
|
||||
<FormGroup>
|
||||
<FormLabel>Show Search</FormLabel>
|
||||
<FormLabel>{translate('ShowSearch')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showSearchAction"
|
||||
value={showSearchAction}
|
||||
helpText="Show search button on hover"
|
||||
helpText={translate('ShowSearchHelpText')}
|
||||
onChange={onTableOptionChangeWrapper}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
@@ -33,8 +33,8 @@ function IndexerStatusCell(props: IndexerStatusCellProps) {
|
||||
const enableKind = redirect ? kinds.INFO : kinds.SUCCESS;
|
||||
const enableIcon = redirect ? icons.REDIRECT : icons.CHECK;
|
||||
const enableTitle = redirect
|
||||
? 'Indexer is Enabled, Redirect is Enabled'
|
||||
: 'Indexer is Enabled';
|
||||
? translate('EnabledRedirected')
|
||||
: translate('Enabled');
|
||||
|
||||
return (
|
||||
<Component className={className} {...otherProps}>
|
||||
@@ -43,7 +43,7 @@ function IndexerStatusCell(props: IndexerStatusCellProps) {
|
||||
className={styles.statusIcon}
|
||||
kind={enabled ? enableKind : kinds.DEFAULT}
|
||||
name={enabled ? enableIcon : icons.BLOCKLIST}
|
||||
title={enabled ? enableTitle : 'Indexer is Disabled'}
|
||||
title={enabled ? enableTitle : translate('EnabledIndexerIsDisabled')}
|
||||
/>
|
||||
}
|
||||
{status ? (
|
||||
|
||||
@@ -10,6 +10,7 @@ import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import { align, kinds } from 'Helpers/Props';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import StatsFilterMenu from './StatsFilterMenu';
|
||||
import styles from './Stats.css';
|
||||
|
||||
@@ -188,53 +189,53 @@ function Stats(props) {
|
||||
<div className={styles.fullWidthChart}>
|
||||
<BarChart
|
||||
data={getAverageResponseTimeData(item.indexers)}
|
||||
title='Average Response Times (ms)'
|
||||
title={translate('AverageResponseTimesMs')}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.fullWidthChart}>
|
||||
<BarChart
|
||||
data={getFailureRateData(item.indexers)}
|
||||
title='Indexer Failure Rate'
|
||||
title={translate('IndexerFailureRate')}
|
||||
kind={kinds.WARNING}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.halfWidthChart}>
|
||||
<StackedBarChart
|
||||
data={getTotalRequestsData(item.indexers)}
|
||||
title='Total Indexer Queries'
|
||||
title={translate('TotalIndexerQueries')}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.halfWidthChart}>
|
||||
<BarChart
|
||||
data={getNumberGrabsData(item.indexers)}
|
||||
title='Total Indexer Successful Grabs'
|
||||
title={translate('TotalIndexerSuccessfulGrabs')}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.halfWidthChart}>
|
||||
<BarChart
|
||||
data={getUserAgentQueryData(item.userAgents)}
|
||||
title='Total User Agent Queries'
|
||||
title={translate('TotalUserAgentQueries')}
|
||||
horizontal={true}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.halfWidthChart}>
|
||||
<BarChart
|
||||
data={getUserAgentGrabsData(item.userAgents)}
|
||||
title='Total User Agent Grabs'
|
||||
title={translate('TotalUserAgentGrabs')}
|
||||
horizontal={true}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.halfWidthChart}>
|
||||
<DoughnutChart
|
||||
data={getHostQueryData(item.hosts)}
|
||||
title='Total Host Queries'
|
||||
title={translate('TotalHostQueries')}
|
||||
horizontal={true}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.halfWidthChart}>
|
||||
<DoughnutChart
|
||||
data={getHostGrabsData(item.hosts)}
|
||||
title='Total Host Grabs'
|
||||
title={translate('TotalHostGrabs')}
|
||||
horizontal={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -17,6 +17,7 @@ function TagDetailsModalContent(props) {
|
||||
indexers,
|
||||
notifications,
|
||||
indexerProxies,
|
||||
applications,
|
||||
onModalClose,
|
||||
onDeleteTagPress
|
||||
} = props;
|
||||
@@ -79,6 +80,21 @@ function TagDetailsModalContent(props) {
|
||||
}
|
||||
</FieldSet>
|
||||
}
|
||||
|
||||
{
|
||||
!!applications.length &&
|
||||
<FieldSet legend={translate('Applications')}>
|
||||
{
|
||||
applications.map((item) => {
|
||||
return (
|
||||
<div key={item.id}>
|
||||
{item.name}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</FieldSet>
|
||||
}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
@@ -110,6 +126,7 @@ TagDetailsModalContent.propTypes = {
|
||||
indexers: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
notifications: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
indexerProxies: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
applications: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onDeleteTagPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -18,16 +18,24 @@ function createMatchingIndexersSelector() {
|
||||
|
||||
function createMatchingIndexerProxiesSelector() {
|
||||
return createSelector(
|
||||
(state, { notificationIds }) => notificationIds,
|
||||
(state) => state.settings.notifications.items,
|
||||
(state, { indexerProxyIds }) => indexerProxyIds,
|
||||
(state) => state.settings.indexerProxies.items,
|
||||
findMatchingItems
|
||||
);
|
||||
}
|
||||
|
||||
function createMatchingNotificationsSelector() {
|
||||
return createSelector(
|
||||
(state, { indexerProxyIds }) => indexerProxyIds,
|
||||
(state) => state.settings.indexerProxies.items,
|
||||
(state, { notificationIds }) => notificationIds,
|
||||
(state) => state.settings.notifications.items,
|
||||
findMatchingItems
|
||||
);
|
||||
}
|
||||
|
||||
function createMatchingApplicationsSelector() {
|
||||
return createSelector(
|
||||
(state, { applicationIds }) => applicationIds,
|
||||
(state) => state.settings.applications.items,
|
||||
findMatchingItems
|
||||
);
|
||||
}
|
||||
@@ -37,11 +45,13 @@ function createMapStateToProps() {
|
||||
createMatchingIndexersSelector(),
|
||||
createMatchingIndexerProxiesSelector(),
|
||||
createMatchingNotificationsSelector(),
|
||||
(indexers, indexerProxies, notifications) => {
|
||||
createMatchingApplicationsSelector(),
|
||||
(indexers, indexerProxies, notifications, applications) => {
|
||||
return {
|
||||
indexers,
|
||||
indexerProxies,
|
||||
notifications
|
||||
notifications,
|
||||
applications
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@@ -55,7 +55,8 @@ class Tag extends Component {
|
||||
label,
|
||||
notificationIds,
|
||||
indexerIds,
|
||||
indexerProxyIds
|
||||
indexerProxyIds,
|
||||
applicationIds
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@@ -66,7 +67,8 @@ class Tag extends Component {
|
||||
const isTagUsed = !!(
|
||||
indexerIds.length ||
|
||||
notificationIds.length ||
|
||||
indexerProxyIds.length
|
||||
indexerProxyIds.length ||
|
||||
applicationIds.length
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -102,6 +104,13 @@ class Tag extends Component {
|
||||
{indexerProxyIds.length} {indexerProxyIds.length > 1 ? translate('IndexerProxies') : translate('IndexerProxy')}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
!!applicationIds.length &&
|
||||
<div>
|
||||
{applicationIds.length} {applicationIds.length > 1 ? translate('Applications') : translate('Application')}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -118,6 +127,7 @@ class Tag extends Component {
|
||||
indexerIds={indexerIds}
|
||||
notificationIds={notificationIds}
|
||||
indexerProxyIds={indexerProxyIds}
|
||||
applicationIds={applicationIds}
|
||||
isOpen={isDetailsModalOpen}
|
||||
onModalClose={this.onDetailsModalClose}
|
||||
onDeleteTagPress={this.onDeleteTagPress}
|
||||
@@ -143,13 +153,15 @@ Tag.propTypes = {
|
||||
notificationIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
indexerIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
indexerProxyIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
applicationIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
onConfirmDeleteTag: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
Tag.defaultProps = {
|
||||
indexerIds: [],
|
||||
notificationIds: [],
|
||||
indexerProxyIds: []
|
||||
indexerProxyIds: [],
|
||||
applicationIds: []
|
||||
};
|
||||
|
||||
export default Tag;
|
||||
|
||||
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchIndexerProxies, fetchNotifications } from 'Store/Actions/settingsActions';
|
||||
import { fetchApplications, fetchIndexerProxies, fetchNotifications } from 'Store/Actions/settingsActions';
|
||||
import { fetchTagDetails } from 'Store/Actions/tagActions';
|
||||
import Tags from './Tags';
|
||||
|
||||
@@ -27,7 +27,8 @@ function createMapStateToProps() {
|
||||
const mapDispatchToProps = {
|
||||
dispatchFetchTagDetails: fetchTagDetails,
|
||||
dispatchFetchNotifications: fetchNotifications,
|
||||
dispatchFetchIndexerProxies: fetchIndexerProxies
|
||||
dispatchFetchIndexerProxies: fetchIndexerProxies,
|
||||
dispatchFetchApplications: fetchApplications
|
||||
};
|
||||
|
||||
class MetadatasConnector extends Component {
|
||||
@@ -39,12 +40,14 @@ class MetadatasConnector extends Component {
|
||||
const {
|
||||
dispatchFetchTagDetails,
|
||||
dispatchFetchNotifications,
|
||||
dispatchFetchIndexerProxies
|
||||
dispatchFetchIndexerProxies,
|
||||
dispatchFetchApplications
|
||||
} = this.props;
|
||||
|
||||
dispatchFetchTagDetails();
|
||||
dispatchFetchNotifications();
|
||||
dispatchFetchIndexerProxies();
|
||||
dispatchFetchApplications();
|
||||
}
|
||||
|
||||
//
|
||||
@@ -62,7 +65,8 @@ class MetadatasConnector extends Component {
|
||||
MetadatasConnector.propTypes = {
|
||||
dispatchFetchTagDetails: PropTypes.func.isRequired,
|
||||
dispatchFetchNotifications: PropTypes.func.isRequired,
|
||||
dispatchFetchIndexerProxies: PropTypes.func.isRequired
|
||||
dispatchFetchIndexerProxies: PropTypes.func.isRequired,
|
||||
dispatchFetchApplications: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(MetadatasConnector);
|
||||
|
||||
@@ -144,7 +144,7 @@ export const defaultState = {
|
||||
},
|
||||
|
||||
category: function(item) {
|
||||
if (item.categories.length > 0) {
|
||||
if (item.categories !== undefined && item.categories.length > 0) {
|
||||
const sortedCats = item.categories.filter((cat) => cat.name !== undefined).sort((c) => c.id);
|
||||
const firstCat = sortedCats[0];
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
@define-mixin scrollbar {
|
||||
scrollbar-color: var(--scrollbarBackgroundColor) transparent;
|
||||
scrollbar-width: thin;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
|
||||
@@ -74,29 +74,29 @@ module.exports = {
|
||||
|
||||
defaultButtonTextColor: '#eee',
|
||||
defaultButtonBackgroundColor: '#333',
|
||||
defaultBorderColor: '#eaeaea',
|
||||
defaultBorderColor: '#393f45',
|
||||
defaultHoverBackgroundColor: '#444',
|
||||
defaultHoverBorderColor: '#d6d6d6;',
|
||||
defaultHoverBorderColor: '#5a6265',
|
||||
|
||||
primaryBackgroundColor: '#5d9cec',
|
||||
primaryBorderColor: '#5899eb',
|
||||
primaryHoverBackgroundColor: '#4b91ea',
|
||||
primaryHoverBorderColor: '#3483e7;',
|
||||
primaryHoverBorderColor: '#3483e7',
|
||||
|
||||
successBackgroundColor: '#27c24c',
|
||||
successBorderColor: '#26be4a',
|
||||
successHoverBackgroundColor: '#24b145',
|
||||
successHoverBorderColor: '#1f9c3d;',
|
||||
successHoverBorderColor: '#1f9c3d',
|
||||
|
||||
warningBackgroundColor: '#ff902b',
|
||||
warningBorderColor: '#ff8d26',
|
||||
warningHoverBackgroundColor: '#ff8517',
|
||||
warningHoverBorderColor: '#fc7800;',
|
||||
warningHoverBorderColor: '#fc7800',
|
||||
|
||||
dangerBackgroundColor: '#f05050',
|
||||
dangerBorderColor: '#f04b4b',
|
||||
dangerHoverBackgroundColor: '#ee3d3d',
|
||||
dangerHoverBorderColor: '#ec2626;',
|
||||
dangerHoverBorderColor: '#ec2626',
|
||||
|
||||
iconButtonDisabledColor: '#7a7a7a',
|
||||
iconButtonHoverColor: '#666',
|
||||
|
||||
@@ -76,27 +76,27 @@ module.exports = {
|
||||
defaultButtonBackgroundColor: '#fff',
|
||||
defaultBorderColor: '#eaeaea',
|
||||
defaultHoverBackgroundColor: '#f5f5f5',
|
||||
defaultHoverBorderColor: '#d6d6d6;',
|
||||
defaultHoverBorderColor: '#d6d6d6',
|
||||
|
||||
primaryBackgroundColor: '#5d9cec',
|
||||
primaryBorderColor: '#5899eb',
|
||||
primaryHoverBackgroundColor: '#4b91ea',
|
||||
primaryHoverBorderColor: '#3483e7;',
|
||||
primaryHoverBorderColor: '#3483e7',
|
||||
|
||||
successBackgroundColor: '#27c24c',
|
||||
successBorderColor: '#26be4a',
|
||||
successHoverBackgroundColor: '#24b145',
|
||||
successHoverBorderColor: '#1f9c3d;',
|
||||
successHoverBorderColor: '#1f9c3d',
|
||||
|
||||
warningBackgroundColor: '#ff902b',
|
||||
warningBorderColor: '#ff8d26',
|
||||
warningHoverBackgroundColor: '#ff8517',
|
||||
warningHoverBorderColor: '#fc7800;',
|
||||
warningHoverBorderColor: '#fc7800',
|
||||
|
||||
dangerBackgroundColor: '#f05050',
|
||||
dangerBorderColor: '#f04b4b',
|
||||
dangerHoverBackgroundColor: '#ee3d3d',
|
||||
dangerHoverBorderColor: '#ec2626;',
|
||||
dangerHoverBorderColor: '#ec2626',
|
||||
|
||||
iconButtonDisabledColor: '#7a7a7a',
|
||||
iconButtonHoverColor: '#666',
|
||||
|
||||
98
package.json
98
package.json
@@ -5,7 +5,7 @@
|
||||
"scripts": {
|
||||
"build": "webpack --config ./frontend/build/webpack.config.js",
|
||||
"prebuild": "yarn clean",
|
||||
"clean": "rimraf ./_output/UI && rimraf \"**/*.js.map\"",
|
||||
"clean": "rimraf ./_output/UI && rimraf -g \"**/*.js.map\"",
|
||||
"start": "webpack --watch --config ./frontend/build/webpack.config.js",
|
||||
"watch": "webpack --watch --config ./frontend/build/webpack.config.js",
|
||||
"lint": "eslint --config frontend/.eslintrc.js --ignore-path frontend/.eslintignore frontend/",
|
||||
@@ -20,42 +20,39 @@
|
||||
"readmeFilename": "readme.md",
|
||||
"main": "index.js",
|
||||
"browserslist": [
|
||||
">0.25%",
|
||||
"not ie 11",
|
||||
"not op_mini all",
|
||||
"not chrome < 60"
|
||||
"defaults"
|
||||
],
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "6.2.1",
|
||||
"@fortawesome/fontawesome-svg-core": "6.2.1",
|
||||
"@fortawesome/free-regular-svg-icons": "6.2.1",
|
||||
"@fortawesome/free-solid-svg-icons": "6.2.1",
|
||||
"@fortawesome/fontawesome-free": "6.4.0",
|
||||
"@fortawesome/fontawesome-svg-core": "6.4.0",
|
||||
"@fortawesome/free-regular-svg-icons": "6.4.0",
|
||||
"@fortawesome/free-solid-svg-icons": "6.4.0",
|
||||
"@fortawesome/react-fontawesome": "0.2.0",
|
||||
"@juggle/resize-observer": "3.4.0",
|
||||
"@microsoft/signalr": "6.0.13",
|
||||
"@sentry/browser": "7.28.0",
|
||||
"@sentry/integrations": "7.28.0",
|
||||
"@types/jest": "29.2.5",
|
||||
"@types/node": "18.11.18",
|
||||
"@types/react": "18.0.26",
|
||||
"@types/react-dom": "18.0.10",
|
||||
"chart.js": "4.1.1",
|
||||
"@microsoft/signalr": "6.0.16",
|
||||
"@sentry/browser": "7.46.0",
|
||||
"@sentry/integrations": "7.46.0",
|
||||
"@types/jest": "29.5.0",
|
||||
"@types/node": "18.15.11",
|
||||
"@types/react": "18.0.31",
|
||||
"@types/react-dom": "18.0.11",
|
||||
"chart.js": "4.2.1",
|
||||
"classnames": "2.3.2",
|
||||
"clipboard": "2.0.11",
|
||||
"connected-react-router": "6.9.3",
|
||||
"element-class": "0.2.2",
|
||||
"filesize": "10.0.6",
|
||||
"filesize": "10.0.7",
|
||||
"history": "4.10.1",
|
||||
"https-browserify": "1.0.0",
|
||||
"jdu": "1.0.0",
|
||||
"jquery": "3.6.2",
|
||||
"jquery": "3.6.4",
|
||||
"lodash": "4.17.21",
|
||||
"mobile-detect": "1.4.5",
|
||||
"moment": "2.29.4",
|
||||
"mousetrap": "1.6.5",
|
||||
"normalize.css": "8.0.1",
|
||||
"prop-types": "15.8.1",
|
||||
"qs": "6.11.0",
|
||||
"qs": "6.11.1",
|
||||
"react": "17.0.2",
|
||||
"react-addons-shallow-compare": "15.6.3",
|
||||
"react-async-script": "1.2.0",
|
||||
@@ -67,7 +64,7 @@
|
||||
"react-dnd-touch-backend": "14.1.1",
|
||||
"react-document-title": "2.0.3",
|
||||
"react-dom": "17.0.2",
|
||||
"react-focus-lock": "2.9.2",
|
||||
"react-focus-lock": "2.9.4",
|
||||
"react-google-recaptcha": "2.1.0",
|
||||
"react-lazyload": "3.2.0",
|
||||
"react-measure": "1.4.7",
|
||||
@@ -77,79 +74,78 @@
|
||||
"react-router-dom": "5.2.0",
|
||||
"react-text-truncate": "0.19.0",
|
||||
"react-use-measure": "2.1.1",
|
||||
"react-virtualized": "9.21.1",
|
||||
"react-virtualized": "9.22.3",
|
||||
"react-window": "1.8.8",
|
||||
"redux": "4.2.0",
|
||||
"redux": "4.2.1",
|
||||
"redux-actions": "2.6.5",
|
||||
"redux-batched-actions": "0.5.0",
|
||||
"redux-localstorage": "0.4.1",
|
||||
"redux-thunk": "2.4.2",
|
||||
"reselect": "4.1.7",
|
||||
"stacktrace-js": "2.0.2",
|
||||
"typescript": "4.9.4"
|
||||
"typescript": "5.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.20.5",
|
||||
"@babel/eslint-parser": "7.19.1",
|
||||
"@babel/core": "7.21.3",
|
||||
"@babel/eslint-parser": "7.21.3",
|
||||
"@babel/plugin-proposal-class-properties": "7.18.6",
|
||||
"@babel/plugin-proposal-decorators": "7.20.5",
|
||||
"@babel/plugin-proposal-decorators": "7.21.0",
|
||||
"@babel/plugin-proposal-export-default-from": "7.18.10",
|
||||
"@babel/plugin-proposal-export-namespace-from": "7.18.9",
|
||||
"@babel/plugin-proposal-function-sent": "7.18.6",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "7.18.6",
|
||||
"@babel/plugin-proposal-numeric-separator": "7.18.6",
|
||||
"@babel/plugin-proposal-optional-chaining": "7.18.9",
|
||||
"@babel/plugin-proposal-optional-chaining": "7.21.0",
|
||||
"@babel/plugin-proposal-throw-expressions": "7.18.6",
|
||||
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
||||
"@babel/preset-env": "7.20.2",
|
||||
"@babel/preset-react": "7.18.6",
|
||||
"@babel/preset-typescript": "7.18.6",
|
||||
"@babel/preset-typescript": "7.21.0",
|
||||
"@types/react-window": "1.8.5",
|
||||
"@typescript-eslint/eslint-plugin": "5.48.1",
|
||||
"@typescript-eslint/parser": "5.48.0",
|
||||
"@typescript-eslint/eslint-plugin": "5.57.0",
|
||||
"@typescript-eslint/parser": "5.57.0",
|
||||
"are-you-es5": "2.1.2",
|
||||
"autoprefixer": "10.4.13",
|
||||
"babel-eslint": "10.1.0",
|
||||
"babel-loader": "9.1.0",
|
||||
"autoprefixer": "10.4.14",
|
||||
"babel-loader": "9.1.2",
|
||||
"babel-plugin-inline-classnames": "2.0.1",
|
||||
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
|
||||
"core-js": "3.26.1",
|
||||
"core-js": "3.30.1",
|
||||
"css-loader": "6.7.3",
|
||||
"css-modules-typescript-loader": "4.0.1",
|
||||
"eslint": "8.30.0",
|
||||
"eslint-config-prettier": "8.6.0",
|
||||
"eslint": "8.37.0",
|
||||
"eslint-config-prettier": "8.8.0",
|
||||
"eslint-plugin-filenames": "1.3.2",
|
||||
"eslint-plugin-import": "2.26.0",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"eslint-plugin-prettier": "4.2.1",
|
||||
"eslint-plugin-react": "7.31.11",
|
||||
"eslint-plugin-react": "7.32.2",
|
||||
"eslint-plugin-react-hooks": "4.6.0",
|
||||
"eslint-plugin-simple-import-sort": "8.0.0",
|
||||
"eslint-plugin-simple-import-sort": "10.0.0",
|
||||
"file-loader": "6.2.0",
|
||||
"filemanager-webpack-plugin": "8.0.0",
|
||||
"fork-ts-checker-webpack-plugin": "7.2.14",
|
||||
"fork-ts-checker-webpack-plugin": "8.0.0",
|
||||
"html-webpack-plugin": "5.5.0",
|
||||
"loader-utils": "^3.2.1",
|
||||
"mini-css-extract-plugin": "2.7.2",
|
||||
"postcss": "8.4.20",
|
||||
"mini-css-extract-plugin": "2.7.5",
|
||||
"postcss": "8.4.21",
|
||||
"postcss-color-function": "4.1.0",
|
||||
"postcss-loader": "7.0.2",
|
||||
"postcss-loader": "7.1.0",
|
||||
"postcss-mixins": "9.0.4",
|
||||
"postcss-nested": "6.0.0",
|
||||
"postcss-nested": "6.0.1",
|
||||
"postcss-simple-vars": "7.0.1",
|
||||
"postcss-url": "10.1.3",
|
||||
"prettier": "2.8.2",
|
||||
"prettier": "2.8.7",
|
||||
"require-nocache": "1.0.0",
|
||||
"rimraf": "3.0.2",
|
||||
"rimraf": "4.4.1",
|
||||
"run-sequence": "2.2.1",
|
||||
"streamqueue": "1.1.2",
|
||||
"style-loader": "3.3.1",
|
||||
"style-loader": "3.3.2",
|
||||
"stylelint": "14.16.0",
|
||||
"stylelint-order": "5.0.0",
|
||||
"ts-loader": "9.4.2",
|
||||
"typescript-plugin-css-modules": "4.1.1",
|
||||
"typescript-plugin-css-modules": "5.0.0",
|
||||
"url-loader": "4.1.1",
|
||||
"webpack": "5.75.0",
|
||||
"webpack-cli": "5.0.1",
|
||||
"webpack": "5.82.0",
|
||||
"webpack-cli": "5.0.2",
|
||||
"webpack-livereload-plugin": "3.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +74,8 @@
|
||||
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
|
||||
|
||||
<Deterministic Condition="$(AssemblyVersion.EndsWith('*'))">False</Deterministic>
|
||||
|
||||
<PathMap>$(MSBuildProjectDirectory)=./$(MSBuildProjectName)/</PathMap>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Set the AssemblyConfiguration attribute for projects -->
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.EnsureThat;
|
||||
using NzbDrone.Test.Common;
|
||||
|
||||
@@ -12,14 +13,14 @@ namespace NzbDrone.Common.Test.EnsureTest
|
||||
public void EnsureWindowsPath(string path)
|
||||
{
|
||||
WindowsOnly();
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||
}
|
||||
|
||||
[TestCase(@"/var/user/file with, comma.mkv")]
|
||||
public void EnsureLinuxPath(string path)
|
||||
{
|
||||
PosixOnly();
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
101
src/NzbDrone.Common.Test/Http/CookieUtilFixture.cs
Normal file
101
src/NzbDrone.Common.Test/Http/CookieUtilFixture.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Http;
|
||||
|
||||
namespace NzbDrone.Common.Test.Http
|
||||
{
|
||||
[TestFixture]
|
||||
public class CookieUtilFixture
|
||||
{
|
||||
[Test]
|
||||
public void CookieHeaderToDictionaryGood()
|
||||
{
|
||||
// valid cookies with non-alpha characters in the value
|
||||
var cookieHeader = "__cfduid=d6237f041586694295; __cf_bm=TlOng/xyqckk-TMen38z+0RFYA7YA=";
|
||||
var expectedCookieDictionary = new Dictionary<string, string>
|
||||
{
|
||||
{ "__cfduid", "d6237f041586694295" },
|
||||
{ "__cf_bm", "TlOng/xyqckk-TMen38z+0RFYA7YA=" }
|
||||
};
|
||||
CollectionAssert.AreEqual(expectedCookieDictionary, CookieUtil.CookieHeaderToDictionary(cookieHeader));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CookieHeaderToDictionaryDuplicateKeys()
|
||||
{
|
||||
// cookie with duplicate keys and whitespace separator instead of ;
|
||||
var cookieHeader = "__cfduid=d6237f041586694295; __cf_bm=TlOng/xyqckk-TMen38z+0RFYA7YA= __cf_bm=test";
|
||||
var expectedCookieDictionary = new Dictionary<string, string>
|
||||
{
|
||||
{ "__cfduid", "d6237f041586694295" },
|
||||
{ "__cf_bm", "test" } // we always assume the latest value is the most recent
|
||||
};
|
||||
CollectionAssert.AreEqual(expectedCookieDictionary, CookieUtil.CookieHeaderToDictionary(cookieHeader));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CookieHeaderToDictionaryMalformed()
|
||||
{
|
||||
// malformed cookies
|
||||
var cookieHeader = "__cfduidd6237f041586694295; __cf_;bm TlOng; good_cookie=value";
|
||||
var expectedCookieDictionary = new Dictionary<string, string> { { "good_cookie", "value" }, };
|
||||
CollectionAssert.AreEqual(expectedCookieDictionary, CookieUtil.CookieHeaderToDictionary(cookieHeader));
|
||||
}
|
||||
|
||||
[Test]
|
||||
[SuppressMessage("ReSharper", "CollectionNeverUpdated.Local")]
|
||||
public void CookieHeaderToDictionaryNull()
|
||||
{
|
||||
// null cookie header
|
||||
var expectedCookieDictionary = new Dictionary<string, string>();
|
||||
CollectionAssert.AreEqual(expectedCookieDictionary, CookieUtil.CookieHeaderToDictionary(null));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CookieDictionaryToHeaderGood()
|
||||
{
|
||||
// valid cookies with non-alpha characters in the value
|
||||
var cookieDictionary = new Dictionary<string, string>
|
||||
{
|
||||
{ "__cfduid", "d6237f041586694295" },
|
||||
{ "__cf_bm", "TlOng/xyqckk-TMen38z+0RFYA7YA=" }
|
||||
};
|
||||
var expectedCookieHeader = "__cfduid=d6237f041586694295; __cf_bm=TlOng/xyqckk-TMen38z+0RFYA7YA=";
|
||||
CollectionAssert.AreEqual(expectedCookieHeader, CookieUtil.CookieDictionaryToHeader(cookieDictionary));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CookieDictionaryToHeaderMalformed1()
|
||||
{
|
||||
// malformed key
|
||||
var cookieDictionary = new Dictionary<string, string>
|
||||
{
|
||||
{ "__cf_=bm", "34234234" }
|
||||
};
|
||||
var ex = Assert.Throws<FormatException>(() => CookieUtil.CookieDictionaryToHeader(cookieDictionary));
|
||||
Assert.AreEqual("The cookie '__cf_=bm=34234234' is malformed.", ex.Message);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CookieDictionaryToHeaderMalformed2()
|
||||
{
|
||||
// malformed value
|
||||
var cookieDictionary = new Dictionary<string, string>
|
||||
{
|
||||
{ "__cf_bm", "34234 234" }
|
||||
};
|
||||
var ex = Assert.Throws<FormatException>(() => CookieUtil.CookieDictionaryToHeader(cookieDictionary));
|
||||
Assert.AreEqual("The cookie '__cf_bm=34234 234' is malformed.", ex.Message);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CookieDictionaryToHeaderNull()
|
||||
{
|
||||
// null cookie dictionary
|
||||
var expectedCookieHeader = "";
|
||||
CollectionAssert.AreEqual(expectedCookieHeader, CookieUtil.CookieDictionaryToHeader(null));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ namespace NzbDrone.Common.Test
|
||||
[TestCase(@"\\Testserver\Test\file.ext", @"\\Testserver\Test\file.ext")]
|
||||
[TestCase(@"\\Testserver\Test\file.ext\\", @"\\Testserver\Test\file.ext")]
|
||||
[TestCase(@"\\Testserver\Test\file.ext \\", @"\\Testserver\Test\file.ext")]
|
||||
[TestCase(@"//CAPITAL//lower// ", @"\\CAPITAL\lower")]
|
||||
public void Clean_Path_Windows(string dirty, string clean)
|
||||
{
|
||||
WindowsOnly();
|
||||
|
||||
@@ -5,8 +5,6 @@ using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Loader;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
|
||||
namespace NzbDrone.Common.Composition
|
||||
@@ -19,16 +17,17 @@ namespace NzbDrone.Common.Composition
|
||||
RegisterSQLiteResolver();
|
||||
}
|
||||
|
||||
public static IEnumerable<Assembly> Load(IEnumerable<string> assemblies)
|
||||
public static IList<Assembly> Load(IList<string> assemblyNames)
|
||||
{
|
||||
var toLoad = assemblies.ToList();
|
||||
var toLoad = assemblyNames.ToList();
|
||||
toLoad.Add("Prowlarr.Common");
|
||||
toLoad.Add(OsInfo.IsWindows ? "Prowlarr.Windows" : "Prowlarr.Mono");
|
||||
|
||||
var startupPath = AppDomain.CurrentDomain.BaseDirectory;
|
||||
|
||||
return toLoad.Select(x =>
|
||||
AssemblyLoadContext.Default.LoadFromAssemblyPath(Path.Combine(startupPath, $"{x}.dll")));
|
||||
return toLoad
|
||||
.Select(x => AssemblyLoadContext.Default.LoadFromAssemblyPath(Path.Combine(startupPath, $"{x}.dll")))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static Assembly ContainerResolveEventHandler(object sender, ResolveEventArgs args)
|
||||
|
||||
@@ -65,7 +65,7 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
private void CheckFolderExists(string path)
|
||||
{
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
if (!FolderExists(path))
|
||||
{
|
||||
@@ -75,7 +75,7 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
private void CheckFileExists(string path)
|
||||
{
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
if (!FileExists(path))
|
||||
{
|
||||
@@ -93,19 +93,19 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
public bool FolderExists(string path)
|
||||
{
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||
return Directory.Exists(path);
|
||||
}
|
||||
|
||||
public bool FileExists(string path)
|
||||
{
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||
return FileExists(path, PathStringComparison);
|
||||
}
|
||||
|
||||
public bool FileExists(string path, StringComparison stringComparison)
|
||||
{
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
switch (stringComparison)
|
||||
{
|
||||
@@ -125,7 +125,7 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
public bool FolderWritable(string path)
|
||||
{
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -144,35 +144,35 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
public bool FolderEmpty(string path)
|
||||
{
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
return Directory.EnumerateFileSystemEntries(path).Empty();
|
||||
}
|
||||
|
||||
public string[] GetDirectories(string path)
|
||||
{
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
return Directory.GetDirectories(path);
|
||||
}
|
||||
|
||||
public string[] GetFiles(string path, SearchOption searchOption)
|
||||
{
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
return Directory.GetFiles(path, "*.*", searchOption);
|
||||
}
|
||||
|
||||
public long GetFolderSize(string path)
|
||||
{
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
return GetFiles(path, SearchOption.AllDirectories).Sum(e => new FileInfo(e).Length);
|
||||
}
|
||||
|
||||
public long GetFileSize(string path)
|
||||
{
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
if (!FileExists(path))
|
||||
{
|
||||
@@ -185,13 +185,13 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
public void CreateFolder(string path)
|
||||
{
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||
Directory.CreateDirectory(path);
|
||||
}
|
||||
|
||||
public void DeleteFile(string path)
|
||||
{
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||
Logger.Trace("Deleting file: {0}", path);
|
||||
|
||||
RemoveReadOnly(path);
|
||||
@@ -201,8 +201,8 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
public void CloneFile(string source, string destination, bool overwrite = false)
|
||||
{
|
||||
Ensure.That(source, () => source).IsValidPath();
|
||||
Ensure.That(destination, () => destination).IsValidPath();
|
||||
Ensure.That(source, () => source).IsValidPath(PathValidationType.CurrentOs);
|
||||
Ensure.That(destination, () => destination).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
if (source.PathEquals(destination))
|
||||
{
|
||||
@@ -219,8 +219,8 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
public void CopyFile(string source, string destination, bool overwrite = false)
|
||||
{
|
||||
Ensure.That(source, () => source).IsValidPath();
|
||||
Ensure.That(destination, () => destination).IsValidPath();
|
||||
Ensure.That(source, () => source).IsValidPath(PathValidationType.CurrentOs);
|
||||
Ensure.That(destination, () => destination).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
if (source.PathEquals(destination))
|
||||
{
|
||||
@@ -237,8 +237,8 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
public void MoveFile(string source, string destination, bool overwrite = false)
|
||||
{
|
||||
Ensure.That(source, () => source).IsValidPath();
|
||||
Ensure.That(destination, () => destination).IsValidPath();
|
||||
Ensure.That(source, () => source).IsValidPath(PathValidationType.CurrentOs);
|
||||
Ensure.That(destination, () => destination).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
if (source.PathEquals(destination))
|
||||
{
|
||||
@@ -256,8 +256,8 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
public void MoveFolder(string source, string destination, bool overwrite = false)
|
||||
{
|
||||
Ensure.That(source, () => source).IsValidPath();
|
||||
Ensure.That(destination, () => destination).IsValidPath();
|
||||
Ensure.That(source, () => source).IsValidPath(PathValidationType.CurrentOs);
|
||||
Ensure.That(destination, () => destination).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
Directory.Move(source, destination);
|
||||
}
|
||||
@@ -281,7 +281,7 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
public void DeleteFolder(string path, bool recursive)
|
||||
{
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
var files = Directory.GetFiles(path, "*.*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly);
|
||||
Array.ForEach(files, RemoveReadOnly);
|
||||
@@ -291,14 +291,14 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
public string ReadAllText(string filePath)
|
||||
{
|
||||
Ensure.That(filePath, () => filePath).IsValidPath();
|
||||
Ensure.That(filePath, () => filePath).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
return File.ReadAllText(filePath);
|
||||
}
|
||||
|
||||
public void WriteAllText(string filename, string contents)
|
||||
{
|
||||
Ensure.That(filename, () => filename).IsValidPath();
|
||||
Ensure.That(filename, () => filename).IsValidPath(PathValidationType.CurrentOs);
|
||||
RemoveReadOnly(filename);
|
||||
|
||||
// File.WriteAllText is broken on net core when writing to some CIFS mounts
|
||||
@@ -314,7 +314,7 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
public void FolderSetLastWriteTime(string path, DateTime dateTime)
|
||||
{
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
if (dateTime.Before(DateTimeExtensions.Epoch))
|
||||
{
|
||||
@@ -326,7 +326,7 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
public void FileSetLastWriteTime(string path, DateTime dateTime)
|
||||
{
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
if (dateTime.Before(DateTimeExtensions.Epoch))
|
||||
{
|
||||
@@ -351,16 +351,16 @@ namespace NzbDrone.Common.Disk
|
||||
}
|
||||
}
|
||||
|
||||
public string GetPathRoot(string path)
|
||||
public virtual string GetPathRoot(string path)
|
||||
{
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
return Path.GetPathRoot(path);
|
||||
}
|
||||
|
||||
public string GetParentFolder(string path)
|
||||
{
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
var parent = Directory.GetParent(path.TrimEnd(Path.DirectorySeparatorChar));
|
||||
|
||||
@@ -407,7 +407,7 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
public void EmptyFolder(string path)
|
||||
{
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
foreach (var file in GetFiles(path, SearchOption.TopDirectoryOnly))
|
||||
{
|
||||
@@ -478,8 +478,7 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
return mounts.Where(drive => drive.RootDirectory.PathEquals(path) ||
|
||||
drive.RootDirectory.IsParentPath(path))
|
||||
.OrderByDescending(drive => drive.RootDirectory.Length)
|
||||
.FirstOrDefault();
|
||||
.MaxBy(drive => drive.RootDirectory.Length);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -497,7 +496,7 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
public List<DirectoryInfo> GetDirectoryInfos(string path)
|
||||
{
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
var di = new DirectoryInfo(path);
|
||||
|
||||
@@ -506,14 +505,14 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
public FileInfo GetFileInfo(string path)
|
||||
{
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
return new FileInfo(path);
|
||||
}
|
||||
|
||||
public List<FileInfo> GetFileInfos(string path, SearchOption searchOption = SearchOption.TopDirectoryOnly)
|
||||
{
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
var di = new DirectoryInfo(path);
|
||||
|
||||
|
||||
@@ -43,8 +43,8 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
public TransferMode TransferFolder(string sourcePath, string targetPath, TransferMode mode)
|
||||
{
|
||||
Ensure.That(sourcePath, () => sourcePath).IsValidPath();
|
||||
Ensure.That(targetPath, () => targetPath).IsValidPath();
|
||||
Ensure.That(sourcePath, () => sourcePath).IsValidPath(PathValidationType.CurrentOs);
|
||||
Ensure.That(targetPath, () => targetPath).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
sourcePath = ResolveRealParentPath(sourcePath);
|
||||
targetPath = ResolveRealParentPath(targetPath);
|
||||
@@ -140,8 +140,8 @@ namespace NzbDrone.Common.Disk
|
||||
{
|
||||
var filesCopied = 0;
|
||||
|
||||
Ensure.That(sourcePath, () => sourcePath).IsValidPath();
|
||||
Ensure.That(targetPath, () => targetPath).IsValidPath();
|
||||
Ensure.That(sourcePath, () => sourcePath).IsValidPath(PathValidationType.CurrentOs);
|
||||
Ensure.That(targetPath, () => targetPath).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
sourcePath = ResolveRealParentPath(sourcePath);
|
||||
targetPath = ResolveRealParentPath(targetPath);
|
||||
@@ -255,8 +255,8 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
public TransferMode TransferFile(string sourcePath, string targetPath, TransferMode mode, bool overwrite = false)
|
||||
{
|
||||
Ensure.That(sourcePath, () => sourcePath).IsValidPath();
|
||||
Ensure.That(targetPath, () => targetPath).IsValidPath();
|
||||
Ensure.That(sourcePath, () => sourcePath).IsValidPath(PathValidationType.CurrentOs);
|
||||
Ensure.That(targetPath, () => targetPath).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
sourcePath = ResolveRealParentPath(sourcePath);
|
||||
targetPath = ResolveRealParentPath(targetPath);
|
||||
|
||||
@@ -71,7 +71,7 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
if (
|
||||
allowFoldersWithoutTrailingSlashes &&
|
||||
query.IsPathValid() &&
|
||||
query.IsPathValid(PathValidationType.CurrentOs) &&
|
||||
_diskProvider.FolderExists(query))
|
||||
{
|
||||
return GetResult(query, includeFiles);
|
||||
|
||||
@@ -162,7 +162,7 @@ namespace NzbDrone.Common.Disk
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsValid => _path.IsPathValid();
|
||||
public bool IsValid => _path.IsPathValid(PathValidationType.CurrentOs);
|
||||
|
||||
private int GetFileNameIndex()
|
||||
{
|
||||
|
||||
8
src/NzbDrone.Common/Disk/PathValidationType.cs
Normal file
8
src/NzbDrone.Common/Disk/PathValidationType.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace NzbDrone.Common.Disk
|
||||
{
|
||||
public enum PathValidationType
|
||||
{
|
||||
CurrentOs,
|
||||
AnyOs
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text.RegularExpressions;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.EnsureThat.Resources;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
@@ -111,14 +112,14 @@ namespace NzbDrone.Common.EnsureThat
|
||||
}
|
||||
|
||||
[DebuggerStepThrough]
|
||||
public static Param<string> IsValidPath(this Param<string> param)
|
||||
public static Param<string> IsValidPath(this Param<string> param, PathValidationType validationType)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(param.Value))
|
||||
{
|
||||
throw ExceptionFactory.CreateForParamValidation(param.Name, ExceptionMessages.EnsureExtensions_IsNotNullOrWhiteSpace);
|
||||
}
|
||||
|
||||
if (param.Value.IsPathValid())
|
||||
if (param.Value.IsPathValid(validationType))
|
||||
{
|
||||
return param;
|
||||
}
|
||||
|
||||
@@ -29,12 +29,12 @@ namespace NzbDrone.Common.Extensions
|
||||
public static string CleanFilePath(this string path)
|
||||
{
|
||||
Ensure.That(path, () => path).IsNotNullOrWhiteSpace();
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.AnyOs);
|
||||
|
||||
var info = new FileInfo(path.Trim());
|
||||
|
||||
// UNC
|
||||
if (OsInfo.IsWindows && info.FullName.StartsWith(@"\\"))
|
||||
if (!info.FullName.Contains('/') && info.FullName.StartsWith(@"\\"))
|
||||
{
|
||||
return info.FullName.TrimEnd('/', '\\', ' ');
|
||||
}
|
||||
@@ -136,24 +136,24 @@ namespace NzbDrone.Common.Extensions
|
||||
|
||||
private static readonly Regex WindowsPathWithDriveRegex = new Regex(@"^[a-zA-Z]:\\", RegexOptions.Compiled);
|
||||
|
||||
public static bool IsPathValid(this string path)
|
||||
public static bool IsPathValid(this string path, PathValidationType validationType)
|
||||
{
|
||||
if (path.ContainsInvalidPathChars() || string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (validationType == PathValidationType.AnyOs)
|
||||
{
|
||||
return IsPathValidForWindows(path) || IsPathValidForNonWindows(path);
|
||||
}
|
||||
|
||||
if (OsInfo.IsNotWindows)
|
||||
{
|
||||
return path.StartsWith(Path.DirectorySeparatorChar.ToString());
|
||||
return IsPathValidForNonWindows(path);
|
||||
}
|
||||
|
||||
if (path.StartsWith("\\") || WindowsPathWithDriveRegex.IsMatch(path))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return IsPathValidForWindows(path);
|
||||
}
|
||||
|
||||
public static bool ContainsInvalidPathChars(this string text)
|
||||
@@ -337,5 +337,15 @@ namespace NzbDrone.Common.Extensions
|
||||
{
|
||||
return Path.Combine(appFolderInfo.StartUpFolder, NLOG_CONFIG_FILE);
|
||||
}
|
||||
|
||||
private static bool IsPathValidForWindows(string path)
|
||||
{
|
||||
return path.StartsWith("\\") || WindowsPathWithDriveRegex.IsMatch(path);
|
||||
}
|
||||
|
||||
private static bool IsPathValidForNonWindows(string path)
|
||||
{
|
||||
return path.StartsWith("/");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,18 +16,7 @@ namespace NzbDrone.Common.Extensions
|
||||
return false;
|
||||
}
|
||||
|
||||
Uri uri;
|
||||
if (!Uri.TryCreate(path, UriKind.Absolute, out uri))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!uri.IsWellFormedOriginalString())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return Uri.TryCreate(path, UriKind.Absolute, out var uri) && uri.IsWellFormedOriginalString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,14 @@ namespace NzbDrone.Common
|
||||
return $"{mCrc:x8}";
|
||||
}
|
||||
|
||||
public static string ComputeSha256Hash(string rawData)
|
||||
{
|
||||
using var sha256Hash = SHA256.Create();
|
||||
var hashBytes = sha256Hash.ComputeHash(Encoding.UTF8.GetBytes(rawData));
|
||||
|
||||
return Convert.ToHexString(hashBytes);
|
||||
}
|
||||
|
||||
public static string CalculateMd5(string s)
|
||||
{
|
||||
// Use input string to calculate MD5 hash
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace NzbDrone.Common.Http
|
||||
{
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
|
||||
// NOTE: we are not checking non-ascii characters and we should
|
||||
private static readonly Regex _CookieRegex = new Regex(@"([^\(\)<>@,;:\\""/\[\]\?=\{\}\s]+)=([^,;\\""\s]+)");
|
||||
private static readonly Regex CookieRegex = new (@"([^\(\)<>@,;:\\""/\[\]\?=\{\}\s]+)=([^,;\\""\s]+)");
|
||||
private static readonly string[] FilterProps = { "COMMENT", "COMMENTURL", "DISCORD", "DOMAIN", "EXPIRES", "MAX-AGE", "PATH", "PORT", "SECURE", "VERSION", "HTTPONLY", "SAMESITE" };
|
||||
private static readonly char[] InvalidKeyChars = { '(', ')', '<', '>', '@', ',', ';', ':', '\\', '"', '/', '[', ']', '?', '=', '{', '}', ' ', '\t', '\n' };
|
||||
private static readonly char[] InvalidValueChars = { '"', ',', ';', '\\', ' ', '\t', '\n' };
|
||||
@@ -22,7 +22,7 @@ namespace NzbDrone.Common.Http
|
||||
return cookieDictionary;
|
||||
}
|
||||
|
||||
var matches = _CookieRegex.Match(cookieHeader);
|
||||
var matches = CookieRegex.Match(cookieHeader);
|
||||
while (matches.Success)
|
||||
{
|
||||
if (matches.Groups.Count > 2 && !FilterProps.Contains(matches.Groups[1].Value.ToUpperInvariant()))
|
||||
|
||||
@@ -21,6 +21,7 @@ namespace NzbDrone.Common.Http
|
||||
public Dictionary<string, string> Segments { get; private set; }
|
||||
public HttpHeader Headers { get; private set; }
|
||||
public bool SuppressHttpError { get; set; }
|
||||
public IEnumerable<HttpStatusCode> SuppressHttpErrorStatusCodes { get; set; }
|
||||
public bool LogHttpError { get; set; }
|
||||
public bool UseSimplifiedUserAgent { get; set; }
|
||||
public bool AllowAutoRedirect { get; set; }
|
||||
@@ -108,6 +109,7 @@ namespace NzbDrone.Common.Http
|
||||
request.Method = Method;
|
||||
request.Encoding = Encoding;
|
||||
request.SuppressHttpError = SuppressHttpError;
|
||||
request.SuppressHttpErrorStatusCodes = SuppressHttpErrorStatusCodes;
|
||||
request.LogHttpError = LogHttpError;
|
||||
request.UseSimplifiedUserAgent = UseSimplifiedUserAgent;
|
||||
request.AllowAutoRedirect = AllowAutoRedirect;
|
||||
|
||||
@@ -14,14 +14,14 @@ namespace NzbDrone.Common.OAuth
|
||||
{
|
||||
get
|
||||
{
|
||||
var parameters = this.Where(p => p.Name.Equals(name));
|
||||
var parameters = this.Where(p => p.Name.Equals(name)).ToArray();
|
||||
|
||||
if (!parameters.Any())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (parameters.Count() == 1)
|
||||
if (parameters.Length == 1)
|
||||
{
|
||||
return parameters.Single();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
|
||||
|
||||
@@ -4,16 +4,16 @@
|
||||
<DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DryIoc.dll" Version="5.3.3" />
|
||||
<PackageReference Include="DryIoc.dll" Version="5.3.4" />
|
||||
<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" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="NLog" Version="5.1.0" />
|
||||
<PackageReference Include="NLog.Extensions.Logging" Version="5.2.0" />
|
||||
<PackageReference Include="Npgsql" Version="5.0.11" />
|
||||
<PackageReference Include="Sentry" Version="3.24.1" />
|
||||
<PackageReference Include="Sentry" Version="3.29.1" />
|
||||
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
|
||||
<PackageReference Include="SharpZipLib" Version="1.3.3" />
|
||||
<PackageReference Include="SharpZipLib" Version="1.4.2" />
|
||||
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
||||
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.1" />
|
||||
|
||||
826
src/NzbDrone.Core.Test/Files/Indexers/AnimeBytes/recentfeed.json
Normal file
826
src/NzbDrone.Core.Test/Files/Indexers/AnimeBytes/recentfeed.json
Normal file
@@ -0,0 +1,826 @@
|
||||
{
|
||||
"Results": 9999,
|
||||
"Pagination": {
|
||||
"Current": 1,
|
||||
"Max": 99,
|
||||
"Limit": {
|
||||
"Min": 15,
|
||||
"Coerced": 15,
|
||||
"Max": 50
|
||||
}
|
||||
},
|
||||
"Matches": 2,
|
||||
"Groups": [
|
||||
{
|
||||
"ID": 575,
|
||||
"CategoryName": "Anime",
|
||||
"FullName": "Cowboy Bebop: Tengoku no Tobira - Movie [2001]",
|
||||
"GroupName": "Movie",
|
||||
"SeriesID": "141",
|
||||
"SeriesName": "Cowboy Bebop: Tengoku no Tobira",
|
||||
"Artists": null,
|
||||
"Year": "2001",
|
||||
"Image": "https://mei.animebytes.tv/2bac1a04148be77ce41251d3cb44bbd5.jpg",
|
||||
"Synonymns": [
|
||||
"カウボーイビバップ天国の扉",
|
||||
"Cowboy Bebop: Knockin' on Heaven's Door"
|
||||
],
|
||||
"SynonymnsV2": {
|
||||
"Japanese": "カウボーイビバップ天国の扉",
|
||||
"Romaji": "",
|
||||
"Alternative": "Cowboy Bebop: Knockin' on Heaven's Door"
|
||||
},
|
||||
"Snatched": 4900,
|
||||
"Comments": 11,
|
||||
"Links": {
|
||||
"AniDB": "https://anidb.net/anime/219",
|
||||
"ANN": "https://www.animenewsnetwork.com/encyclopedia/anime.php?id=353",
|
||||
"Wikipedia": "https://en.wikipedia.org/wiki/Cowboy_Bebop:_The_Movie",
|
||||
"MAL": "https://myanimelist.net/anime/5"
|
||||
},
|
||||
"Votes": 572,
|
||||
"AvgVote": 8.3,
|
||||
"Associations": null,
|
||||
"Description": "Mars is under siege! Just before Halloween 2071, a terrorist bomb destroys a tanker truck on Highway One, close to the densely-populated crater city. There are casualties up to half a mile from the blast — 500 killed or injured by what appears to be a biochemical weapon. The reward for the bomber's capture is a massive 300,000,000 woolongs... and there are four humans and a dog who really need the money. Down on their luck as usual, the crew of the Bebop get on the case.\r\n\r\n[i]Note: The movie takes place between (in the time period of) the Cowboy Bebop episodes 22 and 23.[/i]",
|
||||
"DescriptionHTML": "Mars is under siege! Just before Halloween 2071, a terrorist bomb destroys a tanker truck on Highway One, close to the densely-populated crater city. There are casualties up to half a mile from the blast — 500 killed or injured by what appears to be a biochemical weapon. The reward for the bomber's capture is a massive 300,000,000 woolongs... and there are four humans and a dog who really need the money. Down on their luck as usual, the crew of the Bebop get on the case.<br />\r\n<br />\r\n<em>Note: The movie takes place between (in the time period of) the Cowboy Bebop episodes 22 and 23.</em>",
|
||||
"EpCount": 0,
|
||||
"StudioList": "Sunrise///28|BONES///35",
|
||||
"PastWeek": 2,
|
||||
"Incomplete": false,
|
||||
"Ongoing": false,
|
||||
"Tags": [
|
||||
"adventure",
|
||||
"comedy",
|
||||
"drama",
|
||||
"scifi",
|
||||
"seinen",
|
||||
"action"
|
||||
],
|
||||
"Torrents": [
|
||||
{
|
||||
"ID": 959397,
|
||||
"EditionData": {
|
||||
"EditionTitle": ""
|
||||
},
|
||||
"RawDownMultiplier": 0,
|
||||
"RawUpMultiplier": 1,
|
||||
"Link": "https://animebytes.tv/torrent/959397/download/somepass",
|
||||
"Property": "Blu-ray | MKV | h265 10-bit | 1080p | Opus 5.1 | Softsubs (Polarwindz) | Freeleech",
|
||||
"Snatched": 16,
|
||||
"Seeders": 5,
|
||||
"Leechers": 1,
|
||||
"Status": 0,
|
||||
"Size": 13090646841,
|
||||
"FileCount": 1,
|
||||
"FileList": [
|
||||
{
|
||||
"filename": "[Polarwindz] Cowboy Bebop The Movie - Knockin' on Heaven's Door [BD 1080p x265 10bit Opus 5.1].mkv",
|
||||
"size": 13090646841
|
||||
}
|
||||
],
|
||||
"UploadTime": "2023-04-02 05:00:43"
|
||||
},
|
||||
{
|
||||
"ID": 909565,
|
||||
"EditionData": {
|
||||
"EditionTitle": ""
|
||||
},
|
||||
"RawDownMultiplier": 0,
|
||||
"RawUpMultiplier": 1,
|
||||
"Link": "https://animebytes.tv/torrent/909565/download/somepass",
|
||||
"Property": "Blu-ray | MKV | h265 10-bit | 1080p | Opus 5.1 | Dual Audio | Softsubs (Yūrei) | Freeleech",
|
||||
"Snatched": 29,
|
||||
"Seeders": 6,
|
||||
"Leechers": 0,
|
||||
"Status": 0,
|
||||
"Size": 15717521349,
|
||||
"FileCount": 1,
|
||||
"FileList": [
|
||||
{
|
||||
"filename": "Cowboy Bebop - Tengoku no Tobira.mkv",
|
||||
"size": 15717521349
|
||||
}
|
||||
],
|
||||
"UploadTime": "2020-09-03 18:04:38"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"ID": 2709,
|
||||
"CategoryName": "Anime",
|
||||
"FullName": "BLEACH - TV Series [2004]",
|
||||
"GroupName": "TV Series",
|
||||
"SeriesID": "191",
|
||||
"SeriesName": "BLEACH",
|
||||
"Artists": null,
|
||||
"Year": "2004",
|
||||
"Image": "https://mei.animebytes.tv/997c8ec3ca0e70254b182b0a176f0161.jpg",
|
||||
"Synonymns": [
|
||||
"ブリーチ"
|
||||
],
|
||||
"SynonymnsV2": {
|
||||
"Japanese": "ブリーチ",
|
||||
"Romaji": "",
|
||||
"Alternative": ""
|
||||
},
|
||||
"Snatched": 22653,
|
||||
"Comments": 51,
|
||||
"Links": {
|
||||
"AniDB": "https://anidb.net/anime/2369",
|
||||
"ANN": "https://www.animenewsnetwork.com/encyclopedia/anime.php?id=4240",
|
||||
"Wikipedia": "https://en.wikipedia.org/wiki/Bleach_(anime)",
|
||||
"MAL": "https://myanimelist.net/anime/269"
|
||||
},
|
||||
"Votes": 530,
|
||||
"AvgVote": 7.3,
|
||||
"Associations": null,
|
||||
"Description": "Kurosaki Ichigo is a teenager gifted with the ability to see spirits. His life is drastically changed by the sudden appearance of a Shinigami (literally, Death god) - one who governs the flow of souls between the human world and the afterlife - named Kuchiki Rukia, who arrives in search of a Hollow, a dangerous lost soul. When Rukia is severely wounded while trying to defeat the Hollow, she attempts to transfer half of her Reiatsu (literally, Spiritual pressure) energy to Ichigo so that he can defeat the Hollow. However, Ichigo takes almost all of her energy, transforming into a Shinigami and allowing him to defeat the Hollow with ease. With her powers diminished, Rukia is left stranded in the human world until she can recover her strength. In the meantime, Ichigo must take over Rukia's role as a Shinigami, battling Hollows and guiding souls to the afterlife realm known as the Soul Society.",
|
||||
"DescriptionHTML": "Kurosaki Ichigo is a teenager gifted with the ability to see spirits. His life is drastically changed by the sudden appearance of a Shinigami (literally, Death god) - one who governs the flow of souls between the human world and the afterlife - named Kuchiki Rukia, who arrives in search of a Hollow, a dangerous lost soul. When Rukia is severely wounded while trying to defeat the Hollow, she attempts to transfer half of her Reiatsu (literally, Spiritual pressure) energy to Ichigo so that he can defeat the Hollow. However, Ichigo takes almost all of her energy, transforming into a Shinigami and allowing him to defeat the Hollow with ease. With her powers diminished, Rukia is left stranded in the human world until she can recover her strength. In the meantime, Ichigo must take over Rukia's role as a Shinigami, battling Hollows and guiding souls to the afterlife realm known as the Soul Society.",
|
||||
"EpCount": 366,
|
||||
"StudioList": "Studio Pierrot///45",
|
||||
"PastWeek": 26,
|
||||
"Incomplete": false,
|
||||
"Ongoing": false,
|
||||
"Tags": [
|
||||
"adventure",
|
||||
"comedy",
|
||||
"fantasy",
|
||||
"martial.arts",
|
||||
"school.life",
|
||||
"shounen",
|
||||
"super.power",
|
||||
"contemporary.fantasy",
|
||||
"swordplay",
|
||||
"action",
|
||||
"supernatural"
|
||||
],
|
||||
"Torrents": [
|
||||
{
|
||||
"ID": 1031199,
|
||||
"EditionData": {
|
||||
"EditionTitle": "Season 02: The Entry (021-041)"
|
||||
},
|
||||
"RawDownMultiplier": 0,
|
||||
"RawUpMultiplier": 1,
|
||||
"Link": "https://animebytes.tv/torrent/1031199/download/somepass",
|
||||
"Property": "Blu-ray | MKV | h265 10-bit | 1080p | AAC 2.0 | Dual Audio | Softsubs (GHOST) | Freeleech",
|
||||
"Snatched": 20,
|
||||
"Seeders": 24,
|
||||
"Leechers": 0,
|
||||
"Status": 0,
|
||||
"Size": 19584943785,
|
||||
"FileCount": 21,
|
||||
"FileList": [
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 021 [BD HEVC 10bit Dual Audio AC3][035452C3].mkv",
|
||||
"size": 880693454
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 022 [BD HEVC 10bit Dual Audio AC3][0E923AAD].mkv",
|
||||
"size": 851531918
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 023 [BD HEVC 10bit Dual Audio AC3][604A0EC6].mkv",
|
||||
"size": 882518038
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 024 [BD HEVC 10bit Dual Audio AC3][ABABE9B3].mkv",
|
||||
"size": 837335522
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 025 [BD HEVC 10bit Dual Audio AC3][17AEB0C1].mkv",
|
||||
"size": 933034706
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 026 [BD HEVC 10bit Dual Audio AC3][E6F0017C].mkv",
|
||||
"size": 891916996
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 027 [BD HEVC 10bit Dual Audio AC3][317791C7].mkv",
|
||||
"size": 896856044
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 028 [BD HEVC 10bit Dual Audio AC3][5CA4DF06].mkv",
|
||||
"size": 1010510386
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 029 [BD HEVC 10bit Dual Audio AC3][549CF25A].mkv",
|
||||
"size": 995648398
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 030 [BD HEVC 10bit Dual Audio AC3][CED479F9].mkv",
|
||||
"size": 991633973
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 031 [BD HEVC 10bit Dual Audio AC3][1EEFAB0E].mkv",
|
||||
"size": 950195341
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 032 [BD HEVC 10bit Dual Audio AC3][C6F3C39A].mkv",
|
||||
"size": 859218784
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 033 [BD HEVC 10bit Dual Audio AC3][A8424897].mkv",
|
||||
"size": 933230823
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 034 [BD HEVC 10bit Dual Audio AC3][96C94DB8].mkv",
|
||||
"size": 818179634
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 035 [BD HEVC 10bit Dual Audio AC3][A07831AE].mkv",
|
||||
"size": 882457138
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 036 [BD HEVC 10bit Dual Audio AC3][D984C169].mkv",
|
||||
"size": 895683290
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 037 [BD HEVC 10bit Dual Audio AC3][32C05A7E].mkv",
|
||||
"size": 970033716
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 038 [BD HEVC 10bit Dual Audio AC3][824B3EEC].mkv",
|
||||
"size": 956573899
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 039 [BD HEVC 10bit Dual Audio AC3][A45233DF].mkv",
|
||||
"size": 1238240707
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 040 [BD HEVC 10bit Dual Audio AC3][5334A7F1].mkv",
|
||||
"size": 894491562
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 041 [BD HEVC 10bit Dual Audio AC3][A4F17363].mkv",
|
||||
"size": 1014959456
|
||||
}
|
||||
],
|
||||
"UploadTime": "2023-03-03 03:03:17"
|
||||
},
|
||||
{
|
||||
"ID": 1031203,
|
||||
"EditionData": {
|
||||
"EditionTitle": "Season 03: The Rescue (042-063)"
|
||||
},
|
||||
"RawDownMultiplier": 0,
|
||||
"RawUpMultiplier": 1,
|
||||
"Link": "https://animebytes.tv/torrent/1031203/download/somepass",
|
||||
"Property": "Blu-ray | MKV | h265 10-bit | 1080p | AC3 2.0 | Dual Audio | Softsubs (GHOST) | Freeleech",
|
||||
"Snatched": 6,
|
||||
"Seeders": 12,
|
||||
"Leechers": 2,
|
||||
"Status": 0,
|
||||
"Size": 24498538059,
|
||||
"FileCount": 22,
|
||||
"FileList": [
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 042 [BD HEVC 10bit Dual Audio AC3][55763BF6].mkv",
|
||||
"size": 899825828
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 043 [BD HEVC 10bit Dual Audio AC3][70B71ECC].mkv",
|
||||
"size": 902256094
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 044 [BD HEVC 10bit Dual Audio AC3][35F5526B].mkv",
|
||||
"size": 885323344
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 045 [BD HEVC 10bit Dual Audio AC3][9C1DAE4E].mkv",
|
||||
"size": 1082465042
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 046 [BD HEVC 10bit Dual Audio AC3][869EF5B6].mkv",
|
||||
"size": 957433930
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 047 [BD HEVC 10bit Dual Audio AC3][890DA7CE].mkv",
|
||||
"size": 997124540
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 048 [BD HEVC 10bit Dual Audio AC3][39064E08].mkv",
|
||||
"size": 1026751383
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 049 [BD HEVC 10bit Dual Audio AC3][C536D3DB].mkv",
|
||||
"size": 994918391
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 050 [BD HEVC 10bit Dual Audio AC3][A7A0CB24].mkv",
|
||||
"size": 1141146920
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 051 [BD HEVC 10bit Dual Audio AC3][25C06D9D].mkv",
|
||||
"size": 951751791
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 052 [BD HEVC 10bit Dual Audio AC3][FB506194].mkv",
|
||||
"size": 1131756065
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 053 [BD HEVC 10bit Dual Audio AC3][4A76C66D].mkv",
|
||||
"size": 1076952986
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 054 [BD HEVC 10bit Dual Audio AC3][51D8E5F8].mkv",
|
||||
"size": 1369454462
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 055 [BD HEVC 10bit Dual Audio AC3][DCF20007].mkv",
|
||||
"size": 1428073116
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 056 [BD HEVC 10bit Dual Audio AC3][34A28687].mkv",
|
||||
"size": 1304804717
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 057 [BD HEVC 10bit Dual Audio AC3][D1D5FE29].mkv",
|
||||
"size": 1056220730
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 058 [BD HEVC 10bit Dual Audio AC3][C6EAC278].mkv",
|
||||
"size": 1551455953
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 059 [BD HEVC 10bit Dual Audio AC3][E7B25869].mkv",
|
||||
"size": 1026910923
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 060 [BD HEVC 10bit Dual Audio AC3][C9D257D4].mkv",
|
||||
"size": 1059631177
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 061 [BD HEVC 10bit Dual Audio AC3][0521C8D3].mkv",
|
||||
"size": 1457893287
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 062 [BD HEVC 10bit Dual Audio AC3][65CBA616].mkv",
|
||||
"size": 1262863946
|
||||
},
|
||||
{
|
||||
"filename": "[GHOST][1080p] Bleach - 063 [BD HEVC 10bit Dual Audio AC3][CF63E244].mkv",
|
||||
"size": 933523434
|
||||
}
|
||||
],
|
||||
"UploadTime": "2023-04-03 03:14:35"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"ID": 81926,
|
||||
"CategoryName": "Anime",
|
||||
"FullName": "Dr. STONE: NEW WORLD - TV Series [2023]",
|
||||
"GroupName": "TV Series",
|
||||
"SeriesID": "79217",
|
||||
"SeriesName": "Dr. STONE: NEW WORLD",
|
||||
"Artists": null,
|
||||
"Year": "2023",
|
||||
"Image": "https://mei.animebytes.tv/Tu0p0k56514.jpg",
|
||||
"Synonymns": {
|
||||
"0": "ドクターストーン NEW WORLD",
|
||||
"2": "Dr. STONE S3, Dr. STONE Season 3, Dr.STONE 3rd Season"
|
||||
},
|
||||
"SynonymnsV2": {
|
||||
"Japanese": "ドクターストーン NEW WORLD",
|
||||
"Romaji": "",
|
||||
"Alternative": "Dr. STONE S3, Dr. STONE Season 3, Dr.STONE 3rd Season"
|
||||
},
|
||||
"Snatched": 1870,
|
||||
"Comments": 0,
|
||||
"Links": {
|
||||
"AniDB": "https://anidb.net/anime/17053",
|
||||
"ANN": "https://www.animenewsnetwork.com/encyclopedia/anime.php?id=25068",
|
||||
"Wikipedia": "https://en.wikipedia.org/wiki/Dr._Stone",
|
||||
"MAL": "https://myanimelist.net/anime/48549"
|
||||
},
|
||||
"Votes": 0,
|
||||
"AvgVote": 0,
|
||||
"Associations": null,
|
||||
"Description": "Third season of [i]Dr. STONE[/i].\r\n\r\nWith the Stone Wars over, the former members of Tsukasa's Empire of Might join forces with the Kingdom of Science to build a ship capable of sailing across the open ocean to seek answers to the mystery of global petrification. However, before they can begin their voyage Senku and his friends need to find some key resources and push some new scientific advancements to build the type of vessel they need.\r\n\r\n* Based on an adventure sci-fi shounen manga series written by Inagaki Riichirou and illustrated by Boichi.\r\n\r\n[i]Note: The first episode received an early screening at a special event on March 12, 2023 at Iino Hall in Tokyo. The regular TV broadcast started on April 6, 2023.[/i]",
|
||||
"DescriptionHTML": "Third season of <em>Dr. STONE</em>.<br />\r\n<br />\r\nWith the Stone Wars over, the former members of Tsukasa's Empire of Might join forces with the Kingdom of Science to build a ship capable of sailing across the open ocean to seek answers to the mystery of global petrification. However, before they can begin their voyage Senku and his friends need to find some key resources and push some new scientific advancements to build the type of vessel they need.<br />\r\n<br />\r\n* Based on an adventure sci-fi shounen manga series written by Inagaki Riichirou and illustrated by Boichi.<br />\r\n<br />\r\n<em>Note: The first episode received an early screening at a special event on March 12, 2023 at Iino Hall in Tokyo. The regular TV broadcast started on April 6, 2023.</em>",
|
||||
"EpCount": 0,
|
||||
"StudioList": "TMS Entertainment///11",
|
||||
"PastWeek": 6,
|
||||
"Incomplete": false,
|
||||
"Ongoing": false,
|
||||
"Tags": [
|
||||
"adventure",
|
||||
"scifi",
|
||||
"shounen",
|
||||
"post.apocalyptic"
|
||||
],
|
||||
"Torrents": [
|
||||
{
|
||||
"ID": 1041495,
|
||||
"EditionData": {
|
||||
"EditionTitle": "Episode 3"
|
||||
},
|
||||
"RawDownMultiplier": 0,
|
||||
"RawUpMultiplier": 1,
|
||||
"Link": "https://animebytes.tv/torrent/1041495/download/somepass",
|
||||
"Property": "Web | MKV | h264 | 720p | AAC 2.0 | Softsubs (SubsPlease) | Episode 3 | Freeleech",
|
||||
"Snatched": 165,
|
||||
"Seeders": 137,
|
||||
"Leechers": 3,
|
||||
"Status": 0,
|
||||
"Size": 748209543,
|
||||
"FileCount": 1,
|
||||
"FileList": [
|
||||
{
|
||||
"filename": "[SubsPlease] Dr. Stone S3 - 03 (720p) [DAC92E18].mkv",
|
||||
"size": 748209543
|
||||
}
|
||||
],
|
||||
"UploadTime": "2023-04-20 14:32:29"
|
||||
},
|
||||
{
|
||||
"ID": 1037731,
|
||||
"EditionData": {
|
||||
"EditionTitle": "Episode 2"
|
||||
},
|
||||
"RawDownMultiplier": 0,
|
||||
"RawUpMultiplier": 1,
|
||||
"Link": "https://animebytes.tv/torrent/1037731/download/somepass",
|
||||
"Property": "Web | MKV | h264 | 720p | AAC 2.0 | Softsubs (SubsPlease) | Episode 2 | Freeleech",
|
||||
"Snatched": 174,
|
||||
"Seeders": 122,
|
||||
"Leechers": 1,
|
||||
"Status": 0,
|
||||
"Size": 748808730,
|
||||
"FileCount": 1,
|
||||
"FileList": [
|
||||
{
|
||||
"filename": "[SubsPlease] Dr. Stone S3 - 02 (720p) [AE2DA9AB].mkv",
|
||||
"size": 748808730
|
||||
}
|
||||
],
|
||||
"UploadTime": "2023-04-13 14:34:16"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"ID": 69267,
|
||||
"CategoryName": "Anime",
|
||||
"FullName": "Dr. STONE: STONE WARS - TV Series [2021]",
|
||||
"GroupName": "TV Series",
|
||||
"SeriesID": "67161",
|
||||
"SeriesName": "Dr. STONE: STONE WARS",
|
||||
"Artists": null,
|
||||
"Year": "2021",
|
||||
"Image": "https://mei.animebytes.tv/6pqXEK82OfD.jpg",
|
||||
"Synonymns": {
|
||||
"0": "ドクターストーン STONE WARS",
|
||||
"2": "Dr. STONE S2, Dr. STONE Season 2, Dr.STONE 2nd Season"
|
||||
},
|
||||
"SynonymnsV2": {
|
||||
"Japanese": "ドクターストーン STONE WARS",
|
||||
"Romaji": "",
|
||||
"Alternative": "Dr. STONE S2, Dr. STONE Season 2, Dr.STONE 2nd Season"
|
||||
},
|
||||
"Snatched": 1181,
|
||||
"Comments": 4,
|
||||
"Links": {
|
||||
"AniDB": "https://anidb.net/anime/15305",
|
||||
"ANN": "https://www.animenewsnetwork.com/encyclopedia/anime.php?id=22942",
|
||||
"Wikipedia": "https://en.wikipedia.org/wiki/Dr._Stone",
|
||||
"MAL": "https://myanimelist.net/anime/40852/Dr_Stone__Stone_Wars"
|
||||
},
|
||||
"Votes": 23,
|
||||
"AvgVote": 7.8,
|
||||
"Associations": null,
|
||||
"Description": "* Based on an adventure sci-fi shounen manga series written by Inagaki Riichirou and illustrated by Boichi.\r\n\r\nSenkuu, Chrome and the other villagers are in a battle of wits and brawn against the Tsukasa Empire after the revelation that Senkuu's father left behind a lasting message.\r\n\r\nWith a plan to build a phone that could be used to take down the bad guys from within, Senkuu will need to finally enlist the help of his friends who are a part of the Tsukasa Empire.",
|
||||
"DescriptionHTML": "* Based on an adventure sci-fi shounen manga series written by Inagaki Riichirou and illustrated by Boichi.<br />\r\n<br />\r\nSenkuu, Chrome and the other villagers are in a battle of wits and brawn against the Tsukasa Empire after the revelation that Senkuu's father left behind a lasting message.<br />\r\n<br />\r\nWith a plan to build a phone that could be used to take down the bad guys from within, Senkuu will need to finally enlist the help of his friends who are a part of the Tsukasa Empire.",
|
||||
"EpCount": 11,
|
||||
"StudioList": "TMS Entertainment///11",
|
||||
"PastWeek": 0,
|
||||
"Incomplete": false,
|
||||
"Ongoing": false,
|
||||
"Tags": [
|
||||
"adventure",
|
||||
"scifi",
|
||||
"shounen",
|
||||
"post.apocalyptic"
|
||||
],
|
||||
"Torrents": [
|
||||
{
|
||||
"ID": 944509,
|
||||
"EditionData": {
|
||||
"EditionTitle": ""
|
||||
},
|
||||
"RawDownMultiplier": 0,
|
||||
"RawUpMultiplier": 1,
|
||||
"Link": "https://animebytes.tv/torrent/944509/download/somepass",
|
||||
"Property": "Web | MKV | h264 | 1080p | AAC 2.0 | Dual Audio | Softsubs (-ZR-) | Freeleech",
|
||||
"Snatched": 188,
|
||||
"Seeders": 31,
|
||||
"Leechers": 1,
|
||||
"Status": 0,
|
||||
"Size": 16611719364,
|
||||
"FileCount": 11,
|
||||
"FileList": [
|
||||
{
|
||||
"filename": "Dr. Stone S02E01v2 2021 1080p WEB-DL AVC AAC 2.0 Dual Audio -ZR-.mkv",
|
||||
"size": 1512195256
|
||||
},
|
||||
{
|
||||
"filename": "Dr. Stone S02E02 2021 1080p WEB-DL AVC AAC 2.0 Dual Audio -ZR-.mkv",
|
||||
"size": 1507917714
|
||||
},
|
||||
{
|
||||
"filename": "Dr. Stone S02E03 2021 1080p WEB-DL AVC AAC 2.0 Dual Audio -ZR-.mkv",
|
||||
"size": 1510054199
|
||||
},
|
||||
{
|
||||
"filename": "Dr. Stone S02E04 2021 1080p WEB-DL AVC AAC 2.0 Dual Audio -ZR-.mkv",
|
||||
"size": 1507100461
|
||||
},
|
||||
{
|
||||
"filename": "Dr. Stone S02E05 2021 1080p WEB-DL AVC AAC 2.0 Dual Audio -ZR-.mkv",
|
||||
"size": 1507258273
|
||||
},
|
||||
{
|
||||
"filename": "Dr. Stone S02E06 2021 1080p WEB-DL AVC AAC 2.0 Dual Audio -ZR-.mkv",
|
||||
"size": 1511039711
|
||||
},
|
||||
{
|
||||
"filename": "Dr. Stone S02E07 2021 1080p WEB-DL AVC AAC 2.0 Dual Audio -ZR-.mkv",
|
||||
"size": 1507219047
|
||||
},
|
||||
{
|
||||
"filename": "Dr. Stone S02E08 2021 1080p WEB-DL AVC AAC 2.0 Dual Audio -ZR-.mkv",
|
||||
"size": 1510996213
|
||||
},
|
||||
{
|
||||
"filename": "Dr. Stone S02E09 2021 1080p WEB-DL AVC AAC 2.0 Dual Audio -ZR-.mkv",
|
||||
"size": 1512785600
|
||||
},
|
||||
{
|
||||
"filename": "Dr. Stone S02E10 2021 1080p WEB-DL AVC AAC 2.0 Dual Audio -ZR-.mkv",
|
||||
"size": 1511889715
|
||||
},
|
||||
{
|
||||
"filename": "Dr. Stone S02E11 2021 1080p WEB-DL AVC AAC 2.0 Dual Audio -ZR-.mkv",
|
||||
"size": 1513263175
|
||||
}
|
||||
],
|
||||
"UploadTime": "2021-06-03 20:30:00"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"ID": 60598,
|
||||
"CategoryName": "Anime",
|
||||
"FullName": "Dr. STONE - TV Series [2019]",
|
||||
"GroupName": "TV Series",
|
||||
"SeriesID": "56617",
|
||||
"SeriesName": "Dr. STONE",
|
||||
"Artists": null,
|
||||
"Year": "2019",
|
||||
"Image": "https://mei.animebytes.tv/SaFez5XG8T3.jpg",
|
||||
"Synonymns": [
|
||||
"Dr.STONE [ドクターストーン]"
|
||||
],
|
||||
"SynonymnsV2": {
|
||||
"Japanese": "Dr.STONE [ドクターストーン]",
|
||||
"Romaji": "",
|
||||
"Alternative": ""
|
||||
},
|
||||
"Snatched": 2174,
|
||||
"Comments": 8,
|
||||
"Links": {
|
||||
"AniDB": "https://anidb.net/anime/14491",
|
||||
"ANN": "https://www.animenewsnetwork.com/encyclopedia/anime.php?id=21703",
|
||||
"Wikipedia": "https://en.wikipedia.org/wiki/Dr._Stone",
|
||||
"MAL": "https://myanimelist.net/anime/38691"
|
||||
},
|
||||
"Votes": 68,
|
||||
"AvgVote": 7.9,
|
||||
"Associations": null,
|
||||
"Description": "Several thousand years after a mysterious phenomenon that turns all of humanity to stone, the extraordinarily intelligent, science-driven boy, Senku Ishigami, awakens. Facing a world of stone and the total collapse of civilization, Senku makes up his mind to use science to rebuild the world. Starting with his super strong childhood friend Taiju Oki, who awakened at the same time, they will begin to rebuild civilization from nothing... Depicting two million years of scientific history from the Stone Age to present day, the unprecedented crafting adventure story is about to begin!\r\n\r\n* Based on an adventure sci-fi shounen manga series written by Inagaki Riichirou and illustrated by Boichi.",
|
||||
"DescriptionHTML": "Several thousand years after a mysterious phenomenon that turns all of humanity to stone, the extraordinarily intelligent, science-driven boy, Senku Ishigami, awakens. Facing a world of stone and the total collapse of civilization, Senku makes up his mind to use science to rebuild the world. Starting with his super strong childhood friend Taiju Oki, who awakened at the same time, they will begin to rebuild civilization from nothing... Depicting two million years of scientific history from the Stone Age to present day, the unprecedented crafting adventure story is about to begin!<br />\r\n<br />\r\n* Based on an adventure sci-fi shounen manga series written by Inagaki Riichirou and illustrated by Boichi.",
|
||||
"EpCount": 24,
|
||||
"StudioList": "TMS Entertainment///11|8PAN///6344",
|
||||
"PastWeek": 0,
|
||||
"Incomplete": false,
|
||||
"Ongoing": false,
|
||||
"Tags": [
|
||||
"adventure",
|
||||
"comedy",
|
||||
"scifi",
|
||||
"shounen"
|
||||
],
|
||||
"Torrents": [
|
||||
{
|
||||
"ID": 430074,
|
||||
"EditionData": {
|
||||
"EditionTitle": ""
|
||||
},
|
||||
"RawDownMultiplier": 1,
|
||||
"RawUpMultiplier": 1,
|
||||
"Link": "https://animebytes.tv/torrent/430074/download/somepass",
|
||||
"Property": "Web | MKV | h264 | 720p | AAC 2.0 | Softsubs (HorribleSubs)",
|
||||
"Snatched": 108,
|
||||
"Seeders": 33,
|
||||
"Leechers": 1,
|
||||
"Status": 0,
|
||||
"Size": 16366224176,
|
||||
"FileCount": 24,
|
||||
"FileList": [
|
||||
{
|
||||
"filename": "[HorribleSubs] Dr. Stone - 01 [720p].mkv",
|
||||
"size": 477027555
|
||||
},
|
||||
{
|
||||
"filename": "[HorribleSubs] Dr. Stone - 02 [720p].mkv",
|
||||
"size": 489436551
|
||||
},
|
||||
{
|
||||
"filename": "[HorribleSubs] Dr. Stone - 03 [720p].mkv",
|
||||
"size": 503786828
|
||||
},
|
||||
{
|
||||
"filename": "[HorribleSubs] Dr. Stone - 04 [720p].mkv",
|
||||
"size": 442977598
|
||||
},
|
||||
{
|
||||
"filename": "[HorribleSubs] Dr. Stone - 05 [720p].mkv",
|
||||
"size": 523531555
|
||||
},
|
||||
{
|
||||
"filename": "[HorribleSubs] Dr. Stone - 06 [720p].mkv",
|
||||
"size": 506742468
|
||||
},
|
||||
{
|
||||
"filename": "[HorribleSubs] Dr. Stone - 07 [720p].mkv",
|
||||
"size": 746577276
|
||||
},
|
||||
{
|
||||
"filename": "[HorribleSubs] Dr. Stone - 08 [720p].mkv",
|
||||
"size": 745942485
|
||||
},
|
||||
{
|
||||
"filename": "[HorribleSubs] Dr. Stone - 09 [720p].mkv",
|
||||
"size": 746035250
|
||||
},
|
||||
{
|
||||
"filename": "[HorribleSubs] Dr. Stone - 10 [720p].mkv",
|
||||
"size": 746001386
|
||||
},
|
||||
{
|
||||
"filename": "[HorribleSubs] Dr. Stone - 11 [720p].mkv",
|
||||
"size": 746155088
|
||||
},
|
||||
{
|
||||
"filename": "[HorribleSubs] Dr. Stone - 12 [720p].mkv",
|
||||
"size": 746560710
|
||||
},
|
||||
{
|
||||
"filename": "[HorribleSubs] Dr. Stone - 13 [720p].mkv",
|
||||
"size": 745880614
|
||||
},
|
||||
{
|
||||
"filename": "[HorribleSubs] Dr. Stone - 14 [720p].mkv",
|
||||
"size": 744563919
|
||||
},
|
||||
{
|
||||
"filename": "[HorribleSubs] Dr. Stone - 15 [720p].mkv",
|
||||
"size": 745303312
|
||||
},
|
||||
{
|
||||
"filename": "[HorribleSubs] Dr. Stone - 16 [720p].mkv",
|
||||
"size": 746850910
|
||||
},
|
||||
{
|
||||
"filename": "[HorribleSubs] Dr. Stone - 17 [720p].mkv",
|
||||
"size": 744188496
|
||||
},
|
||||
{
|
||||
"filename": "[HorribleSubs] Dr. Stone - 18 [720p].mkv",
|
||||
"size": 746212236
|
||||
},
|
||||
{
|
||||
"filename": "[HorribleSubs] Dr. Stone - 19 [720p].mkv",
|
||||
"size": 744840131
|
||||
},
|
||||
{
|
||||
"filename": "[HorribleSubs] Dr. Stone - 20 [720p].mkv",
|
||||
"size": 746380081
|
||||
},
|
||||
{
|
||||
"filename": "[HorribleSubs] Dr. Stone - 21 [720p].mkv",
|
||||
"size": 744975636
|
||||
},
|
||||
{
|
||||
"filename": "[HorribleSubs] Dr. Stone - 22 [720p].mkv",
|
||||
"size": 746214757
|
||||
},
|
||||
{
|
||||
"filename": "[HorribleSubs] Dr. Stone - 23 [720p].mkv",
|
||||
"size": 744924693
|
||||
},
|
||||
{
|
||||
"filename": "[HorribleSubs] Dr. Stone - 24 [720p].mkv",
|
||||
"size": 745114641
|
||||
}
|
||||
],
|
||||
"UploadTime": "2019-12-13 17:02:48"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"ID": 41952,
|
||||
"CategoryName": "Anime",
|
||||
"FullName": "One Piece - TV Series [2019]",
|
||||
"GroupName": "TV Series",
|
||||
"SeriesID": "114",
|
||||
"SeriesName": "One Piece",
|
||||
"Artists": null,
|
||||
"Year": "2019",
|
||||
"Image": "https://mei.animebytes.tv/cQieN6oZ6Ft.jpg",
|
||||
"Synonymns": {
|
||||
"0": "ワンピース",
|
||||
"2": "One Piece: The Great Gold Pirate"
|
||||
},
|
||||
"SynonymnsV2": {
|
||||
"Japanese": "ワンピース",
|
||||
"Romaji": "",
|
||||
"Alternative": "One Piece: The Great Gold Pirate"
|
||||
},
|
||||
"Snatched": 100700,
|
||||
"Comments": 3,
|
||||
"Links": {
|
||||
"AniDB": "https://anidb.net/anime/69",
|
||||
"ANN": "https://www.animenewsnetwork.com/encyclopedia/anime.php?id=836",
|
||||
"Wikipedia": "https://en.wikipedia.org/wiki/One_Piece",
|
||||
"MAL": "https://myanimelist.net/anime/21/One_Piece"
|
||||
},
|
||||
"Votes": 89,
|
||||
"AvgVote": 8.8,
|
||||
"Associations": null,
|
||||
"Description": "The 20th season of One Piece. This represents episode 892 to current.",
|
||||
"DescriptionHTML": "The 20th season of One Piece. This represents episode 892 to current.",
|
||||
"EpCount": 0,
|
||||
"StudioList": null,
|
||||
"PastWeek": 10,
|
||||
"Incomplete": false,
|
||||
"Ongoing": false,
|
||||
"Tags": [
|
||||
"adventure",
|
||||
"fantasy",
|
||||
"martial.arts",
|
||||
"shounen",
|
||||
"super.power",
|
||||
"action"
|
||||
],
|
||||
"Torrents": [
|
||||
{
|
||||
"ID": 1043925,
|
||||
"EditionData": {
|
||||
"EditionTitle": "Episode 1059"
|
||||
},
|
||||
"RawDownMultiplier": 0,
|
||||
"RawUpMultiplier": 1,
|
||||
"Link": "https://animebytes.tv/torrent/1043925/download/somepass",
|
||||
"Property": "Web | MKV | h264 | 720p | AAC 2.0 | Softsubs (SubsPlease) | Episode 1059 | Freeleech",
|
||||
"Snatched": 125,
|
||||
"Seeders": 114,
|
||||
"Leechers": 1,
|
||||
"Status": 0,
|
||||
"Size": 743629489,
|
||||
"FileCount": 1,
|
||||
"FileList": [
|
||||
{
|
||||
"filename": "[SubsPlease] One Piece - 1059 (720p) [B347D9DE].mkv",
|
||||
"size": 743629489
|
||||
}
|
||||
],
|
||||
"UploadTime": "2023-04-23 02:06:08"
|
||||
},
|
||||
{
|
||||
"ID": 1039046,
|
||||
"EditionData": {
|
||||
"EditionTitle": "Episode 1058"
|
||||
},
|
||||
"RawDownMultiplier": 0,
|
||||
"RawUpMultiplier": 1,
|
||||
"Link": "https://animebytes.tv/torrent/1039046/download/somepass",
|
||||
"Property": "Web | MKV | h264 | 1080p | AAC 2.0 | Softsubs (SubsPlease) | Episode 1058 | Freeleech",
|
||||
"Snatched": 290,
|
||||
"Seeders": 232,
|
||||
"Leechers": 2,
|
||||
"Status": 0,
|
||||
"Size": 1453835224,
|
||||
"FileCount": 1,
|
||||
"FileList": [
|
||||
{
|
||||
"filename": "[SubsPlease] One Piece - 1058 (1080p) [E4094B4A].mkv",
|
||||
"size": 1453835224
|
||||
}
|
||||
],
|
||||
"UploadTime": "2023-04-16 02:07:42"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
1461
src/NzbDrone.Core.Test/Files/Indexers/Redacted/recentfeed.json
Normal file
1461
src/NzbDrone.Core.Test/Files/Indexers/Redacted/recentfeed.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,160 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Indexers.Definitions;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.IndexerTests.AnimeBytesTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class AnimeBytesFixture : CoreTest<AnimeBytes>
|
||||
{
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
Subject.Definition = new IndexerDefinition
|
||||
{
|
||||
Name = "AnimeBytes",
|
||||
Settings = new AnimeBytesSettings
|
||||
{
|
||||
BaseUrl = "https://animebytes.tv/",
|
||||
Username = "someuser",
|
||||
Passkey = "somepass"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task should_parse_recent_feed_from_animebytes()
|
||||
{
|
||||
var recentFeed = ReadAllText(@"Files/Indexers/AnimeBytes/recentfeed.json");
|
||||
|
||||
Mocker.GetMock<IIndexerHttpClient>()
|
||||
.Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get), Subject.Definition))
|
||||
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed)));
|
||||
|
||||
var releases = (await Subject.Fetch(new BasicSearchCriteria { Categories = new[] { 2000, 5000 } })).Releases;
|
||||
|
||||
releases.Should().HaveCount(33);
|
||||
releases.First().Should().BeOfType<TorrentInfo>();
|
||||
|
||||
var firstTorrentInfo = releases.ElementAt(2) as TorrentInfo;
|
||||
|
||||
firstTorrentInfo.Title.Should().Be("[SubsPlease] One Piece: The Great Gold Pirate - 1059 [Web][MKV][h264][720p][AAC 2.0][Softsubs (SubsPlease)][Episode 1059]");
|
||||
firstTorrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
||||
firstTorrentInfo.DownloadUrl.Should().Be("https://animebytes.tv/torrent/1043925/download/somepass");
|
||||
firstTorrentInfo.InfoUrl.Should().Be("https://animebytes.tv/torrent/1043925/group");
|
||||
firstTorrentInfo.Guid.Should().Be("https://animebytes.tv/torrent/1043925/group?nh=0F6BB43603CC07F4C804B9A29139F852");
|
||||
firstTorrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||
firstTorrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||
firstTorrentInfo.PublishDate.Should().Be(DateTime.Parse("2023-04-23 02:06:08", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal));
|
||||
firstTorrentInfo.Size.Should().Be(743629489);
|
||||
firstTorrentInfo.InfoHash.Should().Be(null);
|
||||
firstTorrentInfo.MagnetUrl.Should().Be(null);
|
||||
firstTorrentInfo.Peers.Should().Be(1 + 114);
|
||||
firstTorrentInfo.Seeders.Should().Be(114);
|
||||
firstTorrentInfo.Files.Should().Be(1);
|
||||
firstTorrentInfo.MinimumSeedTime.Should().Be(259200);
|
||||
|
||||
var secondTorrentInfo = releases.ElementAt(16) as TorrentInfo;
|
||||
|
||||
secondTorrentInfo.Title.Should().Be("[GHOST] BLEACH S03 [Blu-ray][MKV][h265 10-bit][1080p][AC3 2.0][Dual Audio][Softsubs (GHOST)]");
|
||||
secondTorrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
||||
secondTorrentInfo.DownloadUrl.Should().Be("https://animebytes.tv/torrent/1031203/download/somepass");
|
||||
secondTorrentInfo.InfoUrl.Should().Be("https://animebytes.tv/torrent/1031203/group");
|
||||
secondTorrentInfo.Guid.Should().Be("https://animebytes.tv/torrent/1031203/group?nh=F7C73EF631FE269D3A7F10BD12EC99A1");
|
||||
secondTorrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||
secondTorrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||
secondTorrentInfo.PublishDate.Should().Be(DateTime.Parse("2023-04-03 03:14:35", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal));
|
||||
secondTorrentInfo.Size.Should().Be(24498538059);
|
||||
secondTorrentInfo.InfoHash.Should().Be(null);
|
||||
secondTorrentInfo.MagnetUrl.Should().Be(null);
|
||||
secondTorrentInfo.Peers.Should().Be(2 + 12);
|
||||
secondTorrentInfo.Seeders.Should().Be(12);
|
||||
secondTorrentInfo.Files.Should().Be(22);
|
||||
secondTorrentInfo.MinimumSeedTime.Should().Be(655200);
|
||||
|
||||
var thirdTorrentInfo = releases.ElementAt(18) as TorrentInfo;
|
||||
|
||||
thirdTorrentInfo.Title.Should().Be("[Polarwindz] Cowboy Bebop: Tengoku no Tobira 2001 [Blu-ray][MKV][h265 10-bit][1080p][Opus 5.1][Softsubs (Polarwindz)]");
|
||||
thirdTorrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
||||
thirdTorrentInfo.DownloadUrl.Should().Be("https://animebytes.tv/torrent/959397/download/somepass");
|
||||
thirdTorrentInfo.InfoUrl.Should().Be("https://animebytes.tv/torrent/959397/group");
|
||||
thirdTorrentInfo.Guid.Should().Be("https://animebytes.tv/torrent/959397/group?nh=D63895DA87A25239C11F9823F46000E1");
|
||||
thirdTorrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||
thirdTorrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||
thirdTorrentInfo.PublishDate.Should().Be(DateTime.Parse("2023-04-02 05:00:43", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal));
|
||||
thirdTorrentInfo.Size.Should().Be(13090646841);
|
||||
thirdTorrentInfo.InfoHash.Should().Be(null);
|
||||
thirdTorrentInfo.MagnetUrl.Should().Be(null);
|
||||
thirdTorrentInfo.Peers.Should().Be(1 + 5);
|
||||
thirdTorrentInfo.Seeders.Should().Be(5);
|
||||
thirdTorrentInfo.Files.Should().Be(1);
|
||||
thirdTorrentInfo.MinimumSeedTime.Should().Be(475200);
|
||||
|
||||
var fourthTorrentInfo = releases.ElementAt(3) as TorrentInfo;
|
||||
|
||||
fourthTorrentInfo.Title.Should().Be("[SubsPlease] Dr. STONE: NEW WORLD S03E03 - 03 [Web][MKV][h264][720p][AAC 2.0][Softsubs (SubsPlease)][Episode 3]");
|
||||
fourthTorrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
||||
fourthTorrentInfo.DownloadUrl.Should().Be("https://animebytes.tv/torrent/1041495/download/somepass");
|
||||
fourthTorrentInfo.InfoUrl.Should().Be("https://animebytes.tv/torrent/1041495/group");
|
||||
fourthTorrentInfo.Guid.Should().Be("https://animebytes.tv/torrent/1041495/group?nh=8B78B0DD3BCC6068BFCD927E4AC674F6");
|
||||
fourthTorrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||
fourthTorrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||
fourthTorrentInfo.PublishDate.Should().Be(DateTime.Parse("2023-04-20 14:32:29", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal));
|
||||
fourthTorrentInfo.Size.Should().Be(748209543);
|
||||
fourthTorrentInfo.InfoHash.Should().Be(null);
|
||||
fourthTorrentInfo.MagnetUrl.Should().Be(null);
|
||||
fourthTorrentInfo.Peers.Should().Be(3 + 137);
|
||||
fourthTorrentInfo.Seeders.Should().Be(137);
|
||||
fourthTorrentInfo.Files.Should().Be(1);
|
||||
fourthTorrentInfo.MinimumSeedTime.Should().Be(259200);
|
||||
|
||||
var fifthTorrentInfo = releases.ElementAt(23) as TorrentInfo;
|
||||
|
||||
fifthTorrentInfo.Title.Should().Be("[-ZR-] Dr. STONE: STONE WARS S02 [Web][MKV][h264][1080p][AAC 2.0][Dual Audio][Softsubs (-ZR-)]");
|
||||
fifthTorrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
||||
fifthTorrentInfo.DownloadUrl.Should().Be("https://animebytes.tv/torrent/944509/download/somepass");
|
||||
fifthTorrentInfo.InfoUrl.Should().Be("https://animebytes.tv/torrent/944509/group");
|
||||
fifthTorrentInfo.Guid.Should().Be("https://animebytes.tv/torrent/944509/group?nh=FDCAA1EAB36D7C802F1E4B13DAE5EED7");
|
||||
fifthTorrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||
fifthTorrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||
fifthTorrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-06-03 20:30:00", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal));
|
||||
fifthTorrentInfo.Size.Should().Be(16611719364);
|
||||
fifthTorrentInfo.InfoHash.Should().Be(null);
|
||||
fifthTorrentInfo.MagnetUrl.Should().Be(null);
|
||||
fifthTorrentInfo.Peers.Should().Be(1 + 31);
|
||||
fifthTorrentInfo.Seeders.Should().Be(31);
|
||||
fifthTorrentInfo.Files.Should().Be(11);
|
||||
fifthTorrentInfo.MinimumSeedTime.Should().Be(529200);
|
||||
|
||||
var sixthTorrentInfo = releases.ElementAt(31) as TorrentInfo;
|
||||
|
||||
sixthTorrentInfo.Title.Should().Be("[HorribleSubs] Dr. STONE S01 [Web][MKV][h264][720p][AAC 2.0][Softsubs (HorribleSubs)]");
|
||||
sixthTorrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
||||
sixthTorrentInfo.DownloadUrl.Should().Be("https://animebytes.tv/torrent/430074/download/somepass");
|
||||
sixthTorrentInfo.InfoUrl.Should().Be("https://animebytes.tv/torrent/430074/group");
|
||||
sixthTorrentInfo.Guid.Should().Be("https://animebytes.tv/torrent/430074/group?nh=32279E138015D8718B2B4B49AEF64574");
|
||||
sixthTorrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||
sixthTorrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||
sixthTorrentInfo.PublishDate.Should().Be(DateTime.Parse("2019-12-13 17:02:48", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal));
|
||||
sixthTorrentInfo.Size.Should().Be(16366224176);
|
||||
sixthTorrentInfo.InfoHash.Should().Be(null);
|
||||
sixthTorrentInfo.MagnetUrl.Should().Be(null);
|
||||
sixthTorrentInfo.Peers.Should().Be(1 + 33);
|
||||
sixthTorrentInfo.Seeders.Should().Be(33);
|
||||
sixthTorrentInfo.Files.Should().Be(24);
|
||||
sixthTorrentInfo.MinimumSeedTime.Should().Be(529200);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,10 +22,10 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
Subject.Definition = new IndexerDefinition()
|
||||
Subject.Definition = new IndexerDefinition
|
||||
{
|
||||
Name = "AvistaZ",
|
||||
Settings = new AvistazSettings() { Username = "someuser", Password = "somepass", Pid = "somepid" }
|
||||
Settings = new AvistazSettings { Username = "someuser", Password = "somepass", Pid = "somepid" }
|
||||
};
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
|
||||
.Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get), Subject.Definition))
|
||||
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed)));
|
||||
|
||||
var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new int[] { 2000 } })).Releases;
|
||||
var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new[] { 2000 } })).Releases;
|
||||
|
||||
releases.Should().HaveCount(100);
|
||||
releases.First().Should().BeOfType<TorrentInfo>();
|
||||
@@ -51,7 +51,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
|
||||
torrentInfo.InfoUrl.Should().Be("https://avistaz.to/torrent/187240-japan-sinks-people-of-hope-2021-s01e05-720p-nf-web-dl-ddp20-x264-seikel");
|
||||
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-11-14 22:26:21"));
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-11-14 21:26:21"));
|
||||
torrentInfo.Size.Should().Be(935127615);
|
||||
torrentInfo.InfoHash.Should().Be("a879261d4e6e792402f92401141a21de70d51bf2");
|
||||
torrentInfo.MagnetUrl.Should().Be(null);
|
||||
|
||||
@@ -22,10 +22,10 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
Subject.Definition = new IndexerDefinition()
|
||||
Subject.Definition = new IndexerDefinition
|
||||
{
|
||||
Name = "ExoticaZ",
|
||||
Settings = new AvistazSettings() { Username = "someuser", Password = "somepass", Pid = "somepid" }
|
||||
Settings = new AvistazSettings { Username = "someuser", Password = "somepass", Pid = "somepid" }
|
||||
};
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
|
||||
.Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get), Subject.Definition))
|
||||
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed)));
|
||||
|
||||
var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new int[] { 2000 } })).Releases;
|
||||
var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new[] { 2000 } })).Releases;
|
||||
|
||||
releases.Should().HaveCount(100);
|
||||
releases.First().Should().BeOfType<TorrentInfo>();
|
||||
@@ -51,7 +51,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
|
||||
torrentInfo.InfoUrl.Should().Be("https://exoticaz.to/torrent/64040-ssis-419-my-first-experience-is-yua-mikami-from-the-day-i-lost-my-virginity-i-was-devoted-to-sex");
|
||||
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-06-11 16:04:50"));
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-06-11 15:04:50"));
|
||||
torrentInfo.Size.Should().Be(7085405541);
|
||||
torrentInfo.InfoHash.Should().Be("asdjfiasdf54asd7f4a2sdf544asdf");
|
||||
torrentInfo.MagnetUrl.Should().Be(null);
|
||||
|
||||
@@ -22,10 +22,10 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
Subject.Definition = new IndexerDefinition()
|
||||
Subject.Definition = new IndexerDefinition
|
||||
{
|
||||
Name = "PrivateHD",
|
||||
Settings = new AvistazSettings() { Username = "someuser", Password = "somepass", Pid = "somepid" }
|
||||
Settings = new AvistazSettings { Username = "someuser", Password = "somepass", Pid = "somepid" }
|
||||
};
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
|
||||
.Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get), Subject.Definition))
|
||||
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed)));
|
||||
|
||||
var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new int[] { 2000 } })).Releases;
|
||||
var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new[] { 2000 } })).Releases;
|
||||
|
||||
releases.Should().HaveCount(100);
|
||||
releases.First().Should().BeOfType<TorrentInfo>();
|
||||
@@ -51,7 +51,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
|
||||
torrentInfo.InfoUrl.Should().Be("https://privatehd.to/torrent/78506-godzilla-2014-2160p-uhd-bluray-remux-hdr-hevc-atmos-triton");
|
||||
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-03-21 05:24:49"));
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-03-21 04:24:49"));
|
||||
torrentInfo.Size.Should().Be(69914591044);
|
||||
torrentInfo.InfoHash.Should().Be("a879261d4e6e792402f92401141a21de70d51bf2");
|
||||
torrentInfo.MagnetUrl.Should().Be(null);
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using DryIoc;
|
||||
using FluentAssertions;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Indexers.BroadcastheNet;
|
||||
using NzbDrone.Core.Indexers.Definitions.HDBits;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.IndexerTests.BroadcastheNetTests
|
||||
{
|
||||
public class BroadcastheNetRequestGeneratorFixture : CoreTest<BroadcastheNetRequestGenerator>
|
||||
{
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
Subject.Settings = new BroadcastheNetSettings
|
||||
{
|
||||
BaseUrl = "https://api.broadcasthe.net/",
|
||||
ApiKey = "abc"
|
||||
};
|
||||
|
||||
Subject.Capabilities = new IndexerCapabilities
|
||||
{
|
||||
LimitsDefault = 100,
|
||||
LimitsMax = 1000,
|
||||
TvSearchParams = new List<TvSearchParam>
|
||||
{
|
||||
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep, TvSearchParam.TvdbId, TvSearchParam.RId
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_have_empty_parameters_if_rss_search()
|
||||
{
|
||||
var tvSearchCriteria = new TvSearchCriteria
|
||||
{
|
||||
Categories = new[] { NewznabStandardCategory.TV.Id, NewznabStandardCategory.TVHD.Id }
|
||||
};
|
||||
|
||||
var results = Subject.GetSearchRequests(tvSearchCriteria);
|
||||
|
||||
results.GetAllTiers().Should().HaveCount(1);
|
||||
|
||||
var page = results.GetAllTiers().First().First();
|
||||
var query = ParseTorrentQueryFromRequest(page.HttpRequest);
|
||||
|
||||
query.Tvdb.Should().BeNull();
|
||||
query.Tvrage.Should().BeNull();
|
||||
query.Search.Should().BeNullOrWhiteSpace();
|
||||
query.Category.Should().BeNullOrWhiteSpace();
|
||||
query.Name.Should().BeNullOrWhiteSpace();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_search_by_tvdbid_season_if_supported()
|
||||
{
|
||||
var tvSearchCriteria = new TvSearchCriteria
|
||||
{
|
||||
Categories = new[] { NewznabStandardCategory.TV.Id, NewznabStandardCategory.TVHD.Id },
|
||||
TvdbId = 371980,
|
||||
Season = 1
|
||||
};
|
||||
|
||||
var results = Subject.GetSearchRequests(tvSearchCriteria);
|
||||
|
||||
results.Tiers.Should().Be(1);
|
||||
results.GetAllTiers().Should().HaveCount(2);
|
||||
|
||||
var firstPage = results.GetAllTiers().First().First();
|
||||
var firstQuery = ParseTorrentQueryFromRequest(firstPage.HttpRequest);
|
||||
|
||||
firstQuery.Tvdb.Should().Be("371980");
|
||||
firstQuery.Tvrage.Should().BeNull();
|
||||
firstQuery.Search.Should().BeNull();
|
||||
firstQuery.Category.Should().Be("Season");
|
||||
firstQuery.Name.Should().Be("Season 1%");
|
||||
|
||||
var secondPage = results.GetAllTiers().Skip(1).First().First();
|
||||
var secondQuery = ParseTorrentQueryFromRequest(secondPage.HttpRequest);
|
||||
|
||||
secondQuery.Tvdb.Should().Be("371980");
|
||||
secondQuery.Tvrage.Should().BeNull();
|
||||
secondQuery.Search.Should().BeNull();
|
||||
secondQuery.Category.Should().Be("Episode");
|
||||
secondQuery.Name.Should().Be("S01E%");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_search_by_tvdbid_season_episode_if_supported()
|
||||
{
|
||||
var tvSearchCriteria = new TvSearchCriteria
|
||||
{
|
||||
Categories = new[] { NewznabStandardCategory.TV.Id, NewznabStandardCategory.TVHD.Id },
|
||||
TvdbId = 371980,
|
||||
Season = 1,
|
||||
Episode = "3"
|
||||
};
|
||||
|
||||
var results = Subject.GetSearchRequests(tvSearchCriteria);
|
||||
|
||||
results.Tiers.Should().Be(1);
|
||||
results.GetAllTiers().Should().HaveCount(1);
|
||||
|
||||
var page = results.GetAllTiers().First().First();
|
||||
var query = ParseTorrentQueryFromRequest(page.HttpRequest);
|
||||
|
||||
query.Tvdb.Should().Be("371980");
|
||||
query.Tvrage.Should().BeNull();
|
||||
query.Search.Should().BeNull();
|
||||
query.Category.Should().Be("Episode");
|
||||
query.Name.Should().Be("S01E03");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_search_by_tvdbid_daily_episode_if_supported()
|
||||
{
|
||||
var tvSearchCriteria = new TvSearchCriteria
|
||||
{
|
||||
Categories = new[] { NewznabStandardCategory.TV.Id, NewznabStandardCategory.TVHD.Id },
|
||||
TvdbId = 289574,
|
||||
Season = 2023,
|
||||
Episode = "01/03"
|
||||
};
|
||||
|
||||
var results = Subject.GetSearchRequests(tvSearchCriteria);
|
||||
|
||||
results.Tiers.Should().Be(1);
|
||||
results.GetAllTiers().Should().HaveCount(1);
|
||||
|
||||
var page = results.GetAllTiers().First().First();
|
||||
var query = ParseTorrentQueryFromRequest(page.HttpRequest);
|
||||
|
||||
query.Tvdb.Should().Be("289574");
|
||||
query.Tvrage.Should().BeNull();
|
||||
query.Search.Should().BeNull();
|
||||
query.Category.Should().Be("Episode");
|
||||
query.Name.Should().Be("2023.01.03");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_prefer_search_by_tvdbid_if_rid_supported()
|
||||
{
|
||||
var tvSearchCriteria = new TvSearchCriteria
|
||||
{
|
||||
Categories = new[] { NewznabStandardCategory.TV.Id, NewznabStandardCategory.TVHD.Id },
|
||||
TvdbId = 371980,
|
||||
RId = 12345
|
||||
};
|
||||
|
||||
var results = Subject.GetSearchRequests(tvSearchCriteria);
|
||||
|
||||
results.Tiers.Should().Be(1);
|
||||
results.GetAllTiers().Should().HaveCount(1);
|
||||
|
||||
var page = results.GetAllTiers().First().First();
|
||||
var query = ParseTorrentQueryFromRequest(page.HttpRequest);
|
||||
|
||||
query.Tvdb.Should().Be("371980");
|
||||
query.Tvrage.Should().BeNull();
|
||||
query.Search.Should().BeNull();
|
||||
query.Category.Should().BeNull();
|
||||
query.Name.Should().BeNull();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_search_by_term_supported()
|
||||
{
|
||||
var tvSearchCriteria = new TvSearchCriteria
|
||||
{
|
||||
Categories = new[] { NewznabStandardCategory.TV.Id, NewznabStandardCategory.TVHD.Id },
|
||||
SearchTerm = "Malcolm in the Middle"
|
||||
};
|
||||
|
||||
var results = Subject.GetSearchRequests(tvSearchCriteria);
|
||||
|
||||
results.Tiers.Should().Be(1);
|
||||
results.GetAllTiers().Should().HaveCount(1);
|
||||
|
||||
var page = results.GetAllTiers().First().First();
|
||||
var query = ParseTorrentQueryFromRequest(page.HttpRequest);
|
||||
|
||||
query.Tvdb.Should().BeNull();
|
||||
query.Tvrage.Should().BeNull();
|
||||
query.Search.Should().Be("Malcolm%in%the%Middle");
|
||||
query.Category.Should().BeNull();
|
||||
query.Name.Should().BeNull();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_search_by_term_season_if_supported()
|
||||
{
|
||||
var tvSearchCriteria = new TvSearchCriteria
|
||||
{
|
||||
Categories = new[] { NewznabStandardCategory.TV.Id, NewznabStandardCategory.TVHD.Id },
|
||||
SearchTerm = "Malcolm in the Middle",
|
||||
Season = 2
|
||||
};
|
||||
|
||||
var results = Subject.GetSearchRequests(tvSearchCriteria);
|
||||
|
||||
results.Tiers.Should().Be(1);
|
||||
results.GetAllTiers().Should().HaveCount(2);
|
||||
|
||||
var firstPage = results.GetAllTiers().First().First();
|
||||
var firstQuery = ParseTorrentQueryFromRequest(firstPage.HttpRequest);
|
||||
|
||||
firstQuery.Tvdb.Should().BeNull();
|
||||
firstQuery.Tvrage.Should().BeNull();
|
||||
firstQuery.Search.Should().Be("Malcolm%in%the%Middle");
|
||||
firstQuery.Category.Should().Be("Season");
|
||||
firstQuery.Name.Should().Be("Season 2%");
|
||||
|
||||
var secondPage = results.GetAllTiers().Skip(1).First().First();
|
||||
var secondQuery = ParseTorrentQueryFromRequest(secondPage.HttpRequest);
|
||||
|
||||
secondQuery.Tvdb.Should().BeNull();
|
||||
secondQuery.Tvrage.Should().BeNull();
|
||||
secondQuery.Search.Should().Be("Malcolm%in%the%Middle");
|
||||
secondQuery.Category.Should().Be("Episode");
|
||||
secondQuery.Name.Should().Be("S02E%");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_search_by_term_season_episode_if_supported()
|
||||
{
|
||||
var tvSearchCriteria = new TvSearchCriteria
|
||||
{
|
||||
Categories = new[] { NewznabStandardCategory.TV.Id, NewznabStandardCategory.TVHD.Id },
|
||||
SearchTerm = "Malcolm in the Middle",
|
||||
Season = 2,
|
||||
Episode = "3"
|
||||
};
|
||||
|
||||
var results = Subject.GetSearchRequests(tvSearchCriteria);
|
||||
|
||||
results.Tiers.Should().Be(1);
|
||||
results.GetAllTiers().Should().HaveCount(1);
|
||||
|
||||
var page = results.GetAllTiers().First().First();
|
||||
var query = ParseTorrentQueryFromRequest(page.HttpRequest);
|
||||
|
||||
query.Tvdb.Should().BeNull();
|
||||
query.Tvrage.Should().BeNull();
|
||||
query.Search.Should().Be("Malcolm%in%the%Middle");
|
||||
query.Category.Should().Be("Episode");
|
||||
query.Name.Should().Be("S02E03");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_search_by_term_daily_episode_if_supported()
|
||||
{
|
||||
var tvSearchCriteria = new TvSearchCriteria
|
||||
{
|
||||
Categories = new[] { NewznabStandardCategory.TV.Id, NewznabStandardCategory.TVHD.Id },
|
||||
SearchTerm = "The Late Show with Stephen Colbert",
|
||||
Season = 2023,
|
||||
Episode = "01/03"
|
||||
};
|
||||
|
||||
var results = Subject.GetSearchRequests(tvSearchCriteria);
|
||||
|
||||
results.Tiers.Should().Be(1);
|
||||
results.GetAllTiers().Should().HaveCount(1);
|
||||
|
||||
var page = results.GetAllTiers().First().First();
|
||||
var query = ParseTorrentQueryFromRequest(page.HttpRequest);
|
||||
|
||||
query.Tvdb.Should().BeNull();
|
||||
query.Tvrage.Should().BeNull();
|
||||
query.Search.Should().Be("The%Late%Show%with%Stephen%Colbert");
|
||||
query.Category.Should().Be("Episode");
|
||||
query.Name.Should().Be("2023.01.03");
|
||||
}
|
||||
|
||||
private static BroadcastheNetTorrentQuery ParseTorrentQueryFromRequest(HttpRequest httpRequest)
|
||||
{
|
||||
var encoding = HttpHeader.GetEncodingFromContentType(httpRequest.Headers.ContentType);
|
||||
var body = encoding.GetString(httpRequest.ContentData);
|
||||
|
||||
var rpcBody = JsonConvert.DeserializeObject<Dictionary<string, object>>(body);
|
||||
|
||||
return JsonConvert.DeserializeObject<BroadcastheNetTorrentQuery>(((JArray)rpcBody["params"])[1].ToJson());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ using System.Collections.Generic;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Indexers.Cardigann;
|
||||
using NzbDrone.Core.Indexers.Definitions.Cardigann;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.IndexerTests.CardigannTests
|
||||
|
||||
@@ -55,7 +55,7 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
|
||||
torrentInfo.InfoUrl.Should().Be("https://filelist.io/details.php?id=665873");
|
||||
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2020-01-25 20:20:19"));
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2020-01-25 19:20:19"));
|
||||
torrentInfo.Size.Should().Be(8300512414);
|
||||
torrentInfo.InfoHash.Should().Be(null);
|
||||
torrentInfo.MagnetUrl.Should().Be(null);
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace NzbDrone.Core.Test.IndexerTests.OrpheusTests
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
Subject.Definition = new IndexerDefinition()
|
||||
Subject.Definition = new IndexerDefinition
|
||||
{
|
||||
Name = "Orpheus",
|
||||
Settings = new OrpheusSettings { Apikey = "somekey" }
|
||||
@@ -37,7 +37,7 @@ namespace NzbDrone.Core.Test.IndexerTests.OrpheusTests
|
||||
.Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get), Subject.Definition))
|
||||
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed)));
|
||||
|
||||
var releases = (await Subject.Fetch(new BasicSearchCriteria { Categories = new[] { 2000 } })).Releases;
|
||||
var releases = (await Subject.Fetch(new BasicSearchCriteria { Categories = new[] { 3000 } })).Releases;
|
||||
|
||||
releases.Should().HaveCount(65);
|
||||
releases.First().Should().BeOfType<GazelleInfo>();
|
||||
@@ -56,6 +56,7 @@ namespace NzbDrone.Core.Test.IndexerTests.OrpheusTests
|
||||
torrentInfo.MagnetUrl.Should().Be(null);
|
||||
torrentInfo.Peers.Should().Be(0);
|
||||
torrentInfo.Seeders.Should().Be(0);
|
||||
torrentInfo.Files.Should().Be(18);
|
||||
torrentInfo.ImdbId.Should().Be(0);
|
||||
torrentInfo.TmdbId.Should().Be(0);
|
||||
torrentInfo.TvdbId.Should().Be(0);
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Indexers.Definitions;
|
||||
using NzbDrone.Core.Indexers.Definitions.Gazelle;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.IndexerTests.RedactedTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class RedactedFixture : CoreTest<Redacted>
|
||||
{
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
Subject.Definition = new IndexerDefinition
|
||||
{
|
||||
Name = "Redacted",
|
||||
Settings = new RedactedSettings { Apikey = "somekey" }
|
||||
};
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task should_parse_recent_feed_from_Redacted()
|
||||
{
|
||||
var recentFeed = ReadAllText(@"Files/Indexers/Redacted/recentfeed.json");
|
||||
|
||||
Mocker.GetMock<IIndexerHttpClient>()
|
||||
.Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get), Subject.Definition))
|
||||
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed)));
|
||||
|
||||
var releases = (await Subject.Fetch(new BasicSearchCriteria { Categories = new[] { 3000 } })).Releases;
|
||||
|
||||
releases.Should().HaveCount(39);
|
||||
releases.First().Should().BeOfType<GazelleInfo>();
|
||||
|
||||
var torrentInfo = releases.First() as GazelleInfo;
|
||||
|
||||
torrentInfo.Title.Should().Be("Red Hot Chili Peppers - Californication [1999] [Album] [US / Reissue 2020] [FLAC 24bit Lossless] [Vinyl]");
|
||||
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
||||
torrentInfo.DownloadUrl.Should().Be("https://redacted.ch/ajax.php?action=download&id=3892313&usetoken=0");
|
||||
torrentInfo.InfoUrl.Should().Be("https://redacted.ch/torrents.php?id=16720&torrentid=3892313");
|
||||
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-12-17 08:02:35"));
|
||||
torrentInfo.Size.Should().Be(1247137236);
|
||||
torrentInfo.InfoHash.Should().Be(null);
|
||||
torrentInfo.MagnetUrl.Should().Be(null);
|
||||
torrentInfo.Peers.Should().Be(4);
|
||||
torrentInfo.Seeders.Should().Be(4);
|
||||
torrentInfo.Files.Should().Be(23);
|
||||
torrentInfo.ImdbId.Should().Be(0);
|
||||
torrentInfo.TmdbId.Should().Be(0);
|
||||
torrentInfo.TvdbId.Should().Be(0);
|
||||
torrentInfo.Languages.Should().HaveCount(0);
|
||||
torrentInfo.Subs.Should().HaveCount(0);
|
||||
torrentInfo.DownloadVolumeFactor.Should().Be(1);
|
||||
torrentInfo.UploadVolumeFactor.Should().Be(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ namespace NzbDrone.Core.Test.IndexerTests.SecretCinemaTests
|
||||
Subject.Definition = new IndexerDefinition()
|
||||
{
|
||||
Name = "SecretCinema",
|
||||
Settings = new GazelleSettings() { Username = "somekey", Password = "somekey" }
|
||||
Settings = new GazelleSettings { Username = "somekey", Password = "somekey" }
|
||||
};
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace NzbDrone.Core.Test.IndexerTests.SecretCinemaTests
|
||||
.Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get), Subject.Definition))
|
||||
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed)));
|
||||
|
||||
var releases = (await Subject.Fetch(new BasicSearchCriteria { Categories = new int[] { 2000 } })).Releases;
|
||||
var releases = (await Subject.Fetch(new BasicSearchCriteria { Categories = new[] { 2000 } })).Releases;
|
||||
|
||||
releases.Should().HaveCount(3);
|
||||
releases.First().Should().BeOfType<GazelleInfo>();
|
||||
@@ -50,7 +50,7 @@ namespace NzbDrone.Core.Test.IndexerTests.SecretCinemaTests
|
||||
torrentInfo.InfoUrl.Should().Be("https://secret-cinema.pw/torrents.php?id=2497&torrentid=45068");
|
||||
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-12-15 19:37:29"));
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-12-15 17:37:29"));
|
||||
torrentInfo.Size.Should().Be(57473058680);
|
||||
torrentInfo.InfoHash.Should().Be(null);
|
||||
torrentInfo.MagnetUrl.Should().Be(null);
|
||||
|
||||
@@ -210,11 +210,11 @@ namespace NzbDrone.Core.Applications
|
||||
|
||||
if (intersectingTags.Any())
|
||||
{
|
||||
_logger.Debug("Application {0} and indexer {1} [{2}] have {3} intersecting tags.", app.Name, indexer.Name, indexer.Id, intersectingTags.Length);
|
||||
_logger.Debug("Application {0} and indexer {1} [{2}] have {3} intersecting (matching) tags.", app.Name, indexer.Name, indexer.Id, intersectingTags.Length);
|
||||
return true;
|
||||
}
|
||||
|
||||
_logger.Info("Application {0} does not have any intersecting tags with {1} [{2}]. Indexer will not be handled.", app.Name, indexer.Name, indexer.Id);
|
||||
_logger.Debug("Application {0} does not have any intersecting (matching) tags with {1} [{2}]. Indexer will neither be synced to nor removed from the application.", app.Name, indexer.Name, indexer.Id);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,10 +19,12 @@ namespace NzbDrone.Core.Applications.LazyLibrarian
|
||||
|
||||
public class LazyLibrarianSettings : IApplicationSettings
|
||||
{
|
||||
private static readonly LazyLibrarianSettingsValidator Validator = new LazyLibrarianSettingsValidator();
|
||||
private static readonly LazyLibrarianSettingsValidator Validator = new ();
|
||||
|
||||
public LazyLibrarianSettings()
|
||||
{
|
||||
ProwlarrUrl = "http://localhost:9696";
|
||||
BaseUrl = "http://localhost:5299";
|
||||
SyncCategories = new[]
|
||||
{
|
||||
NewznabStandardCategory.AudioAudiobook.Id,
|
||||
|
||||
@@ -18,10 +18,12 @@ namespace NzbDrone.Core.Applications.Lidarr
|
||||
|
||||
public class LidarrSettings : IApplicationSettings
|
||||
{
|
||||
private static readonly LidarrSettingsValidator Validator = new LidarrSettingsValidator();
|
||||
private static readonly LidarrSettingsValidator Validator = new ();
|
||||
|
||||
public LidarrSettings()
|
||||
{
|
||||
ProwlarrUrl = "http://localhost:9696";
|
||||
BaseUrl = "http://localhost:8686";
|
||||
SyncCategories = new[] { 3000, 3010, 3030, 3040, 3050, 3060 };
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ namespace NzbDrone.Core.Applications.Lidarr
|
||||
|
||||
public class LidarrV1Proxy : ILidarrV1Proxy
|
||||
{
|
||||
private const string AppApiRoute = "/api/v1";
|
||||
private const string AppIndexerApiRoute = $"{AppApiRoute}/indexer";
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly Logger _logger;
|
||||
|
||||
@@ -34,13 +36,13 @@ namespace NzbDrone.Core.Applications.Lidarr
|
||||
|
||||
public LidarrStatus GetStatus(LidarrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, "/api/v1/system/status", HttpMethod.Get);
|
||||
var request = BuildRequest(settings, $"{AppApiRoute}/system/status", HttpMethod.Get);
|
||||
return Execute<LidarrStatus>(request);
|
||||
}
|
||||
|
||||
public List<LidarrIndexer> GetIndexers(LidarrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, "/api/v1/indexer", HttpMethod.Get);
|
||||
var request = BuildRequest(settings, $"{AppIndexerApiRoute}", HttpMethod.Get);
|
||||
return Execute<List<LidarrIndexer>>(request);
|
||||
}
|
||||
|
||||
@@ -48,7 +50,7 @@ namespace NzbDrone.Core.Applications.Lidarr
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = BuildRequest(settings, $"/api/v1/indexer/{indexerId}", HttpMethod.Get);
|
||||
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/{indexerId}", HttpMethod.Get);
|
||||
return Execute<LidarrIndexer>(request);
|
||||
}
|
||||
catch (HttpException ex)
|
||||
@@ -64,37 +66,37 @@ namespace NzbDrone.Core.Applications.Lidarr
|
||||
|
||||
public void RemoveIndexer(int indexerId, LidarrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, $"/api/v1/indexer/{indexerId}", HttpMethod.Delete);
|
||||
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/{indexerId}", HttpMethod.Delete);
|
||||
_httpClient.Execute(request);
|
||||
}
|
||||
|
||||
public List<LidarrIndexer> GetIndexerSchema(LidarrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, "/api/v1/indexer/schema", HttpMethod.Get);
|
||||
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/schema", HttpMethod.Get);
|
||||
return Execute<List<LidarrIndexer>>(request);
|
||||
}
|
||||
|
||||
public LidarrIndexer AddIndexer(LidarrIndexer indexer, LidarrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, "/api/v1/indexer", HttpMethod.Post);
|
||||
var request = BuildRequest(settings, $"{AppIndexerApiRoute}", HttpMethod.Post);
|
||||
|
||||
request.SetContent(indexer.ToJson());
|
||||
|
||||
return Execute<LidarrIndexer>(request);
|
||||
return ExecuteIndexerRequest(request);
|
||||
}
|
||||
|
||||
public LidarrIndexer UpdateIndexer(LidarrIndexer indexer, LidarrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, $"/api/v1/indexer/{indexer.Id}", HttpMethod.Put);
|
||||
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/{indexer.Id}", HttpMethod.Put);
|
||||
|
||||
request.SetContent(indexer.ToJson());
|
||||
|
||||
return Execute<LidarrIndexer>(request);
|
||||
return ExecuteIndexerRequest(request);
|
||||
}
|
||||
|
||||
public ValidationFailure TestConnection(LidarrIndexer indexer, LidarrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, $"/api/v1/indexer/test", HttpMethod.Post);
|
||||
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/test", HttpMethod.Post);
|
||||
|
||||
request.SetContent(indexer.ToJson());
|
||||
|
||||
@@ -134,6 +136,53 @@ namespace NzbDrone.Core.Applications.Lidarr
|
||||
return null;
|
||||
}
|
||||
|
||||
private LidarrIndexer ExecuteIndexerRequest(HttpRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Execute<LidarrIndexer>(request);
|
||||
}
|
||||
catch (HttpException ex)
|
||||
{
|
||||
switch (ex.Response.StatusCode)
|
||||
{
|
||||
case HttpStatusCode.Unauthorized:
|
||||
_logger.Error(ex, "API Key is invalid");
|
||||
break;
|
||||
case HttpStatusCode.BadRequest:
|
||||
if (ex.Response.Content.Contains("Query successful, but no results in the configured categories were returned from your indexer.", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
_logger.Error(ex, "No Results in configured categories. See FAQ Entry: Prowlarr will not sync X Indexer to App");
|
||||
break;
|
||||
}
|
||||
|
||||
_logger.Error(ex, "Invalid Request");
|
||||
break;
|
||||
case HttpStatusCode.SeeOther:
|
||||
_logger.Error(ex, "App returned redirect and is invalid. Check App URL");
|
||||
break;
|
||||
case HttpStatusCode.NotFound:
|
||||
_logger.Error(ex, "Remote indexer not found");
|
||||
break;
|
||||
default:
|
||||
_logger.Error(ex, "Unexpected response status code: {0}", ex.Response.StatusCode);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
catch (JsonReaderException ex)
|
||||
{
|
||||
_logger.Error(ex, "Unable to parse JSON response from application");
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Unable to add or update indexer");
|
||||
throw;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private HttpRequest BuildRequest(LidarrSettings settings, string resource, HttpMethod method)
|
||||
{
|
||||
var baseUrl = settings.BaseUrl.TrimEnd('/');
|
||||
|
||||
@@ -19,10 +19,12 @@ namespace NzbDrone.Core.Applications.Mylar
|
||||
|
||||
public class MylarSettings : IApplicationSettings
|
||||
{
|
||||
private static readonly MylarSettingsValidator Validator = new MylarSettingsValidator();
|
||||
private static readonly MylarSettingsValidator Validator = new ();
|
||||
|
||||
public MylarSettings()
|
||||
{
|
||||
ProwlarrUrl = "http://localhost:9696";
|
||||
BaseUrl = "http://localhost:8090";
|
||||
SyncCategories = new[] { NewznabStandardCategory.BooksComics.Id };
|
||||
}
|
||||
|
||||
|
||||
@@ -19,10 +19,12 @@ namespace NzbDrone.Core.Applications.Radarr
|
||||
|
||||
public class RadarrSettings : IApplicationSettings
|
||||
{
|
||||
private static readonly RadarrSettingsValidator Validator = new RadarrSettingsValidator();
|
||||
private static readonly RadarrSettingsValidator Validator = new ();
|
||||
|
||||
public RadarrSettings()
|
||||
{
|
||||
ProwlarrUrl = "http://localhost:9696";
|
||||
BaseUrl = "http://localhost:7878";
|
||||
SyncCategories = new[] { 2000, 2010, 2020, 2030, 2040, 2045, 2050, 2060, 2070, 2080 };
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ namespace NzbDrone.Core.Applications.Radarr
|
||||
|
||||
public class RadarrV3Proxy : IRadarrV3Proxy
|
||||
{
|
||||
private const string AppApiRoute = "/api/v3";
|
||||
private const string AppIndexerApiRoute = $"{AppApiRoute}/indexer";
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly Logger _logger;
|
||||
|
||||
@@ -34,13 +36,13 @@ namespace NzbDrone.Core.Applications.Radarr
|
||||
|
||||
public RadarrStatus GetStatus(RadarrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, "/api/v3/system/status", HttpMethod.Get);
|
||||
var request = BuildRequest(settings, $"{AppApiRoute}/system/status", HttpMethod.Get);
|
||||
return Execute<RadarrStatus>(request);
|
||||
}
|
||||
|
||||
public List<RadarrIndexer> GetIndexers(RadarrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, "/api/v3/indexer", HttpMethod.Get);
|
||||
var request = BuildRequest(settings, $"{AppIndexerApiRoute}", HttpMethod.Get);
|
||||
return Execute<List<RadarrIndexer>>(request);
|
||||
}
|
||||
|
||||
@@ -48,7 +50,7 @@ namespace NzbDrone.Core.Applications.Radarr
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = BuildRequest(settings, $"/api/v3/indexer/{indexerId}", HttpMethod.Get);
|
||||
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/{indexerId}", HttpMethod.Get);
|
||||
return Execute<RadarrIndexer>(request);
|
||||
}
|
||||
catch (HttpException ex)
|
||||
@@ -64,37 +66,37 @@ namespace NzbDrone.Core.Applications.Radarr
|
||||
|
||||
public void RemoveIndexer(int indexerId, RadarrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, $"/api/v3/indexer/{indexerId}", HttpMethod.Delete);
|
||||
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/{indexerId}", HttpMethod.Delete);
|
||||
_httpClient.Execute(request);
|
||||
}
|
||||
|
||||
public List<RadarrIndexer> GetIndexerSchema(RadarrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, "/api/v3/indexer/schema", HttpMethod.Get);
|
||||
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/schema", HttpMethod.Get);
|
||||
return Execute<List<RadarrIndexer>>(request);
|
||||
}
|
||||
|
||||
public RadarrIndexer AddIndexer(RadarrIndexer indexer, RadarrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, "/api/v3/indexer", HttpMethod.Post);
|
||||
var request = BuildRequest(settings, $"{AppIndexerApiRoute}", HttpMethod.Post);
|
||||
|
||||
request.SetContent(indexer.ToJson());
|
||||
|
||||
return Execute<RadarrIndexer>(request);
|
||||
return ExecuteIndexerRequest(request);
|
||||
}
|
||||
|
||||
public RadarrIndexer UpdateIndexer(RadarrIndexer indexer, RadarrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, $"/api/v3/indexer/{indexer.Id}", HttpMethod.Put);
|
||||
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/{indexer.Id}", HttpMethod.Put);
|
||||
|
||||
request.SetContent(indexer.ToJson());
|
||||
|
||||
return Execute<RadarrIndexer>(request);
|
||||
return ExecuteIndexerRequest(request);
|
||||
}
|
||||
|
||||
public ValidationFailure TestConnection(RadarrIndexer indexer, RadarrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, $"/api/v3/indexer/test", HttpMethod.Post);
|
||||
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/test", HttpMethod.Post);
|
||||
|
||||
request.SetContent(indexer.ToJson());
|
||||
|
||||
@@ -134,6 +136,53 @@ namespace NzbDrone.Core.Applications.Radarr
|
||||
return null;
|
||||
}
|
||||
|
||||
private RadarrIndexer ExecuteIndexerRequest(HttpRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Execute<RadarrIndexer>(request);
|
||||
}
|
||||
catch (HttpException ex)
|
||||
{
|
||||
switch (ex.Response.StatusCode)
|
||||
{
|
||||
case HttpStatusCode.Unauthorized:
|
||||
_logger.Error(ex, "API Key is invalid");
|
||||
break;
|
||||
case HttpStatusCode.BadRequest:
|
||||
if (ex.Response.Content.Contains("Query successful, but no results in the configured categories were returned from your indexer.", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
_logger.Error(ex, "No Results in configured categories. See FAQ Entry: Prowlarr will not sync X Indexer to App");
|
||||
break;
|
||||
}
|
||||
|
||||
_logger.Error(ex, "Invalid Request");
|
||||
break;
|
||||
case HttpStatusCode.SeeOther:
|
||||
_logger.Error(ex, "App returned redirect and is invalid. Check App URL");
|
||||
break;
|
||||
case HttpStatusCode.NotFound:
|
||||
_logger.Error(ex, "Remote indexer not found");
|
||||
break;
|
||||
default:
|
||||
_logger.Error(ex, "Unexpected response status code: {0}", ex.Response.StatusCode);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
catch (JsonReaderException ex)
|
||||
{
|
||||
_logger.Error(ex, "Unable to parse JSON response from application");
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Unable to add or update indexer");
|
||||
throw;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private HttpRequest BuildRequest(RadarrSettings settings, string resource, HttpMethod method)
|
||||
{
|
||||
var baseUrl = settings.BaseUrl.TrimEnd('/');
|
||||
|
||||
@@ -19,10 +19,12 @@ namespace NzbDrone.Core.Applications.Readarr
|
||||
|
||||
public class ReadarrSettings : IApplicationSettings
|
||||
{
|
||||
private static readonly ReadarrSettingsValidator Validator = new ReadarrSettingsValidator();
|
||||
private static readonly ReadarrSettingsValidator Validator = new ();
|
||||
|
||||
public ReadarrSettings()
|
||||
{
|
||||
ProwlarrUrl = "http://localhost:9696";
|
||||
BaseUrl = "http://localhost:8787";
|
||||
SyncCategories = new[] { 3030, 7000, 7010, 7020, 7030, 7040, 7050, 7060 };
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ namespace NzbDrone.Core.Applications.Readarr
|
||||
|
||||
public class ReadarrV1Proxy : IReadarrV1Proxy
|
||||
{
|
||||
private const string AppApiRoute = "/api/v1";
|
||||
private const string AppIndexerApiRoute = $"{AppApiRoute}/indexer";
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly Logger _logger;
|
||||
|
||||
@@ -34,13 +36,13 @@ namespace NzbDrone.Core.Applications.Readarr
|
||||
|
||||
public ReadarrStatus GetStatus(ReadarrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, "/api/v1/system/status", HttpMethod.Get);
|
||||
var request = BuildRequest(settings, $"{AppApiRoute}/system/status", HttpMethod.Get);
|
||||
return Execute<ReadarrStatus>(request);
|
||||
}
|
||||
|
||||
public List<ReadarrIndexer> GetIndexers(ReadarrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, "/api/v1/indexer", HttpMethod.Get);
|
||||
var request = BuildRequest(settings, $"{AppIndexerApiRoute}", HttpMethod.Get);
|
||||
return Execute<List<ReadarrIndexer>>(request);
|
||||
}
|
||||
|
||||
@@ -48,7 +50,7 @@ namespace NzbDrone.Core.Applications.Readarr
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = BuildRequest(settings, $"/api/v1/indexer/{indexerId}", HttpMethod.Get);
|
||||
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/{indexerId}", HttpMethod.Get);
|
||||
return Execute<ReadarrIndexer>(request);
|
||||
}
|
||||
catch (HttpException ex)
|
||||
@@ -64,37 +66,37 @@ namespace NzbDrone.Core.Applications.Readarr
|
||||
|
||||
public void RemoveIndexer(int indexerId, ReadarrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, $"/api/v1/indexer/{indexerId}", HttpMethod.Delete);
|
||||
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/{indexerId}", HttpMethod.Delete);
|
||||
_httpClient.Execute(request);
|
||||
}
|
||||
|
||||
public List<ReadarrIndexer> GetIndexerSchema(ReadarrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, "/api/v1/indexer/schema", HttpMethod.Get);
|
||||
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/schema", HttpMethod.Get);
|
||||
return Execute<List<ReadarrIndexer>>(request);
|
||||
}
|
||||
|
||||
public ReadarrIndexer AddIndexer(ReadarrIndexer indexer, ReadarrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, "/api/v1/indexer", HttpMethod.Post);
|
||||
var request = BuildRequest(settings, $"{AppIndexerApiRoute}", HttpMethod.Post);
|
||||
|
||||
request.SetContent(indexer.ToJson());
|
||||
|
||||
return Execute<ReadarrIndexer>(request);
|
||||
return ExecuteIndexerRequest(request);
|
||||
}
|
||||
|
||||
public ReadarrIndexer UpdateIndexer(ReadarrIndexer indexer, ReadarrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, $"/api/v1/indexer/{indexer.Id}", HttpMethod.Put);
|
||||
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/{indexer.Id}", HttpMethod.Put);
|
||||
|
||||
request.SetContent(indexer.ToJson());
|
||||
|
||||
return Execute<ReadarrIndexer>(request);
|
||||
return ExecuteIndexerRequest(request);
|
||||
}
|
||||
|
||||
public ValidationFailure TestConnection(ReadarrIndexer indexer, ReadarrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, $"/api/v1/indexer/test", HttpMethod.Post);
|
||||
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/test", HttpMethod.Post);
|
||||
|
||||
request.SetContent(indexer.ToJson());
|
||||
|
||||
@@ -134,6 +136,53 @@ namespace NzbDrone.Core.Applications.Readarr
|
||||
return null;
|
||||
}
|
||||
|
||||
private ReadarrIndexer ExecuteIndexerRequest(HttpRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Execute<ReadarrIndexer>(request);
|
||||
}
|
||||
catch (HttpException ex)
|
||||
{
|
||||
switch (ex.Response.StatusCode)
|
||||
{
|
||||
case HttpStatusCode.Unauthorized:
|
||||
_logger.Error(ex, "API Key is invalid");
|
||||
break;
|
||||
case HttpStatusCode.BadRequest:
|
||||
if (ex.Response.Content.Contains("Query successful, but no results in the configured categories were returned from your indexer.", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
_logger.Error(ex, "No Results in configured categories. See FAQ Entry: Prowlarr will not sync X Indexer to App");
|
||||
break;
|
||||
}
|
||||
|
||||
_logger.Error(ex, "Invalid Request");
|
||||
break;
|
||||
case HttpStatusCode.SeeOther:
|
||||
_logger.Error(ex, "App returned redirect and is invalid. Check App URL");
|
||||
break;
|
||||
case HttpStatusCode.NotFound:
|
||||
_logger.Error(ex, "Remote indexer not found");
|
||||
break;
|
||||
default:
|
||||
_logger.Error(ex, "Unexpected response status code: {0}", ex.Response.StatusCode);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
catch (JsonReaderException ex)
|
||||
{
|
||||
_logger.Error(ex, "Unable to parse JSON response from application");
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Unable to add or update indexer");
|
||||
throw;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private HttpRequest BuildRequest(ReadarrSettings settings, string resource, HttpMethod method)
|
||||
{
|
||||
var baseUrl = settings.BaseUrl.TrimEnd('/');
|
||||
|
||||
@@ -18,10 +18,12 @@ namespace NzbDrone.Core.Applications.Sonarr
|
||||
|
||||
public class SonarrSettings : IApplicationSettings
|
||||
{
|
||||
private static readonly SonarrSettingsValidator Validator = new SonarrSettingsValidator();
|
||||
private static readonly SonarrSettingsValidator Validator = new ();
|
||||
|
||||
public SonarrSettings()
|
||||
{
|
||||
ProwlarrUrl = "http://localhost:9696";
|
||||
BaseUrl = "http://localhost:8989";
|
||||
SyncCategories = new[] { 5000, 5010, 5020, 5030, 5040, 5045, 5050 };
|
||||
AnimeSyncCategories = new[] { 5070 };
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ namespace NzbDrone.Core.Applications.Sonarr
|
||||
|
||||
public class SonarrV3Proxy : ISonarrV3Proxy
|
||||
{
|
||||
private const string AppApiRoute = "/api/v3";
|
||||
private const string AppIndexerApiRoute = $"{AppApiRoute}/indexer";
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly Logger _logger;
|
||||
|
||||
@@ -34,13 +36,13 @@ namespace NzbDrone.Core.Applications.Sonarr
|
||||
|
||||
public SonarrStatus GetStatus(SonarrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, "/api/v3/system/status", HttpMethod.Get);
|
||||
var request = BuildRequest(settings, $"{AppApiRoute}/system/status", HttpMethod.Get);
|
||||
return Execute<SonarrStatus>(request);
|
||||
}
|
||||
|
||||
public List<SonarrIndexer> GetIndexers(SonarrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, "/api/v3/indexer", HttpMethod.Get);
|
||||
var request = BuildRequest(settings, $"{AppIndexerApiRoute}", HttpMethod.Get);
|
||||
return Execute<List<SonarrIndexer>>(request);
|
||||
}
|
||||
|
||||
@@ -48,7 +50,7 @@ namespace NzbDrone.Core.Applications.Sonarr
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = BuildRequest(settings, $"/api/v3/indexer/{indexerId}", HttpMethod.Get);
|
||||
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/{indexerId}", HttpMethod.Get);
|
||||
return Execute<SonarrIndexer>(request);
|
||||
}
|
||||
catch (HttpException ex)
|
||||
@@ -64,37 +66,37 @@ namespace NzbDrone.Core.Applications.Sonarr
|
||||
|
||||
public void RemoveIndexer(int indexerId, SonarrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, $"/api/v3/indexer/{indexerId}", HttpMethod.Delete);
|
||||
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/{indexerId}", HttpMethod.Delete);
|
||||
_httpClient.Execute(request);
|
||||
}
|
||||
|
||||
public List<SonarrIndexer> GetIndexerSchema(SonarrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, "/api/v3/indexer/schema", HttpMethod.Get);
|
||||
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/schema", HttpMethod.Get);
|
||||
return Execute<List<SonarrIndexer>>(request);
|
||||
}
|
||||
|
||||
public SonarrIndexer AddIndexer(SonarrIndexer indexer, SonarrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, "/api/v3/indexer", HttpMethod.Post);
|
||||
var request = BuildRequest(settings, $"{AppIndexerApiRoute}", HttpMethod.Post);
|
||||
|
||||
request.SetContent(indexer.ToJson());
|
||||
|
||||
return Execute<SonarrIndexer>(request);
|
||||
return ExecuteIndexerRequest(request);
|
||||
}
|
||||
|
||||
public SonarrIndexer UpdateIndexer(SonarrIndexer indexer, SonarrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, $"/api/v3/indexer/{indexer.Id}", HttpMethod.Put);
|
||||
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/{indexer.Id}", HttpMethod.Put);
|
||||
|
||||
request.SetContent(indexer.ToJson());
|
||||
|
||||
return Execute<SonarrIndexer>(request);
|
||||
return ExecuteIndexerRequest(request);
|
||||
}
|
||||
|
||||
public ValidationFailure TestConnection(SonarrIndexer indexer, SonarrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, $"/api/v3/indexer/test", HttpMethod.Post);
|
||||
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/test", HttpMethod.Post);
|
||||
|
||||
request.SetContent(indexer.ToJson());
|
||||
|
||||
@@ -140,6 +142,53 @@ namespace NzbDrone.Core.Applications.Sonarr
|
||||
return null;
|
||||
}
|
||||
|
||||
private SonarrIndexer ExecuteIndexerRequest(HttpRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Execute<SonarrIndexer>(request);
|
||||
}
|
||||
catch (HttpException ex)
|
||||
{
|
||||
switch (ex.Response.StatusCode)
|
||||
{
|
||||
case HttpStatusCode.Unauthorized:
|
||||
_logger.Error(ex, "API Key is invalid");
|
||||
break;
|
||||
case HttpStatusCode.BadRequest:
|
||||
if (ex.Response.Content.Contains("Query successful, but no results in the configured categories were returned from your indexer.", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
_logger.Error(ex, "No Results in configured categories. See FAQ Entry: Prowlarr will not sync X Indexer to App");
|
||||
break;
|
||||
}
|
||||
|
||||
_logger.Error(ex, "Invalid Request");
|
||||
break;
|
||||
case HttpStatusCode.SeeOther:
|
||||
_logger.Error(ex, "App returned redirect and is invalid. Check App URL");
|
||||
break;
|
||||
case HttpStatusCode.NotFound:
|
||||
_logger.Error(ex, "Remote indexer not found");
|
||||
break;
|
||||
default:
|
||||
_logger.Error(ex, "Unexpected response status code: {0}", ex.Response.StatusCode);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
catch (JsonReaderException ex)
|
||||
{
|
||||
_logger.Error(ex, "Unable to parse JSON response from application");
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Unable to add or update indexer");
|
||||
throw;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private HttpRequest BuildRequest(SonarrSettings settings, string resource, HttpMethod method)
|
||||
{
|
||||
var baseUrl = settings.BaseUrl.TrimEnd('/');
|
||||
|
||||
@@ -19,10 +19,12 @@ namespace NzbDrone.Core.Applications.Whisparr
|
||||
|
||||
public class WhisparrSettings : IApplicationSettings
|
||||
{
|
||||
private static readonly WhisparrSettingsValidator Validator = new WhisparrSettingsValidator();
|
||||
private static readonly WhisparrSettingsValidator Validator = new ();
|
||||
|
||||
public WhisparrSettings()
|
||||
{
|
||||
ProwlarrUrl = "http://localhost:9696";
|
||||
BaseUrl = "http://localhost:6969";
|
||||
SyncCategories = new[] { 6000, 6010, 6020, 6030, 6040, 6045, 6050, 6070, 6080, 6090 };
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ namespace NzbDrone.Core.Applications.Whisparr
|
||||
|
||||
public class WhisparrV3Proxy : IWhisparrV3Proxy
|
||||
{
|
||||
private const string AppApiRoute = "/api/v3";
|
||||
private const string AppIndexerApiRoute = $"{AppApiRoute}/indexer";
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly Logger _logger;
|
||||
|
||||
@@ -34,13 +36,13 @@ namespace NzbDrone.Core.Applications.Whisparr
|
||||
|
||||
public WhisparrStatus GetStatus(WhisparrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, "/api/v3/system/status", HttpMethod.Get);
|
||||
var request = BuildRequest(settings, $"{AppApiRoute}/system/status", HttpMethod.Get);
|
||||
return Execute<WhisparrStatus>(request);
|
||||
}
|
||||
|
||||
public List<WhisparrIndexer> GetIndexers(WhisparrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, "/api/v3/indexer", HttpMethod.Get);
|
||||
var request = BuildRequest(settings, $"{AppIndexerApiRoute}", HttpMethod.Get);
|
||||
return Execute<List<WhisparrIndexer>>(request);
|
||||
}
|
||||
|
||||
@@ -48,7 +50,7 @@ namespace NzbDrone.Core.Applications.Whisparr
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = BuildRequest(settings, $"/api/v3/indexer/{indexerId}", HttpMethod.Get);
|
||||
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/{indexerId}", HttpMethod.Get);
|
||||
return Execute<WhisparrIndexer>(request);
|
||||
}
|
||||
catch (HttpException ex)
|
||||
@@ -64,19 +66,19 @@ namespace NzbDrone.Core.Applications.Whisparr
|
||||
|
||||
public void RemoveIndexer(int indexerId, WhisparrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, $"/api/v3/indexer/{indexerId}", HttpMethod.Delete);
|
||||
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/{indexerId}", HttpMethod.Delete);
|
||||
_httpClient.Execute(request);
|
||||
}
|
||||
|
||||
public List<WhisparrIndexer> GetIndexerSchema(WhisparrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, "/api/v3/indexer/schema", HttpMethod.Get);
|
||||
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/schema", HttpMethod.Get);
|
||||
return Execute<List<WhisparrIndexer>>(request);
|
||||
}
|
||||
|
||||
public WhisparrIndexer AddIndexer(WhisparrIndexer indexer, WhisparrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, "/api/v3/indexer", HttpMethod.Post);
|
||||
var request = BuildRequest(settings, $"{AppIndexerApiRoute}", HttpMethod.Post);
|
||||
|
||||
request.SetContent(indexer.ToJson());
|
||||
|
||||
@@ -85,16 +87,16 @@ namespace NzbDrone.Core.Applications.Whisparr
|
||||
|
||||
public WhisparrIndexer UpdateIndexer(WhisparrIndexer indexer, WhisparrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, $"/api/v3/indexer/{indexer.Id}", HttpMethod.Put);
|
||||
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/{indexer.Id}", HttpMethod.Put);
|
||||
|
||||
request.SetContent(indexer.ToJson());
|
||||
|
||||
return Execute<WhisparrIndexer>(request);
|
||||
return ExecuteIndexerRequest(request);
|
||||
}
|
||||
|
||||
public ValidationFailure TestConnection(WhisparrIndexer indexer, WhisparrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, $"/api/v3/indexer/test", HttpMethod.Post);
|
||||
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/test", HttpMethod.Post);
|
||||
|
||||
request.SetContent(indexer.ToJson());
|
||||
|
||||
@@ -134,6 +136,53 @@ namespace NzbDrone.Core.Applications.Whisparr
|
||||
return null;
|
||||
}
|
||||
|
||||
private WhisparrIndexer ExecuteIndexerRequest(HttpRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Execute<WhisparrIndexer>(request);
|
||||
}
|
||||
catch (HttpException ex)
|
||||
{
|
||||
switch (ex.Response.StatusCode)
|
||||
{
|
||||
case HttpStatusCode.Unauthorized:
|
||||
_logger.Error(ex, "API Key is invalid");
|
||||
break;
|
||||
case HttpStatusCode.BadRequest:
|
||||
if (ex.Response.Content.Contains("Query successful, but no results in the configured categories were returned from your indexer.", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
_logger.Error(ex, "No Results in configured categories. See FAQ Entry: Prowlarr will not sync X Indexer to App");
|
||||
break;
|
||||
}
|
||||
|
||||
_logger.Error(ex, "Invalid Request");
|
||||
break;
|
||||
case HttpStatusCode.SeeOther:
|
||||
_logger.Error(ex, "App returned redirect and is invalid. Check App URL");
|
||||
break;
|
||||
case HttpStatusCode.NotFound:
|
||||
_logger.Error(ex, "Remote indexer not found");
|
||||
break;
|
||||
default:
|
||||
_logger.Error(ex, "Unexpected response status code: {0}", ex.Response.StatusCode);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
catch (JsonReaderException ex)
|
||||
{
|
||||
_logger.Error(ex, "Unable to parse JSON response from application");
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Unable to add or update indexer");
|
||||
throw;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private HttpRequest BuildRequest(WhisparrSettings settings, string resource, HttpMethod method)
|
||||
{
|
||||
var baseUrl = settings.BaseUrl.TrimEnd('/');
|
||||
|
||||
@@ -69,7 +69,8 @@ namespace NzbDrone.Core.Backup
|
||||
_diskProvider.EnsureFolder(_backupTempFolder);
|
||||
_diskProvider.EnsureFolder(GetBackupFolder(backupType));
|
||||
|
||||
var backupFilename = string.Format("prowlarr_backup_v{0}_{1:yyyy.MM.dd_HH.mm.ss}.zip", BuildInfo.Version, DateTime.Now);
|
||||
var dateNow = DateTime.Now;
|
||||
var backupFilename = $"prowlarr_backup_v{BuildInfo.Version}_{dateNow:yyyy.MM.dd_HH.mm.ss}.zip";
|
||||
var backupPath = Path.Combine(GetBackupFolder(backupType), backupFilename);
|
||||
|
||||
Cleanup();
|
||||
@@ -81,7 +82,7 @@ namespace NzbDrone.Core.Backup
|
||||
|
||||
BackupConfigFile();
|
||||
BackupDatabase();
|
||||
CreateVersionInfo();
|
||||
CreateVersionInfo(dateNow);
|
||||
|
||||
_logger.ProgressDebug("Creating backup zip");
|
||||
|
||||
@@ -208,11 +209,15 @@ namespace NzbDrone.Core.Backup
|
||||
_diskTransferService.TransferFile(configFile, tempConfigFile, TransferMode.Copy);
|
||||
}
|
||||
|
||||
private void CreateVersionInfo()
|
||||
private void CreateVersionInfo(DateTime dateNow)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
var tempFile = Path.Combine(_backupTempFolder, "INFO");
|
||||
|
||||
builder.AppendLine(BuildInfo.Version.ToString());
|
||||
var builder = new StringBuilder();
|
||||
builder.AppendLine($"v{BuildInfo.Version}");
|
||||
builder.AppendLine($"{dateNow:yyyy-MM-dd HH:mm:ss}");
|
||||
|
||||
_diskProvider.WriteAllText(tempFile, builder.ToString());
|
||||
}
|
||||
|
||||
private void CleanupOldBackups(BackupType backupType)
|
||||
|
||||
@@ -25,6 +25,7 @@ namespace NzbDrone.Core.Configuration
|
||||
{
|
||||
Dictionary<string, object> GetConfigDictionary();
|
||||
void SaveConfigDictionary(Dictionary<string, object> configValues);
|
||||
void EnsureDefaultConfigFile();
|
||||
|
||||
string BindAddress { get; }
|
||||
int Port { get; }
|
||||
@@ -258,7 +259,7 @@ namespace NzbDrone.Core.Configuration
|
||||
|
||||
public T GetValueEnum<T>(string key, T defaultValue, bool persist = true)
|
||||
{
|
||||
return (T)Enum.Parse(typeof(T), GetValue(key, defaultValue), persist);
|
||||
return (T)Enum.Parse(typeof(T), GetValue(key, defaultValue, persist), true);
|
||||
}
|
||||
|
||||
public string GetValue(string key, object defaultValue, bool persist = true)
|
||||
@@ -317,7 +318,7 @@ namespace NzbDrone.Core.Configuration
|
||||
SetValue(key, value.ToString().ToLower());
|
||||
}
|
||||
|
||||
private void EnsureDefaultConfigFile()
|
||||
public void EnsureDefaultConfigFile()
|
||||
{
|
||||
if (!File.Exists(_configFile))
|
||||
{
|
||||
|
||||
@@ -103,7 +103,7 @@ namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
if (!ids.Any())
|
||||
{
|
||||
return new List<TModel>();
|
||||
return Array.Empty<TModel>();
|
||||
}
|
||||
|
||||
var result = Query(x => ids.Contains(x.Id));
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.Data;
|
||||
using System.Data.Common;
|
||||
using System.Data.SQLite;
|
||||
using System.Text.RegularExpressions;
|
||||
using Dapper;
|
||||
using NLog;
|
||||
@@ -38,17 +40,9 @@ namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
get
|
||||
{
|
||||
using (var db = _datamapperFactory())
|
||||
{
|
||||
if (db.ConnectionString.Contains(".db"))
|
||||
{
|
||||
return DatabaseType.SQLite;
|
||||
}
|
||||
else
|
||||
{
|
||||
return DatabaseType.PostgreSQL;
|
||||
}
|
||||
}
|
||||
using var db = _datamapperFactory();
|
||||
|
||||
return db is SQLiteConnection ? DatabaseType.SQLite : DatabaseType.PostgreSQL;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,24 +50,11 @@ namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
get
|
||||
{
|
||||
using (var db = _datamapperFactory())
|
||||
{
|
||||
string version;
|
||||
using var db = _datamapperFactory();
|
||||
var dbConnection = db as DbConnection;
|
||||
var version = Regex.Replace(dbConnection.ServerVersion, @"\(.*?\)", "");
|
||||
|
||||
try
|
||||
{
|
||||
version = db.QueryFirstOrDefault<string>("SHOW server_version");
|
||||
|
||||
//Postgres can return extra info about operating system on version call, ignore this
|
||||
version = Regex.Replace(version, @"\(.*?\)", "");
|
||||
}
|
||||
catch
|
||||
{
|
||||
version = db.QueryFirstOrDefault<string>("SELECT sqlite_version()");
|
||||
}
|
||||
|
||||
return new Version(version);
|
||||
}
|
||||
return new Version(version);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
using System.Data;
|
||||
using Dapper;
|
||||
using FluentMigrator;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(031)]
|
||||
public class apprise_server_url : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Execute.WithConnection(MigrateToServerUrl);
|
||||
}
|
||||
|
||||
private void MigrateToServerUrl(IDbConnection conn, IDbTransaction tran)
|
||||
{
|
||||
using var selectCommand = conn.CreateCommand();
|
||||
selectCommand.Transaction = tran;
|
||||
selectCommand.CommandText = "SELECT \"Id\", \"Settings\" FROM \"Notifications\" WHERE \"Implementation\" = 'Apprise'";
|
||||
|
||||
using var reader = selectCommand.ExecuteReader();
|
||||
|
||||
while (reader.Read())
|
||||
{
|
||||
var id = reader.GetInt32(0);
|
||||
var settings = reader.GetString(1);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(settings))
|
||||
{
|
||||
var jsonObject = Json.Deserialize<JObject>(settings);
|
||||
|
||||
if (jsonObject.ContainsKey("baseUrl"))
|
||||
{
|
||||
jsonObject.Add("serverUrl", jsonObject.Value<string>("baseUrl"));
|
||||
jsonObject.Remove("baseUrl");
|
||||
}
|
||||
|
||||
settings = jsonObject.ToJson();
|
||||
}
|
||||
|
||||
var parameters = new { Settings = settings, Id = id };
|
||||
conn.Execute("UPDATE Notifications SET Settings = @Settings WHERE Id = @Id", parameters, transaction: tran);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using NLog;
|
||||
@@ -64,7 +65,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation.Proxies
|
||||
catch (DownloadClientException e)
|
||||
{
|
||||
_logger.Error(e);
|
||||
return new List<DownloadStationTask>();
|
||||
return Array.Empty<DownloadStationTask>();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,11 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
return true;
|
||||
}
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.Unauthorized)
|
||||
{
|
||||
throw new DownloadClientException("Failed to connect to qBittorrent. Check your settings and qBittorrent configuration.", new HttpException(response));
|
||||
}
|
||||
|
||||
if (response.HasHttpError)
|
||||
{
|
||||
throw new DownloadClientException("Failed to connect to qBittorrent, check your settings.", new HttpException(response));
|
||||
|
||||
@@ -16,12 +16,12 @@ namespace NzbDrone.Core.Download.Clients.rTorrent
|
||||
public RTorrentDirectoryValidator(PathExistsValidator pathExistsValidator,
|
||||
MappedNetworkDriveValidator mappedNetworkDriveValidator)
|
||||
{
|
||||
RuleFor(c => c.Directory).Cascade(CascadeMode.StopOnFirstFailure)
|
||||
.IsValidPath()
|
||||
.SetValidator(mappedNetworkDriveValidator)
|
||||
.SetValidator(pathExistsValidator)
|
||||
.When(c => c.Directory.IsNotNullOrWhiteSpace())
|
||||
.When(c => c.Host == "localhost" || c.Host == "127.0.0.1");
|
||||
RuleFor(c => c.Directory).Cascade(CascadeMode.Stop)
|
||||
.IsValidPath()
|
||||
.SetValidator(mappedNetworkDriveValidator)
|
||||
.SetValidator(pathExistsValidator)
|
||||
.When(c => c.Directory.IsNotNullOrWhiteSpace())
|
||||
.When(c => c.Host == "localhost" || c.Host == "127.0.0.1");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
#if !LIBRARY
|
||||
using NzbDrone.Common.EnsureThat;
|
||||
#endif
|
||||
|
||||
namespace NzbDrone.Core
|
||||
{
|
||||
@@ -12,9 +10,8 @@ namespace NzbDrone.Core
|
||||
{
|
||||
public static string WithDefault(this string actual, object defaultValue)
|
||||
{
|
||||
#if !LIBRARY
|
||||
Ensure.That(defaultValue, () => defaultValue).IsNotNull();
|
||||
#endif
|
||||
|
||||
if (string.IsNullOrWhiteSpace(actual))
|
||||
{
|
||||
return defaultValue.ToString();
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Indexers.Cardigann;
|
||||
using NzbDrone.Core.Indexers.Definitions.Cardigann;
|
||||
using NzbDrone.Core.IndexerVersions;
|
||||
using NzbDrone.Core.Localization;
|
||||
using NzbDrone.Core.ThingiProvider.Events;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Indexers.Cardigann;
|
||||
using NzbDrone.Core.Indexers.Definitions.Cardigann;
|
||||
using NzbDrone.Core.IndexerVersions;
|
||||
using NzbDrone.Core.Localization;
|
||||
using NzbDrone.Core.ThingiProvider.Events;
|
||||
|
||||
@@ -68,12 +68,9 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
}
|
||||
}
|
||||
|
||||
if (BuildInfo.BuildDateTime < DateTime.UtcNow.AddDays(-14))
|
||||
if (BuildInfo.BuildDateTime < DateTime.UtcNow.AddDays(-14) && _checkUpdateService.AvailableUpdate() != null)
|
||||
{
|
||||
if (_checkUpdateService.AvailableUpdate() != null)
|
||||
{
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Warning, "New update is available");
|
||||
}
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Warning, _localizationService.GetLocalizedString("UpdateAvailable"), "#new-update-is-available");
|
||||
}
|
||||
|
||||
return new HealthCheck(GetType());
|
||||
|
||||
@@ -31,9 +31,7 @@ namespace NzbDrone.Core.History
|
||||
|
||||
public History MostRecentForDownloadId(string downloadId)
|
||||
{
|
||||
return FindByDownloadId(downloadId)
|
||||
.OrderByDescending(h => h.Date)
|
||||
.FirstOrDefault();
|
||||
return FindByDownloadId(downloadId).MaxBy(h => h.Date);
|
||||
}
|
||||
|
||||
public List<History> FindByDownloadId(string downloadId)
|
||||
@@ -75,9 +73,7 @@ namespace NzbDrone.Core.History
|
||||
|
||||
public History MostRecentForIndexer(int indexerId)
|
||||
{
|
||||
return Query(x => x.IndexerId == indexerId)
|
||||
.OrderByDescending(h => h.Date)
|
||||
.FirstOrDefault();
|
||||
return Query(x => x.IndexerId == indexerId).MaxBy(h => h.Date);
|
||||
}
|
||||
|
||||
public List<History> Between(DateTime start, DateTime end)
|
||||
|
||||
@@ -118,12 +118,14 @@ namespace NzbDrone.Core.History
|
||||
|
||||
public void Handle(IndexerQueryEvent message)
|
||||
{
|
||||
var response = message.QueryResult.Response;
|
||||
|
||||
var history = new History
|
||||
{
|
||||
Date = DateTime.UtcNow,
|
||||
IndexerId = message.IndexerId,
|
||||
EventType = message.Query.IsRssSearch ? HistoryEventType.IndexerRss : HistoryEventType.IndexerQuery,
|
||||
Successful = message.QueryResult.Response?.StatusCode == HttpStatusCode.OK
|
||||
Successful = response?.StatusCode == HttpStatusCode.OK || (response is { Request: { SuppressHttpError: true, SuppressHttpErrorStatusCodes: not null } } && response.Request.SuppressHttpErrorStatusCodes.Contains(response.StatusCode))
|
||||
};
|
||||
|
||||
if (message.Query is MovieSearchCriteria)
|
||||
@@ -168,7 +170,7 @@ namespace NzbDrone.Core.History
|
||||
history.Data.Add("Genre", ((BookSearchCriteria)message.Query).Genre ?? string.Empty);
|
||||
}
|
||||
|
||||
history.Data.Add("ElapsedTime", message.QueryResult.Response?.ElapsedTime.ToString() ?? string.Empty);
|
||||
history.Data.Add("ElapsedTime", message.QueryResult.Cached ? "0" : message.QueryResult.Response?.ElapsedTime.ToString() ?? string.Empty);
|
||||
history.Data.Add("Query", message.Query.SearchTerm ?? string.Empty);
|
||||
history.Data.Add("QueryType", message.Query.SearchType ?? string.Empty);
|
||||
history.Data.Add("Categories", string.Join(",", message.Query.Categories) ?? string.Empty);
|
||||
@@ -176,6 +178,7 @@ namespace NzbDrone.Core.History
|
||||
history.Data.Add("Host", message.Query.Host ?? 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);
|
||||
history.Data.Add("Cached", message.QueryResult.Cached ? "1" : "0");
|
||||
|
||||
_historyRepository.Insert(history);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
@"(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F\uFEFF\uFFFE\uFFFF]",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
public List<ReleaseInfo> Releases { get; set; }
|
||||
public IList<ReleaseInfo> Releases { get; set; }
|
||||
|
||||
private static string RemoveInvalidXMLChars(string text)
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@ using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Indexers.Events;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.IndexerSearch
|
||||
@@ -56,7 +57,9 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
{
|
||||
var searchSpec = Get<MovieSearchCriteria>(request, indexerIds, interactiveSearch);
|
||||
|
||||
searchSpec.ImdbId = request.imdbid;
|
||||
var imdbId = ParseUtil.GetImdbID(request.imdbid);
|
||||
|
||||
searchSpec.ImdbId = imdbId.HasValue ? imdbId.Value.ToString() : null;
|
||||
searchSpec.TmdbId = request.tmdbid;
|
||||
searchSpec.TraktId = request.traktid;
|
||||
searchSpec.DoubanId = request.doubanid;
|
||||
@@ -84,10 +87,12 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
{
|
||||
var searchSpec = Get<TvSearchCriteria>(request, indexerIds, interactiveSearch);
|
||||
|
||||
var imdbId = ParseUtil.GetImdbID(request.imdbid);
|
||||
|
||||
searchSpec.ImdbId = imdbId.HasValue ? imdbId.Value.ToString() : null;
|
||||
searchSpec.Season = request.season;
|
||||
searchSpec.Episode = request.ep;
|
||||
searchSpec.TvdbId = request.tvdbid;
|
||||
searchSpec.ImdbId = request.imdbid;
|
||||
searchSpec.TraktId = request.traktid;
|
||||
searchSpec.TmdbId = request.tmdbid;
|
||||
searchSpec.DoubanId = request.doubanid;
|
||||
@@ -136,7 +141,7 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
spec.Categories = Array.Empty<int>();
|
||||
}
|
||||
|
||||
spec.SearchTerm = query.q;
|
||||
spec.SearchTerm = query.q?.Trim();
|
||||
spec.SearchType = query.t;
|
||||
spec.Limit = query.limit;
|
||||
spec.Offset = query.offset;
|
||||
@@ -148,7 +153,7 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
return spec;
|
||||
}
|
||||
|
||||
private async Task<List<ReleaseInfo>> Dispatch(Func<IIndexer, Task<IndexerPageableQueryResult>> searchAction, SearchCriteriaBase criteriaBase)
|
||||
private async Task<IList<ReleaseInfo>> Dispatch(Func<IIndexer, Task<IndexerPageableQueryResult>> searchAction, SearchCriteriaBase criteriaBase)
|
||||
{
|
||||
var indexers = _indexerFactory.Enabled();
|
||||
|
||||
@@ -168,7 +173,7 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
if (indexers.Count == 0)
|
||||
{
|
||||
_logger.Debug("All provided categories are unsupported by selected indexers: {0}", string.Join(", ", criteriaBase.Categories));
|
||||
return new List<ReleaseInfo>();
|
||||
return Array.Empty<ReleaseInfo>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,7 +194,7 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
{
|
||||
if (_indexerLimitService.AtQueryLimit((IndexerDefinition)indexer.Definition))
|
||||
{
|
||||
return new List<ReleaseInfo>();
|
||||
return Array.Empty<ReleaseInfo>();
|
||||
}
|
||||
|
||||
try
|
||||
@@ -224,7 +229,7 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
_logger.Error(e, "Error while searching for {0}", criteriaBase);
|
||||
}
|
||||
|
||||
return new List<ReleaseInfo>();
|
||||
return Array.Empty<ReleaseInfo>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Indexers.Cardigann;
|
||||
using NzbDrone.Core.Indexers.Definitions.Cardigann;
|
||||
using NzbDrone.Core.Lifecycle;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
@@ -29,7 +29,7 @@ namespace NzbDrone.Core.IndexerVersions
|
||||
/* Update Service will fall back if version # does not exist for an indexer per Ta */
|
||||
|
||||
private const string DEFINITION_BRANCH = "master";
|
||||
private const int DEFINITION_VERSION = 8;
|
||||
private const int DEFINITION_VERSION = 9;
|
||||
|
||||
// Used when moving yml to C#
|
||||
private readonly List<string> _definitionBlocklist = new ()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -25,7 +25,7 @@ public class AudioBookBay : TorrentIndexerBase<NoAuthTorrentBaseSettings>
|
||||
public override string Name => "AudioBook Bay";
|
||||
public override string[] IndexerUrls => new[]
|
||||
{
|
||||
"https://audiobookbay.li/",
|
||||
"https://audiobookbay.is/",
|
||||
"https://audiobookbay.se/"
|
||||
};
|
||||
public override string[] LegacyUrls => new[]
|
||||
@@ -53,7 +53,8 @@ public class AudioBookBay : TorrentIndexerBase<NoAuthTorrentBaseSettings>
|
||||
"https://audiobookbay.unblockit.page/",
|
||||
"https://audiobookbay.unblockit.pet/",
|
||||
"https://audiobookbay.unblockit.ink/",
|
||||
"https://audiobookbay.unblockit.bio/" // error 502
|
||||
"https://audiobookbay.unblockit.bio/", // error 502
|
||||
"https://audiobookbay.li/"
|
||||
};
|
||||
public override string Description => "AudioBook Bay (ABB) is a public Torrent Tracker for AUDIOBOOKS";
|
||||
public override string Language => "en-US";
|
||||
|
||||
@@ -59,6 +59,6 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
public class AvistaZParser : AvistazParserBase
|
||||
{
|
||||
protected override string TimezoneOffset => "+01:00";
|
||||
protected override string TimezoneOffset => "+02:00";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,9 +67,9 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
||||
_logger.Debug("Avistaz authentication succeeded.");
|
||||
}
|
||||
|
||||
protected override bool CheckIfLoginNeeded(HttpResponse response)
|
||||
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
|
||||
{
|
||||
return response.StatusCode == HttpStatusCode.Unauthorized || response.StatusCode == HttpStatusCode.PreconditionFailed;
|
||||
return httpResponse.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.PreconditionFailed;
|
||||
}
|
||||
|
||||
protected override void ModifyRequest(IndexerRequest request)
|
||||
|
||||
@@ -13,18 +13,18 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
||||
{
|
||||
public class AvistazParserBase : IParseIndexerResponse
|
||||
{
|
||||
protected virtual string TimezoneOffset => "-05:00"; // Avistaz does not specify a timezone & returns server time
|
||||
protected virtual string TimezoneOffset => "-04:00"; // Avistaz does not specify a timezone & returns server time
|
||||
private readonly HashSet<string> _hdResolutions = new () { "1080p", "1080i", "720p" };
|
||||
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
|
||||
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
||||
{
|
||||
var torrentInfos = new List<TorrentInfo>();
|
||||
var releaseInfos = new List<ReleaseInfo>();
|
||||
|
||||
if (indexerResponse.HttpResponse.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
return torrentInfos.ToArray();
|
||||
return releaseInfos.ToArray();
|
||||
}
|
||||
|
||||
if (indexerResponse.HttpResponse.StatusCode == HttpStatusCode.TooManyRequests)
|
||||
@@ -80,11 +80,13 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
||||
release.TvdbId = row.MovieTvinfo.Tvdb.IsNullOrWhiteSpace() ? 0 : ParseUtil.TryCoerceInt(row.MovieTvinfo.Tvdb, out var tvdbResult) ? tvdbResult : 0;
|
||||
}
|
||||
|
||||
torrentInfos.Add(release);
|
||||
releaseInfos.Add(release);
|
||||
}
|
||||
|
||||
// order by date
|
||||
return torrentInfos.OrderByDescending(o => o.PublishDate).ToArray();
|
||||
return releaseInfos
|
||||
.OrderByDescending(o => o.PublishDate)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
// hook to adjust category parsing
|
||||
@@ -115,7 +117,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
||||
cats.Add(NewznabStandardCategory.Audio);
|
||||
break;
|
||||
default:
|
||||
throw new Exception(string.Format("Error parsing Avistaz category type {0}", row.Type));
|
||||
throw new Exception($"Error parsing Avistaz category type {row.Type}");
|
||||
}
|
||||
|
||||
return cats;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
@@ -85,6 +86,8 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
||||
var request = new IndexerRequest(searchUrl, HttpAccept.Html);
|
||||
request.HttpRequest.Headers.Add("Authorization", $"Bearer {Settings.Token}");
|
||||
|
||||
request.HttpRequest.SuppressHttpErrorStatusCodes = new[] { HttpStatusCode.NotFound };
|
||||
|
||||
yield return request;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ using System.Threading.Tasks;
|
||||
using AngleSharp.Dom;
|
||||
using AngleSharp.Html.Parser;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Configuration;
|
||||
@@ -27,7 +28,6 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
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;
|
||||
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
|
||||
public override IndexerCapabilities Capabilities => SetCapabilities();
|
||||
@@ -59,7 +59,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
var dom = parser.ParseDocument(response.Content);
|
||||
var downloadLink = dom.QuerySelector(".download_link")?.GetAttribute("href");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(downloadLink))
|
||||
if (downloadLink.IsNullOrWhiteSpace())
|
||||
{
|
||||
throw new Exception("Unable to find download link.");
|
||||
}
|
||||
@@ -71,17 +71,19 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
UpdateCookies(null, null);
|
||||
|
||||
var requestBuilder = new HttpRequestBuilder(LoginUrl)
|
||||
var loginUrl = Settings.BaseUrl + "login.php";
|
||||
|
||||
var requestBuilder = new HttpRequestBuilder(loginUrl)
|
||||
{
|
||||
LogResponseContent = true,
|
||||
AllowAutoRedirect = true,
|
||||
Method = HttpMethod.Post
|
||||
};
|
||||
|
||||
var loginPage = await ExecuteAuth(new HttpRequest(LoginUrl));
|
||||
var loginPage = await ExecuteAuth(new HttpRequest(loginUrl));
|
||||
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(loginPage.Content);
|
||||
var dom = await parser.ParseDocumentAsync(loginPage.Content);
|
||||
var loginKey = dom.QuerySelector("input[name=\"loginKey\"]");
|
||||
if (loginKey != null)
|
||||
{
|
||||
@@ -98,21 +100,23 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
var response = await ExecuteAuth(authLoginRequest);
|
||||
|
||||
if (response.Content != null && response.Content.Contains("<a href=\"logout.php\">Logout</a>"))
|
||||
if (CheckIfLoginNeeded(response))
|
||||
{
|
||||
UpdateCookies(response.GetCookies(), DateTime.Now.AddDays(30));
|
||||
var htmlParser = new HtmlParser();
|
||||
var document = await htmlParser.ParseDocumentAsync(response.Content);
|
||||
var errorMessage = document.QuerySelector("#loginError, .error")?.TextContent.Trim();
|
||||
|
||||
_logger.Debug("BakaBT authentication succeeded");
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new IndexerAuthException("BakaBT authentication failed");
|
||||
throw new IndexerAuthException(errorMessage ?? "Unknown error message, please report.");
|
||||
}
|
||||
|
||||
UpdateCookies(response.GetCookies(), DateTime.Now.AddDays(30));
|
||||
|
||||
_logger.Debug("BakaBT authentication succeeded");
|
||||
}
|
||||
|
||||
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
|
||||
{
|
||||
return !httpResponse.Content.Contains("<a href=\"logout.php\">Logout</a>");
|
||||
return httpResponse.Content.Contains("loginForm");
|
||||
}
|
||||
|
||||
private IndexerCapabilities SetCapabilities()
|
||||
@@ -241,7 +245,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
||||
{
|
||||
var torrentInfos = new List<TorrentInfo>();
|
||||
var releaseInfos = new List<ReleaseInfo>();
|
||||
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
@@ -356,11 +360,11 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
release.DownloadVolumeFactor = row.QuerySelector("span.freeleech") != null ? 0 : 1;
|
||||
release.UploadVolumeFactor = 1;
|
||||
|
||||
torrentInfos.Add(release);
|
||||
releaseInfos.Add(release);
|
||||
}
|
||||
}
|
||||
|
||||
return torrentInfos.ToArray();
|
||||
return releaseInfos.ToArray();
|
||||
}
|
||||
|
||||
private ICollection<IndexerCategory> GetNextCategory(IElement row, ICollection<IndexerCategory> currentCategories)
|
||||
@@ -388,12 +392,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
var categoryName = categoryElement.GetAttribute("title");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(categoryName))
|
||||
{
|
||||
return categoryName;
|
||||
}
|
||||
|
||||
return null;
|
||||
return categoryName.IsNotNullOrWhiteSpace() ? categoryName : null;
|
||||
}
|
||||
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Globalization;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
@@ -50,50 +50,63 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
|
||||
|
||||
var parameters = new BroadcastheNetTorrentQuery();
|
||||
|
||||
var searchString = searchCriteria.SearchTerm != null ? searchCriteria.SearchTerm : "";
|
||||
var searchString = searchCriteria.SearchTerm ?? string.Empty;
|
||||
|
||||
var btnResults = searchCriteria.Limit.GetValueOrDefault();
|
||||
if (btnResults == 0)
|
||||
{
|
||||
btnResults = (int)Capabilities.LimitsDefault;
|
||||
btnResults = Capabilities.LimitsDefault.GetValueOrDefault(PageSize);
|
||||
}
|
||||
|
||||
var btnOffset = searchCriteria.Offset.GetValueOrDefault();
|
||||
var btnOffset = searchCriteria.Offset.GetValueOrDefault(0);
|
||||
|
||||
if (searchCriteria.TvdbId > 0)
|
||||
{
|
||||
parameters.Tvdb = string.Format("{0}", searchCriteria.TvdbId);
|
||||
parameters.Tvdb = $"{searchCriteria.TvdbId}";
|
||||
}
|
||||
|
||||
if (searchCriteria.RId > 0)
|
||||
else if (searchCriteria.RId > 0)
|
||||
{
|
||||
parameters.Tvrage = string.Format("{0}", searchCriteria.RId);
|
||||
parameters.Tvrage = $"{searchCriteria.RId}";
|
||||
}
|
||||
else if (searchString.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
parameters.Search = searchString.Replace(" ", "%");
|
||||
}
|
||||
|
||||
// If only the season/episode is searched for then change format to match expected format
|
||||
if (searchCriteria.Season > 0 && searchCriteria.Episode.IsNullOrWhiteSpace())
|
||||
{
|
||||
// Season Only
|
||||
parameters.Name = string.Format("Season {0}%", searchCriteria.Season.Value);
|
||||
// Search Season
|
||||
parameters.Category = "Season";
|
||||
parameters.Name = $"Season {searchCriteria.Season}%";
|
||||
pageableRequests.Add(GetPagedRequests(parameters, btnResults, btnOffset));
|
||||
|
||||
parameters = parameters.Clone();
|
||||
|
||||
// Search Episode
|
||||
parameters.Category = "Episode";
|
||||
parameters.Name = $"S{searchCriteria.Season:00}E%";
|
||||
pageableRequests.Add(GetPagedRequests(parameters, btnResults, btnOffset));
|
||||
}
|
||||
else if (searchCriteria.Season > 0 && Regex.IsMatch(searchCriteria.EpisodeSearchString, "(\\d{4}\\.\\d{2}\\.\\d{2})"))
|
||||
else if (DateTime.TryParseExact($"{searchCriteria.Season} {searchCriteria.Episode}", "yyyy MM/dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var showDate))
|
||||
{
|
||||
// Daily Episode
|
||||
parameters.Name = searchCriteria.EpisodeSearchString;
|
||||
parameters.Name = showDate.ToString("yyyy.MM.dd");
|
||||
parameters.Category = "Episode";
|
||||
pageableRequests.Add(GetPagedRequests(parameters, btnResults, btnOffset));
|
||||
}
|
||||
else if (searchCriteria.Season > 0 && int.Parse(searchCriteria.Episode) > 0)
|
||||
else if (searchCriteria.Season > 0 && int.TryParse(searchCriteria.Episode, out var episode) && episode > 0)
|
||||
{
|
||||
// Standard (S/E) Episode
|
||||
parameters.Name = string.Format("S{0:00}E{1:00}", searchCriteria.Season.Value, int.Parse(searchCriteria.Episode));
|
||||
parameters.Name = $"S{searchCriteria.Season:00}E{episode:00}";
|
||||
parameters.Category = "Episode";
|
||||
pageableRequests.Add(GetPagedRequests(parameters, btnResults, btnOffset));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Neither a season only search nor daily nor standard, fall back to query
|
||||
pageableRequests.Add(GetPagedRequests(parameters, btnResults, btnOffset));
|
||||
}
|
||||
|
||||
// Neither a season only search nor daily nor standard, fall back to query
|
||||
parameters.Search = searchString.Replace(" ", "%");
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(parameters, btnResults, btnOffset));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
@@ -109,17 +122,17 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
|
||||
|
||||
var parameters = new BroadcastheNetTorrentQuery();
|
||||
|
||||
var searchString = searchCriteria.SearchTerm != null ? searchCriteria.SearchTerm : "";
|
||||
var searchString = searchCriteria.SearchTerm ?? "";
|
||||
|
||||
var btnResults = searchCriteria.Limit.GetValueOrDefault();
|
||||
if (btnResults == 0)
|
||||
{
|
||||
btnResults = (int)Capabilities.LimitsDefault;
|
||||
btnResults = Capabilities.LimitsDefault.GetValueOrDefault(PageSize);
|
||||
}
|
||||
|
||||
parameters.Search = searchString.Replace(" ", "%");
|
||||
var btnOffset = searchCriteria.Offset.GetValueOrDefault(0);
|
||||
|
||||
var btnOffset = searchCriteria.Offset.GetValueOrDefault();
|
||||
parameters.Search = searchString.Replace(" ", "%");
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(parameters, btnResults, btnOffset));
|
||||
|
||||
|
||||
@@ -6,11 +6,10 @@ using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions;
|
||||
|
||||
[Obsolete("Site unavailable")]
|
||||
public class BrokenStones : GazelleBase<GazelleSettings>
|
||||
{
|
||||
public override string Name => "BrokenStones";
|
||||
public override string[] IndexerUrls => new[] { "https://brokenstones.club/" };
|
||||
public override string[] IndexerUrls => new[] { "https://brokenstones.is/" };
|
||||
public override string Description => "Broken Stones is a Private site for MacOS and iOS APPS / GAMES";
|
||||
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Cardigann
|
||||
namespace NzbDrone.Core.Indexers.Definitions.Cardigann
|
||||
{
|
||||
public class Cardigann : TorrentIndexerBase<CardigannSettings>
|
||||
{
|
||||
|
||||
@@ -15,7 +15,7 @@ using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Parser;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Cardigann
|
||||
namespace NzbDrone.Core.Indexers.Definitions.Cardigann
|
||||
{
|
||||
public class CardigannBase
|
||||
{
|
||||
@@ -825,9 +825,19 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
protected JArray JsonParseRowsSelector(JToken parsedJson, string rowSelector)
|
||||
{
|
||||
var selector = rowSelector.Split(':')[0];
|
||||
var rowsObj = parsedJson.SelectToken(selector).Value<JArray>();
|
||||
return new JArray(rowsObj.Where(t =>
|
||||
JsonParseFieldSelector(t.Value<JObject>(), rowSelector.Remove(0, selector.Length)) != null));
|
||||
|
||||
try
|
||||
{
|
||||
var rowsObj = parsedJson.SelectToken(selector).Value<JArray>();
|
||||
|
||||
return new JArray(rowsObj.Where(t => JsonParseFieldSelector(t.Value<JObject>(), rowSelector.Remove(0, selector.Length)) != null));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex, "Failed to parse JSON rows for selector \"{0}\"", rowSelector);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private string JsonParseFieldSelector(JToken parsedJson, string rowSelector)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user