mirror of
https://github.com/Radarr/Radarr.git
synced 2026-03-25 17:35:35 -04:00
Compare commits
42 Commits
import-lis
...
v4.6.3.751
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8fd267580a | ||
|
|
8974aa823b | ||
|
|
41492efd2e | ||
|
|
d008768fff | ||
|
|
cb21fe535d | ||
|
|
4cce2727e2 | ||
|
|
b1ff82da37 | ||
|
|
c5266152c5 | ||
|
|
783878be1e | ||
|
|
0cbfb4ca65 | ||
|
|
c22c9400c2 | ||
|
|
0288c4b704 | ||
|
|
e4429d2919 | ||
|
|
7052a7a5ec | ||
|
|
b38912851b | ||
|
|
1354c2c337 | ||
|
|
e259235df6 | ||
|
|
0cc1fe8308 | ||
|
|
f4fe18a440 | ||
|
|
eeed935e3a | ||
|
|
1b3701371a | ||
|
|
d56f3ec2e7 | ||
|
|
e7e3aac971 | ||
|
|
d2cb36c88a | ||
|
|
2fe28cb1dc | ||
|
|
5d65b4cae4 | ||
|
|
b0f56e2840 | ||
|
|
5593837482 | ||
|
|
8231290c7b | ||
|
|
0c1b88c60a | ||
|
|
0b8478e4a1 | ||
|
|
69e09c8687 | ||
|
|
3f2ea49023 | ||
|
|
32f09633e9 | ||
|
|
3542b263c7 | ||
|
|
d5cc84d8c8 | ||
|
|
c0790060fb | ||
|
|
5ec7e86488 | ||
|
|
b8abafd72f | ||
|
|
a471f1b44f | ||
|
|
7fe34be789 | ||
|
|
471a34eabf |
@@ -9,7 +9,7 @@ variables:
|
||||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '4.6.0'
|
||||
majorVersion: '4.6.3'
|
||||
minorVersion: $[counter('minorVersion', 2000)]
|
||||
radarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
@@ -156,16 +157,16 @@ class Blocklist extends Component {
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadBlocklist')}
|
||||
</div>
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !error && !items.length &&
|
||||
<div>
|
||||
{translate('NoHistory')}
|
||||
</div>
|
||||
<Alert kind={kinds.INFO}>
|
||||
{translate('NoHistoryBlocklist')}
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
@@ -209,7 +210,7 @@ class Blocklist extends Component {
|
||||
isOpen={isConfirmRemoveModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('RemoveSelected')}
|
||||
message={translate('AreYouSureYouWantToRemoveTheSelectedItemsFromBlocklist')}
|
||||
message={translate('RemoveSelectedItemBlocklistMessageText')}
|
||||
confirmLabel={translate('RemoveSelected')}
|
||||
onConfirm={this.onRemoveSelectedConfirmed}
|
||||
onCancel={this.onConfirmRemoveModalClose}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
@@ -11,7 +12,7 @@ import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import TablePager from 'Components/Table/TablePager';
|
||||
import { align, icons } from 'Helpers/Props';
|
||||
import { align, icons, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import HistoryRowConnector from './HistoryRowConnector';
|
||||
|
||||
@@ -83,9 +84,9 @@ class History extends Component {
|
||||
|
||||
{
|
||||
!isFetchingAny && hasError &&
|
||||
<div>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadHistory')}
|
||||
</div>
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
@@ -93,9 +94,9 @@ class History extends Component {
|
||||
// wait for the episodes to populate because they are never coming.
|
||||
|
||||
isPopulated && !hasError && !items.length &&
|
||||
<div>
|
||||
<Alert kind={kinds.INFO}>
|
||||
{translate('NoHistory')}
|
||||
</div>
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
@@ -12,7 +13,7 @@ import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import TablePager from 'Components/Table/TablePager';
|
||||
import { align, icons } from 'Helpers/Props';
|
||||
import { align, icons, kinds } from 'Helpers/Props';
|
||||
import getRemovedItems from 'Utilities/Object/getRemovedItems';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import translate from 'Utilities/String/translate';
|
||||
@@ -231,17 +232,17 @@ class Queue extends Component {
|
||||
|
||||
{
|
||||
!isRefreshing && hasError ?
|
||||
<div>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('FailedToLoadQueue')}
|
||||
</div> :
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
isAllPopulated && !hasError && !items.length ?
|
||||
<div>
|
||||
<Alert kind={kinds.INFO}>
|
||||
{translate('QueueIsEmpty')}
|
||||
</div> :
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ class RemoveQueueItemsModal extends Component {
|
||||
|
||||
<ModalBody>
|
||||
<div className={styles.message}>
|
||||
{selectedCount > 1 ? translate('AreYouSureYouWantToRemoveSelectedItemsFromQueue', selectedCount) : translate('AreYouSureYouWantToRemoveSelectedItemFromQueue')}
|
||||
{selectedCount > 1 ? translate('RemoveSelectedItemsQueueMessageText', selectedCount) : translate('RemoveSelectedItemQueueMessageText')}
|
||||
</div>
|
||||
|
||||
{
|
||||
@@ -133,7 +133,7 @@ class RemoveQueueItemsModal extends Component {
|
||||
kind={kinds.DANGER}
|
||||
onPress={this.onRemoveConfirmed}
|
||||
>
|
||||
Remove
|
||||
{translate('Remove')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { reduce } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
@@ -105,9 +107,9 @@ class ImportMovie extends Component {
|
||||
|
||||
{
|
||||
!rootFoldersFetching && !!rootFoldersError ?
|
||||
<div>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadRootFolders')}
|
||||
</div> :
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
|
||||
@@ -116,9 +118,9 @@ class ImportMovie extends Component {
|
||||
!rootFoldersFetching &&
|
||||
rootFoldersPopulated &&
|
||||
!unmappedFolders.length ?
|
||||
<div>
|
||||
<Alert kind={kinds.INFO}>
|
||||
{translate('AllMoviesInPathHaveBeenImported', [path])}
|
||||
</div> :
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
|
||||
|
||||
@@ -92,9 +92,9 @@ class ImportMovieSelectFolder extends Component {
|
||||
|
||||
{
|
||||
!isFetching && error ?
|
||||
<div>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadRootFolders')}
|
||||
</div> :
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AgendaConnector from './Agenda/AgendaConnector';
|
||||
import * as calendarViews from './calendarViews';
|
||||
@@ -31,9 +33,9 @@ class Calendar extends Component {
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadTheCalendar')}
|
||||
</div>
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
@@ -9,7 +10,7 @@ import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import { align, icons, sortDirections } from 'Helpers/Props';
|
||||
import { align, icons, kinds, sortDirections } from 'Helpers/Props';
|
||||
import styles from 'Movie/Index/MovieIndex.css';
|
||||
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
||||
import translate from 'Utilities/String/translate';
|
||||
@@ -313,9 +314,9 @@ class Collection extends Component {
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadCollections')}
|
||||
</div>
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -63,6 +63,7 @@ function ProviderFieldFormGroup(props) {
|
||||
name,
|
||||
label,
|
||||
helpText,
|
||||
helpTextWarning,
|
||||
helpLink,
|
||||
placeholder,
|
||||
value,
|
||||
@@ -96,6 +97,7 @@ function ProviderFieldFormGroup(props) {
|
||||
name={name}
|
||||
label={label}
|
||||
helpText={helpText}
|
||||
helpTextWarning={helpTextWarning}
|
||||
helpLink={helpLink}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
@@ -122,6 +124,7 @@ ProviderFieldFormGroup.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
helpText: PropTypes.string,
|
||||
helpTextWarning: PropTypes.string,
|
||||
helpLink: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
value: PropTypes.any,
|
||||
|
||||
@@ -19,6 +19,8 @@ function createCleanMovieSelector() {
|
||||
year,
|
||||
images,
|
||||
alternateTitles = [],
|
||||
tmdbId,
|
||||
imdbId,
|
||||
tags = []
|
||||
} = movie;
|
||||
|
||||
@@ -29,6 +31,8 @@ function createCleanMovieSelector() {
|
||||
year,
|
||||
images,
|
||||
alternateTitles,
|
||||
tmdbId,
|
||||
imdbId,
|
||||
firstCharacter: title.charAt(0).toLowerCase(),
|
||||
tags: tags.reduce((acc, id) => {
|
||||
const matchingTag = allTags.find((tag) => tag.id === id);
|
||||
|
||||
@@ -12,6 +12,8 @@ function MovieSearchResult(props) {
|
||||
year,
|
||||
images,
|
||||
alternateTitles,
|
||||
tmdbId,
|
||||
imdbId,
|
||||
tags
|
||||
} = props;
|
||||
|
||||
@@ -47,6 +49,22 @@ function MovieSearchResult(props) {
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
match.key === 'tmdbId' && tmdbId ?
|
||||
<div className={styles.alternateTitle}>
|
||||
TmdbId: {tmdbId}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
match.key === 'imdbId' && imdbId ?
|
||||
<div className={styles.alternateTitle}>
|
||||
ImdbId: {imdbId}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
tag ?
|
||||
<div className={styles.tagContainer}>
|
||||
@@ -69,6 +87,8 @@ MovieSearchResult.propTypes = {
|
||||
year: PropTypes.number.isRequired,
|
||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
tmdbId: PropTypes.number,
|
||||
imdbId: PropTypes.string,
|
||||
tags: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
match: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
@@ -9,6 +9,8 @@ const fuseOptions = {
|
||||
keys: [
|
||||
'title',
|
||||
'alternateTitles.title',
|
||||
'tmdbId',
|
||||
'imdbId',
|
||||
'tags.label'
|
||||
]
|
||||
};
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
|
||||
function PageSectionContent(props) {
|
||||
const {
|
||||
@@ -17,7 +19,7 @@ function PageSectionContent(props) {
|
||||
);
|
||||
} else if (!isFetching && !!error) {
|
||||
return (
|
||||
<div>{errorMessage}</div>
|
||||
<Alert kind={kinds.DANGER}>{errorMessage}</Alert>
|
||||
);
|
||||
} else if (isPopulated && !error) {
|
||||
return (
|
||||
|
||||
@@ -16,6 +16,46 @@
|
||||
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
|
||||
color: var(--white);
|
||||
transition: width 0.6s ease;
|
||||
|
||||
&.default {
|
||||
background-color: var(--darkGray);
|
||||
}
|
||||
|
||||
&.primary {
|
||||
background-color: var(--primaryColor);
|
||||
}
|
||||
|
||||
&.danger {
|
||||
background-color: var(--dangerColor);
|
||||
|
||||
&:global(.colorImpaired) {
|
||||
background: repeating-linear-gradient(90deg, color(#f05050 shade(5%)), color(#f05050 shade(5%)) 5px, color(#f05050 shade(15%)) 5px, color(#f05050 shade(15%)) 10px);
|
||||
}
|
||||
}
|
||||
|
||||
&.success {
|
||||
background-color: var(--successColor);
|
||||
}
|
||||
|
||||
&.purple {
|
||||
background-color: var(--purple);
|
||||
}
|
||||
|
||||
&.warning {
|
||||
background-color: var(--warningColor);
|
||||
|
||||
&:global(.colorImpaired) {
|
||||
background: repeating-linear-gradient(45deg, #ffa500, #ffa500 5px, color(#ffa500 tint(15%)) 5px, color(#ffa500 tint(15%)) 10px);
|
||||
}
|
||||
}
|
||||
|
||||
&.info {
|
||||
background-color: var(--infoColor);
|
||||
}
|
||||
|
||||
&.queue {
|
||||
background-color: var(--queueColor);
|
||||
}
|
||||
}
|
||||
|
||||
.frontTextContainer {
|
||||
@@ -45,46 +85,6 @@
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.default {
|
||||
background-color: var(--darkGray);
|
||||
}
|
||||
|
||||
.primary {
|
||||
background-color: var(--primaryColor);
|
||||
}
|
||||
|
||||
.danger {
|
||||
background-color: var(--dangerColor);
|
||||
|
||||
&:global(.colorImpaired) {
|
||||
background: repeating-linear-gradient(90deg, color(#f05050 shade(5%)), color(#f05050 shade(5%)) 5px, color(#f05050 shade(15%)) 5px, color(#f05050 shade(15%)) 10px);
|
||||
}
|
||||
}
|
||||
|
||||
.success {
|
||||
background-color: var(--successColor);
|
||||
}
|
||||
|
||||
.purple {
|
||||
background-color: var(--purple);
|
||||
}
|
||||
|
||||
.warning {
|
||||
background-color: var(--warningColor);
|
||||
|
||||
&:global(.colorImpaired) {
|
||||
background: repeating-linear-gradient(45deg, #ffa500, #ffa500 5px, color(#ffa500 tint(15%)) 5px, color(#ffa500 tint(15%)) 10px);
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
background-color: var(--infoColor);
|
||||
}
|
||||
|
||||
.queue {
|
||||
background-color: var(--queueColor);
|
||||
}
|
||||
|
||||
.small {
|
||||
height: $progressBarSmallHeight;
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ function ProgressBar(props) {
|
||||
{
|
||||
showText && width ?
|
||||
<div
|
||||
className={styles.backTextContainer}
|
||||
className={classNames(styles.backTextContainer, styles[kind])}
|
||||
style={{ width: actualWidth }}
|
||||
>
|
||||
<div className={styles.backText}>
|
||||
@@ -67,7 +67,7 @@ function ProgressBar(props) {
|
||||
{
|
||||
showText ?
|
||||
<div
|
||||
className={styles.frontTextContainer}
|
||||
className={classNames(styles.frontTextContainer, styles[kind])}
|
||||
style={{ width: progressPercent }}
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
@@ -10,7 +11,7 @@ import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import { align, icons, sortDirections } from 'Helpers/Props';
|
||||
import { align, icons, kinds, sortDirections } from 'Helpers/Props';
|
||||
import styles from 'Movie/Index/MovieIndex.css';
|
||||
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
||||
import translate from 'Utilities/String/translate';
|
||||
@@ -369,9 +370,9 @@ class DiscoverMovie extends Component {
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadMovies')}
|
||||
</div>
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useCallback, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { LanguageSettingsAppState } from 'App/State/SettingsAppState';
|
||||
import Alert from 'Components/Alert';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
@@ -86,7 +87,9 @@ function SelectLanguageModalContent(props: SelectLanguageModalContentProps) {
|
||||
{isFetching ? <LoadingIndicator /> : null}
|
||||
|
||||
{!isFetching && error ? (
|
||||
<div>{translate('UnableToLoadLanguages')}</div>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadLanguages')}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{isPopulated && !error ? (
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { Error } from 'App/State/AppSectionState';
|
||||
import AppState from 'App/State/AppState';
|
||||
import Alert from 'Components/Alert';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
@@ -130,7 +131,9 @@ function SelectQualityModalContent(props: SelectQualityModalContentProps) {
|
||||
{isFetching && <LoadingIndicator />}
|
||||
|
||||
{!isFetching && error ? (
|
||||
<div>{translate('UnableToLoadQualities')}</div>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadQualities')}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{isPopulated && !error ? (
|
||||
|
||||
@@ -10,6 +10,7 @@ import { SelectProvider } from 'App/SelectContext';
|
||||
import ClientSideCollectionAppState from 'App/State/ClientSideCollectionAppState';
|
||||
import MoviesAppState, { MovieIndexAppState } from 'App/State/MoviesAppState';
|
||||
import { RSS_SYNC } from 'Commands/commandNames';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
@@ -20,7 +21,7 @@ import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import withScrollPosition from 'Components/withScrollPosition';
|
||||
import { align, icons } from 'Helpers/Props';
|
||||
import { align, icons, kinds } from 'Helpers/Props';
|
||||
import SortDirection from 'Helpers/Props/SortDirection';
|
||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||
import NoMovie from 'Movie/NoMovie';
|
||||
@@ -337,7 +338,9 @@ const MovieIndex = withScrollPosition((props: MovieIndexProps) => {
|
||||
{isFetching && !isPopulated ? <LoadingIndicator /> : null}
|
||||
|
||||
{!isFetching && !!error ? (
|
||||
<div>{translate('UnableToLoadMovies')}</div>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadMovies')}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{isLoaded ? (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
@@ -109,9 +110,9 @@ class FileEditModalContent extends Component {
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadQualities')}
|
||||
</div>
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
@@ -92,9 +93,9 @@ class SelectQualityModalContent extends Component {
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadQualities')}
|
||||
</div>
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import RootFolderRowConnector from './RootFolderRowConnector';
|
||||
|
||||
@@ -44,9 +46,9 @@ function RootFolders(props) {
|
||||
|
||||
if (!isFetching && !!error) {
|
||||
return (
|
||||
<div>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadRootFolders')}
|
||||
</div>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -152,13 +152,7 @@ class CustomFormat extends Component {
|
||||
isOpen={this.state.isDeleteCustomFormatModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('DeleteCustomFormat')}
|
||||
message={
|
||||
<div>
|
||||
<div>
|
||||
{translate('AreYouSureYouWantToDeleteFormat', [name])}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
message={translate('DeleteCustomFormatMessageText', [name])}
|
||||
confirmLabel={translate('Delete')}
|
||||
isSpinning={isDeleting}
|
||||
onConfirm={this.onConfirmDeleteCustomFormat}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import Button from 'Components/Link/Button';
|
||||
import ClipboardButton from 'Components/Link/ClipboardButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
@@ -41,9 +42,9 @@ class ExportCustomFormatModalContent extends Component {
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadCustomFormats')}
|
||||
</div>
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
@@ -11,7 +12,7 @@ import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, sizes } from 'Helpers/Props';
|
||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './ImportCustomFormatModalContent.css';
|
||||
|
||||
@@ -95,9 +96,9 @@ class ImportCustomFormatModalContent extends Component {
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadCustomFormats')}
|
||||
</div>
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -78,7 +78,7 @@ class Specification extends Component {
|
||||
|
||||
<IconButton
|
||||
className={styles.cloneButton}
|
||||
title={translate('CloneFormatTag')}
|
||||
title={translate('CloneCondition')}
|
||||
name={icons.CLONE}
|
||||
onPress={this.onCloneSpecificationPress}
|
||||
/>
|
||||
@@ -114,14 +114,8 @@ class Specification extends Component {
|
||||
<ConfirmModal
|
||||
isOpen={this.state.isDeleteSpecificationModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('DeleteCustomFormat')}
|
||||
message={
|
||||
<div>
|
||||
<div>
|
||||
{translate('AreYouSureYouWantToDeleteFormat', [name])}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
title={translate('DeleteCondition')}
|
||||
message={translate('DeleteConditionMessageText', [name])}
|
||||
confirmLabel={translate('Delete')}
|
||||
onConfirm={this.onConfirmDeleteSpecification}
|
||||
onCancel={this.onDeleteSpecificationModalClose}
|
||||
|
||||
@@ -29,9 +29,9 @@ function DownloadClientOptions(props) {
|
||||
|
||||
{
|
||||
!isFetching && error &&
|
||||
<div>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadDownloadClientOptions')}
|
||||
</div>
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -88,8 +88,8 @@ class RemotePathMapping extends Component {
|
||||
<ConfirmModal
|
||||
isOpen={this.state.isDeleteRemotePathMappingModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('DeleteDelayProfile')}
|
||||
message={translate('AreYouSureYouWantToDeleteThisRemotePathMapping')}
|
||||
title={translate('DeleteRemotePathMapping')}
|
||||
message={translate('DeleteRemotePathMappingMessageText')}
|
||||
confirmLabel={translate('Delete')}
|
||||
onConfirm={this.onConfirmDeleteRemotePathMapping}
|
||||
onCancel={this.onDeleteRemotePathMappingModalClose}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import Form from 'Components/Form/Form';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
@@ -123,9 +124,9 @@ class GeneralSettings extends Component {
|
||||
|
||||
{
|
||||
!isFetching && error &&
|
||||
<div>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadGeneralSettings')}
|
||||
</div>
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -168,7 +168,7 @@ class SecuritySettings extends Component {
|
||||
isOpen={this.state.isConfirmApiKeyResetModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('ResetAPIKey')}
|
||||
message={translate('AreYouSureYouWantToResetYourAPIKey')}
|
||||
message={translate('ResetAPIKeyMessageText')}
|
||||
confirmLabel={translate('Reset')}
|
||||
onConfirm={this.onConfirmResetApiKey}
|
||||
onCancel={this.onCloseResetApiKeyModal}
|
||||
|
||||
@@ -89,7 +89,7 @@ class ImportListExclusion extends Component {
|
||||
isOpen={this.state.isDeleteImportExclusionModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('DeleteImportListExclusion')}
|
||||
message={translate('AreYouSureYouWantToDeleteThisImportListExclusion')}
|
||||
message={translate('DeleteImportListExclusionMessageText')}
|
||||
confirmLabel={translate('Delete')}
|
||||
onConfirm={this.onConfirmDeleteImportExclusion}
|
||||
onCancel={this.onDeleteImportExclusionModalClose}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
function ImportListOptions(props) {
|
||||
@@ -37,9 +38,9 @@ function ImportListOptions(props) {
|
||||
|
||||
{
|
||||
!isFetching && error &&
|
||||
<div>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadListOptions')}
|
||||
</div>
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
function IndexerOptions(props) {
|
||||
@@ -28,9 +29,9 @@ function IndexerOptions(props) {
|
||||
|
||||
{
|
||||
!isFetching && error &&
|
||||
<div>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadIndexerOptions')}
|
||||
</div>
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
@@ -8,7 +9,7 @@ import FormLabel from 'Components/Form/FormLabel';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import { inputTypes, sizes } from 'Helpers/Props';
|
||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||
import RootFoldersConnector from 'RootFolder/RootFoldersConnector';
|
||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
@@ -72,9 +73,9 @@ class MediaManagement extends Component {
|
||||
{
|
||||
!isFetching && error ?
|
||||
<FieldSet legend={translate('NamingSettings')}>
|
||||
<div>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadMediaManagementSettings')}
|
||||
</div>
|
||||
</Alert>
|
||||
</FieldSet> : null
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
@@ -7,7 +8,7 @@ import FormInputButton from 'Components/Form/FormInputButton';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import { inputTypes, sizes } from 'Helpers/Props';
|
||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import NamingModal from './NamingModal';
|
||||
import styles from './Naming.css';
|
||||
@@ -110,9 +111,9 @@ class Naming extends Component {
|
||||
|
||||
{
|
||||
!isFetching && error &&
|
||||
<div>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadNamingSettings')}
|
||||
</div>
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -6,8 +6,9 @@ import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import Alert from '../../../Components/Alert';
|
||||
|
||||
// Note: Do Not Translate Certification Countries
|
||||
|
||||
@@ -43,9 +44,9 @@ function MetadataOptions(props) {
|
||||
|
||||
{
|
||||
!isFetching && error &&
|
||||
<div>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadIndexerOptions')}
|
||||
</div>
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -141,7 +141,7 @@ class DelayProfile extends Component {
|
||||
isOpen={this.state.isDeleteDelayProfileModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('DeleteDelayProfile')}
|
||||
message={translate('AreYouSureYouWantToDeleteThisDelayProfile')}
|
||||
message={translate('DeleteDelayProfileMessageText')}
|
||||
confirmLabel={translate('Delete')}
|
||||
onConfirm={this.onConfirmDeleteDelayProfile}
|
||||
onCancel={this.onDeleteDelayProfileModalClose}
|
||||
|
||||
@@ -60,17 +60,19 @@ class ResetQualityDefinitionsModalContent extends Component {
|
||||
|
||||
<ModalBody>
|
||||
<div className={styles.messageContainer}>
|
||||
{translate('AreYouSureYouWantToResetQualityDefinitions')}
|
||||
{translate('ResetQualityDefinitionsMessageText')}
|
||||
</div>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ResetTitles')}</FormLabel>
|
||||
<FormLabel>
|
||||
{translate('ResetTitles')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="resetDefinitionTitles"
|
||||
value={resetDefinitionTitles}
|
||||
helpText={translate('ResetTitlesHelpText')}
|
||||
helpText={translate('ResetDefinitionTitlesHelpText')}
|
||||
onChange={this.onResetDefinitionTitlesChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
@@ -8,7 +9,7 @@ import FormLabel from 'Components/Form/FormLabel';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||
import themes from 'Styles/Themes';
|
||||
import titleCase from 'Utilities/String/titleCase';
|
||||
@@ -87,9 +88,9 @@ class UISettings extends Component {
|
||||
|
||||
{
|
||||
!isFetching && error &&
|
||||
<div>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadUISettings')}
|
||||
</div>
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -234,6 +234,18 @@ export const sortPredicates = {
|
||||
}
|
||||
|
||||
return padNumber(result.toString(), 2) + qualityName;
|
||||
},
|
||||
|
||||
inCinemas: function(item) {
|
||||
return item.inCinemas || '';
|
||||
},
|
||||
|
||||
physicalRelease: function(item) {
|
||||
return item.physicalRelease || '';
|
||||
},
|
||||
|
||||
digitalRelease: function(item) {
|
||||
return item.digitalRelease || '';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
@@ -8,7 +9,7 @@ import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import BackupRow from './BackupRow';
|
||||
import RestoreBackupModalConnector from './RestoreBackupModalConnector';
|
||||
@@ -107,16 +108,16 @@ class Backups extends Component {
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadBackups')}
|
||||
</div>
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
noBackups &&
|
||||
<div>
|
||||
<Alert kind={kinds.INFO}>
|
||||
{translate('NoBackupsAreAvailable')}
|
||||
</div>
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -104,7 +104,7 @@
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
|
||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||
<PackageReference Include="NunitXml.TestLogger" Version="3.0.117" />
|
||||
<PackageReference Include="NunitXml.TestLogger" Version="3.0.131" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TestProject)'=='true' and '$(TargetFramework)'=='net6.0'">
|
||||
|
||||
@@ -27,19 +27,47 @@ namespace NzbDrone.Api.Test.ClientSchemaTests
|
||||
|
||||
var schema = SchemaBuilder.ToSchema(model);
|
||||
|
||||
schema.Should().Contain(c => c.Order == 1 && c.Name == "lastName" && c.Label == "Last Name" && c.HelpText == "Your Last Name" && (string)c.Value == "Poop");
|
||||
schema.Should().Contain(c => c.Order == 0 && c.Name == "firstName" && c.Label == "First Name" && c.HelpText == "Your First Name" && (string)c.Value == "Bob");
|
||||
schema.Should().Contain(c => c.Order == 1 && c.Name == "lastName" && c.Label == "Last Name" && c.HelpText == "Your Last Name" && c.HelpTextWarning == "Mandatory Last Name" && (string)c.Value == "Poop");
|
||||
schema.Should().Contain(c => c.Order == 0 && c.Name == "firstName" && c.Label == "First Name" && c.HelpText == "Your First Name" && c.HelpTextWarning == "Mandatory First Name" && (string)c.Value == "Bob");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void schema_should_have_nested_fields()
|
||||
{
|
||||
var model = new NestedTestModel
|
||||
{
|
||||
Name =
|
||||
{
|
||||
FirstName = "Bob",
|
||||
LastName = "Poop"
|
||||
}
|
||||
};
|
||||
|
||||
var schema = SchemaBuilder.ToSchema(model);
|
||||
|
||||
schema.Should().Contain(c => c.Order == 0 && c.Name == "name.firstName" && c.Label == "First Name" && c.HelpText == "Your First Name" && c.HelpTextWarning == "Mandatory First Name" && (string)c.Value == "Bob");
|
||||
schema.Should().Contain(c => c.Order == 1 && c.Name == "name.lastName" && c.Label == "Last Name" && c.HelpText == "Your Last Name" && c.HelpTextWarning == "Mandatory Last Name" && (string)c.Value == "Poop");
|
||||
schema.Should().Contain(c => c.Order == 2 && c.Name == "quote" && c.Label == "Quote" && c.HelpText == "Your Favorite Quote");
|
||||
}
|
||||
}
|
||||
|
||||
public class TestModel
|
||||
{
|
||||
[FieldDefinition(0, Label = "First Name", HelpText = "Your First Name")]
|
||||
[FieldDefinition(0, Label = "First Name", HelpText = "Your First Name", HelpTextWarning = "Mandatory First Name")]
|
||||
public string FirstName { get; set; }
|
||||
|
||||
[FieldDefinition(1, Label = "Last Name", HelpText = "Your Last Name")]
|
||||
[FieldDefinition(1, Label = "Last Name", HelpText = "Your Last Name", HelpTextWarning = "Mandatory Last Name")]
|
||||
public string LastName { get; set; }
|
||||
|
||||
public string Other { get; set; }
|
||||
}
|
||||
|
||||
public class NestedTestModel
|
||||
{
|
||||
[FieldDefinition(0)]
|
||||
public TestModel Name { get; set; } = new TestModel();
|
||||
|
||||
[FieldDefinition(1, Label = "Quote", HelpText = "Your Favorite Quote")]
|
||||
public string Quote { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,9 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
||||
// Indexer Urls
|
||||
[TestCase(@"https://iptorrents.com/torrents/rss?u=mySecret;tp=mySecret;l5;download")]
|
||||
[TestCase(@"http://rss.torrentleech.org/mySecret")]
|
||||
[TestCase(@"http://rss.torrentleech.org/rss/download/12345/01233210/filename.torrent")]
|
||||
[TestCase(@"https://rss24h.torrentleech.org/mySecret")]
|
||||
[TestCase(@"http://rss.torrentleech.org/rss/download/12345/01233210/file.name-RLSGRP.torrent")]
|
||||
[TestCase(@"https://www.torrentleech.org/rss/download/12345/01233210/file.name-RLSGRP.torrent")]
|
||||
[TestCase(@"http://www.bitmetv.org/rss.php?uid=mySecret&passkey=mySecret")]
|
||||
[TestCase(@"https://rss.omgwtfnzbs.org/rss-search.php?catid=19,20&user=sonarr&api=mySecret&eng=1")]
|
||||
[TestCase(@"https://dognzb.cr/fetch/2b51db35e1912ffc138825a12b9933d2/2b51db35e1910123321025a12b9933d2")]
|
||||
@@ -44,6 +46,7 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
||||
// Deluge
|
||||
[TestCase(@",{""download_location"": ""C:\Users\\mySecret mySecret\\Downloads""}")]
|
||||
[TestCase(@",{""download_location"": ""/home/mySecret/Downloads""}")]
|
||||
[TestCase(@",{""download_location"": ""/Users/mySecret/Downloads""}")]
|
||||
[TestCase(@"auth.login(""mySecret"")")]
|
||||
|
||||
// Download Station
|
||||
@@ -59,8 +62,11 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
||||
|
||||
// Internal
|
||||
[TestCase(@"OutputPath=/home/mySecret/Downloads")]
|
||||
[TestCase(@"OutputPath=/Users/mySecret/Downloads")]
|
||||
[TestCase("Hardlinking episode file: /home/mySecret/Downloads to /media/abc.mkv")]
|
||||
[TestCase("Hardlinking episode file: /Users/mySecret/Downloads to /media/abc.mkv")]
|
||||
[TestCase("Hardlink '/home/mySecret/Downloads/abs.mkv' to '/media/abc.mkv' failed.")]
|
||||
[TestCase("Hardlink '/Users/mySecret/Downloads/abs.mkv' to '/media/abc.mkv' failed.")]
|
||||
[TestCase("https://notifiarr.com/notifier.php: api=1234530f-422f-4aac-b6b3-01233210aaaa&radarr_health_issue_message=Download")]
|
||||
[TestCase("/readarr/signalr/messages/negotiate?access_token=1234530f422f4aacb6b301233210aaaa&negotiateVersion=1")]
|
||||
[TestCase(@"[Info] MigrationController: *** Migrating Database=radarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;Enlist=False ***")]
|
||||
|
||||
@@ -60,8 +60,7 @@ namespace NzbDrone.Common.Http
|
||||
|
||||
if (request.AllowAutoRedirect && response.HasHttpRedirect)
|
||||
{
|
||||
var autoRedirectChain = new List<string>();
|
||||
autoRedirectChain.Add(request.Url.ToString());
|
||||
var autoRedirectChain = new List<string> { request.Url.ToString() };
|
||||
|
||||
do
|
||||
{
|
||||
@@ -75,6 +74,14 @@ namespace NzbDrone.Common.Http
|
||||
throw new WebException($"Too many automatic redirections were attempted for {autoRedirectChain.Join(" -> ")}", WebExceptionStatus.ProtocolError);
|
||||
}
|
||||
|
||||
// 302 or 303 should default to GET on redirect even if POST on original
|
||||
if (RequestRequiresForceGet(response.StatusCode, response.Request.Method))
|
||||
{
|
||||
request.Method = HttpMethod.Get;
|
||||
request.ContentData = null;
|
||||
request.ContentSummary = null;
|
||||
}
|
||||
|
||||
response = ExecuteRequest(request, cookieContainer);
|
||||
}
|
||||
while (response.HasHttpRedirect);
|
||||
@@ -105,6 +112,16 @@ namespace NzbDrone.Common.Http
|
||||
return response;
|
||||
}
|
||||
|
||||
private static bool RequestRequiresForceGet(HttpStatusCode statusCode, HttpMethod requestMethod)
|
||||
{
|
||||
return statusCode switch
|
||||
{
|
||||
HttpStatusCode.Moved or HttpStatusCode.Found or HttpStatusCode.MultipleChoices => requestMethod == HttpMethod.Post,
|
||||
HttpStatusCode.SeeOther => requestMethod != HttpMethod.Get && requestMethod != HttpMethod.Head,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
private HttpResponse ExecuteRequest(HttpRequest request, CookieContainer cookieContainer)
|
||||
{
|
||||
foreach (var interceptor in _requestInterceptors)
|
||||
|
||||
@@ -50,6 +50,8 @@ namespace NzbDrone.Common.Http
|
||||
|
||||
public bool HasHttpError => (int)StatusCode >= 400;
|
||||
|
||||
public bool HasHttpServerError => (int)StatusCode >= 500;
|
||||
|
||||
public bool HasHttpRedirect => StatusCode == HttpStatusCode.Moved ||
|
||||
StatusCode == HttpStatusCode.MovedPermanently ||
|
||||
StatusCode == HttpStatusCode.Found ||
|
||||
|
||||
@@ -7,55 +7,55 @@ namespace NzbDrone.Common.Instrumentation
|
||||
{
|
||||
public class CleanseLogMessage
|
||||
{
|
||||
private static readonly Regex[] CleansingRules = new[]
|
||||
{
|
||||
// Url
|
||||
new Regex(@"(?<=\?|&|: )(apikey|(?:access[-_]?)?token|passkey|auth|authkey|user|uid|api|[a-z_]*apikey|account|passwd)=(?<secret>[^&=]+?)(?=[ ""&=]|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"(?<=\?|&)[^=]*?(username|password)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"torrentleech\.org/(?!rss)(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"torrentleech\.org/rss/download/[0-9]+/(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"iptorrents\.com/[/a-z0-9?&;]*?(?:[?&;](u|tp)=(?<secret>[^&=;]+?))+(?= |;|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"/fetch/[a-z0-9]{32}/(?<secret>[a-z0-9]{32})", RegexOptions.Compiled),
|
||||
new Regex(@"getnzb.*?(?<=\?|&)(r)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"\b(\w*)?(_?(?<!use|get_)token|username|passwo?rd)=(?<secret>[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
private static readonly Regex[] CleansingRules =
|
||||
{
|
||||
// Url
|
||||
new (@"(?<=\?|&|: )(apikey|(?:access[-_]?)?token|passkey|auth|authkey|user|uid|api|[a-z_]*apikey|account|passwd)=(?<secret>[^&=]+?)(?=[ ""&=]|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new (@"(?<=\?|&)[^=]*?(username|password)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new (@"rss(24h)?\.torrentleech\.org/(?!rss)(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new (@"torrentleech\.org/rss/download/[0-9]+/(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new (@"iptorrents\.com/[/a-z0-9?&;]*?(?:[?&;](u|tp)=(?<secret>[^&=;]+?))+(?= |;|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new (@"/fetch/[a-z0-9]{32}/(?<secret>[a-z0-9]{32})", RegexOptions.Compiled),
|
||||
new (@"getnzb.*?(?<=\?|&)(r)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new (@"\b(\w*)?(_?(?<!use|get_)token|username|passwo?rd)=(?<secret>[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
// Trackers Announce Keys; Designed for Qbit Json; should work for all in theory
|
||||
new Regex(@"announce(\.php)?(/|%2f|%3fpasskey%3d)(?<secret>[a-z0-9]{16,})|(?<secret>[a-z0-9]{16,})(/|%2f)announce"),
|
||||
// Trackers Announce Keys; Designed for Qbit Json; should work for all in theory
|
||||
new (@"announce(\.php)?(/|%2f|%3fpasskey%3d)(?<secret>[a-z0-9]{16,})|(?<secret>[a-z0-9]{16,})(/|%2f)announce"),
|
||||
|
||||
// Path
|
||||
new Regex(@"C:\\Users\\(?<secret>[^\""]+?)(\\|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"/home/(?<secret>[^/""]+?)(/|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
// Path
|
||||
new (@"C:\\Users\\(?<secret>[^\""]+?)(\\|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new (@"/(home|Users)/(?<secret>[^/""]+?)(/|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
// NzbGet
|
||||
new Regex(@"""Name""\s*:\s*""[^""]*(username|password)""\s*,\s*""Value""\s*:\s*""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
// NzbGet
|
||||
new (@"""Name""\s*:\s*""[^""]*(username|password)""\s*,\s*""Value""\s*:\s*""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
// Sabnzbd
|
||||
new Regex(@"""[^""]*(username|password|api_?key|nzb_key)""\s*:\s*""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"""email_(account|to|from|pwd)""\s*:\s*""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
// Sabnzbd
|
||||
new (@"""[^""]*(username|password|api_?key|nzb_key)""\s*:\s*""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new (@"""email_(account|to|from|pwd)""\s*:\s*""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
// uTorrent
|
||||
new Regex(@"\[""[a-z._]*(username|password)"",\d,""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"\[""(boss_key|boss_key_salt|proxy\.proxy)"",\d,""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
// uTorrent
|
||||
new (@"\[""[a-z._]*(username|password)"",\d,""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new (@"\[""(boss_key|boss_key_salt|proxy\.proxy)"",\d,""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
// Deluge
|
||||
new Regex(@"auth.login\(""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
// Deluge
|
||||
new (@"auth.login\(""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
// BroadcastheNet
|
||||
new Regex(@"""?method""?\s*:\s*""(getTorrents)"",\s*""?params""?\s*:\s*\[\s*""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"getTorrents\(""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"(?<=\?|&)(authkey|torrent_pass)=(?<secret>[^&=]+?)(?=""|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
// BroadcastheNet
|
||||
new (@"""?method""?\s*:\s*""(getTorrents)"",\s*""?params""?\s*:\s*\[\s*""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new (@"getTorrents\(""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new (@"(?<=\?|&)(authkey|torrent_pass)=(?<secret>[^&=]+?)(?=""|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
// Plex
|
||||
new Regex(@"(?<=\?|&)(X-Plex-Client-Identifier|X-Plex-Token)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
// Plex
|
||||
new (@"(?<=\?|&)(X-Plex-Client-Identifier|X-Plex-Token)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
// Notifiarr
|
||||
new Regex(@"api/v[0-9]/notification/radarr/(?<secret>[\w-]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
// Notifiarr
|
||||
new (@"api/v[0-9]/notification/radarr/(?<secret>[\w-]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
// Discord
|
||||
new Regex(@"discord.com/api/webhooks/((?<secret>[\w-]+)/)?(?<secret>[\w-]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase)
|
||||
};
|
||||
// Discord
|
||||
new (@"discord.com/api/webhooks/((?<secret>[\w-]+)/)?(?<secret>[\w-]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase)
|
||||
};
|
||||
|
||||
private static readonly Regex CleanseRemoteIPRegex = new Regex(@"(?:Auth-\w+(?<!Failure|Unauthorized) ip|from) (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})", RegexOptions.Compiled);
|
||||
private static readonly Regex CleanseRemoteIPRegex = new (@"(?:Auth-\w+(?<!Failure|Unauthorized) ip|from) (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})", RegexOptions.Compiled);
|
||||
|
||||
public static string Cleanse(string message)
|
||||
{
|
||||
@@ -67,15 +67,15 @@ namespace NzbDrone.Common.Instrumentation
|
||||
foreach (var regex in CleansingRules)
|
||||
{
|
||||
message = regex.Replace(message, m =>
|
||||
{
|
||||
var value = m.Value;
|
||||
foreach (var capture in m.Groups["secret"].Captures.OfType<Capture>().Reverse())
|
||||
{
|
||||
var value = m.Value;
|
||||
foreach (var capture in m.Groups["secret"].Captures.OfType<Capture>().Reverse())
|
||||
{
|
||||
value = value.Replace(capture.Index - m.Index, capture.Length, "(removed)");
|
||||
}
|
||||
value = value.Replace(capture.Index - m.Index, capture.Length, "(removed)");
|
||||
}
|
||||
|
||||
return value;
|
||||
});
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
message = CleanseRemoteIPRegex.Replace(message, CleanseRemoteIP);
|
||||
@@ -86,7 +86,6 @@ namespace NzbDrone.Common.Instrumentation
|
||||
private static string CleanseRemoteIP(Match match)
|
||||
{
|
||||
var group = match.Groups[1];
|
||||
var valueAll = match.Value;
|
||||
var valueIP = group.Value;
|
||||
|
||||
if (IPAddress.TryParse(valueIP, out var address) && !address.IsLocalAddress())
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
<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="NLog" Version="5.0.1" />
|
||||
<PackageReference Include="NLog.Extensions.Logging" Version="5.0.0" />
|
||||
<PackageReference Include="NLog" Version="5.2.0" />
|
||||
<PackageReference Include="NLog.Extensions.Logging" Version="5.3.0" />
|
||||
<PackageReference Include="Npgsql" Version="5.0.11" />
|
||||
<PackageReference Include="Sentry" Version="3.23.1" />
|
||||
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
|
||||
|
||||
@@ -225,6 +225,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
||||
[TestCase("checkingDL")]
|
||||
[TestCase("checkingUP")]
|
||||
[TestCase("metaDL")]
|
||||
[TestCase("checkingResumeData")]
|
||||
public void queued_item_should_have_required_properties(string state)
|
||||
{
|
||||
var torrent = new QBittorrentTorrent
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
[
|
||||
{
|
||||
"f": "Thunderbirds.Are.Go.S01E09.Slingshot.1080p.WEB-DL.AAC2.0.H.264-Coo7[rartv]",
|
||||
"c": "TV HD Episodes",
|
||||
"d": "magnet:?xt=urn:btih:ff4737b5230307836ec8abce6ab73727f1358bf3&dn=Thunderbirds.Are.Go.S01E09.Slingshot.1080p.WEB-DL.AAC2.0.H.264-Coo7%5Brartv%5D&tr=http%3A%2F%2Ftracker.trackerfix.com%3A80%2Fannounce&tr=udp%3A%2F%2F9.rarbg.me%3A2710&tr=udp%3A%2F%2F9.rarbg.to%3A2710&tr=udp%3A%2F%2Fopen.demonii.com%3A1337%2Fannounce",
|
||||
"s": "44",
|
||||
"l": "19",
|
||||
"t": "896238116",
|
||||
"u": "2015-05-24 19:36:09"
|
||||
},
|
||||
{
|
||||
"f": "Thunderbirds.Are.Go.S01E10.Tunnels.Of.Time.720p.HDTV.x264-RDVAS[rartv]",
|
||||
"c": "TV HD Episodes",
|
||||
"d": "magnet:?xt=urn:btih:47bf1d7bfb72a83300bbe68d0b6aa09591e7a0a1&dn=Thunderbirds.Are.Go.S01E10.Tunnels.Of.Time.720p.HDTV.x264-RDVAS%5Brartv%5D&tr=http%3A%2F%2Ftracker.trackerfix.com%3A80%2Fannounce&tr=udp%3A%2F%2F9.rarbg.me%3A2710&tr=udp%3A%2F%2F9.rarbg.to%3A2710&tr=udp%3A%2F%2Fopen.demonii.com%3A1337%2Fannounce",
|
||||
"s": "179",
|
||||
"l": "125",
|
||||
"t": "556055350",
|
||||
"u": "2015-05-24 19:07:59"
|
||||
},
|
||||
{
|
||||
"f": "Tatau.S01E06.1080p.WEB-DL.AAC2.0.H.264-BS[rartv]",
|
||||
"c": "TV HD Episodes",
|
||||
"d": "magnet:?xt=urn:btih:8857e9b011c7a0483351371721fa9f3ba356dd73&dn=Tatau.S01E06.1080p.WEB-DL.AAC2.0.H.264-BS%5Brartv%5D&tr=http%3A%2F%2Ftracker.trackerfix.com%3A80%2Fannounce&tr=udp%3A%2F%2F9.rarbg.me%3A2710&tr=udp%3A%2F%2F9.rarbg.to%3A2710&tr=udp%3A%2F%2Fopen.demonii.com%3A1337%2Fannounce",
|
||||
"s": "27",
|
||||
"l": "22",
|
||||
"t": "1652442143",
|
||||
"u": "2015-05-24 18:54:49"
|
||||
}
|
||||
]
|
||||
@@ -1,84 +0,0 @@
|
||||
{
|
||||
"torrent_results": [
|
||||
{
|
||||
"title": "Sense8.S01E01.WEBRip.x264-FGT",
|
||||
"category": "TV Episodes",
|
||||
"download": "magnet:?xt=urn:btih:d8bde635f573acb390c7d7e7efc1556965fdc802&dn=Sense8.S01E01.WEBRip.x264-FGT&tr=http%3A%2F%2Ftracker.trackerfix.com%3A80%2Fannounce&tr=udp%3A%2F%2F9.rarbg.me%3A2710&tr=udp%3A%2F%2F9.rarbg.to%3A2710&tr=udp%3A%2F%2Fopen.demonii.com%3A1337%2Fannounce",
|
||||
"seeders": 304,
|
||||
"leechers": 200,
|
||||
"size": 564198371,
|
||||
"pubdate": "2015-06-05 16:58:11 +0000",
|
||||
"episode_info": {
|
||||
"imdb": "tt2431438",
|
||||
"tvrage": "35197",
|
||||
"tvdb": "268156",
|
||||
"airdate": "2015-06-05",
|
||||
"epnum": "01",
|
||||
"seasonnum": "1",
|
||||
"title": "Limbic Resonance"
|
||||
},
|
||||
"ranked": 1,
|
||||
"info_page": "https:\/\/torrentapi.org\/redirect_to_info.php?token=i5cx7b9agd&p=8_6_4_4_5_6__d8bde635f5"
|
||||
},
|
||||
{
|
||||
"title": "Sense8.S01E02.WEBRip.x264-FGT",
|
||||
"category": "TV Episodes",
|
||||
"download": "magnet:?xt=urn:btih:e5ab5f398d929c791ac4f1d5bb2fba0997372a91&dn=Sense8.S01E02.WEBRip.x264-FGT&tr=http%3A%2F%2Ftracker.trackerfix.com%3A80%2Fannounce&tr=udp%3A%2F%2F9.rarbg.me%3A2710&tr=udp%3A%2F%2F9.rarbg.to%3A2710&tr=udp%3A%2F%2Fopen.demonii.com%3A1337%2Fannounce",
|
||||
"seeders": 299,
|
||||
"leechers": 209,
|
||||
"size": 486918696,
|
||||
"pubdate": "2015-06-05 16:58:23 +0000",
|
||||
"episode_info": {
|
||||
"imdb": "tt2431438",
|
||||
"tvrage": "35197",
|
||||
"tvdb": "268156",
|
||||
"airdate": "2015-06-05",
|
||||
"epnum": "02",
|
||||
"seasonnum": "1",
|
||||
"title": "I Am Also A We"
|
||||
},
|
||||
"ranked": 1,
|
||||
"info_page": "https:\/\/torrentapi.org\/redirect_to_info.php?token=i5cx7b9agd&p=8_6_4_4_5_7__e5ab5f398d"
|
||||
},
|
||||
{
|
||||
"title": "Comedy.Bang.Bang.S04E20.HDTV.x264-YesTV[rartv]",
|
||||
"category": "TV Episodes",
|
||||
"download": "magnet:?xt=urn:btih:0ed8bd14206e211eef9d3d36a48b038f280ef20c&dn=Comedy.Bang.Bang.S04E20.HDTV.x264-YesTV%5Brartv%5D&tr=http%3A%2F%2Ftracker.trackerfix.com%3A80%2Fannounce&tr=udp%3A%2F%2F9.rarbg.me%3A2710&tr=udp%3A%2F%2F9.rarbg.to%3A2710&tr=udp%3A%2F%2Fopen.demonii.com%3A1337%2Fannounce",
|
||||
"seeders": 45,
|
||||
"leechers": 15,
|
||||
"size": 154208067,
|
||||
"pubdate": "2015-06-05 17:33:37 +0000",
|
||||
"episode_info": {
|
||||
"imdb": "tt2176287",
|
||||
"tvrage": "31483",
|
||||
"tvdb": "258310",
|
||||
"airdate": "2015-06-05",
|
||||
"epnum": "20",
|
||||
"seasonnum": "4",
|
||||
"title": "Judd Apatow Wears a Polo and Blue Suede Shoes"
|
||||
},
|
||||
"ranked": 1,
|
||||
"info_page": "https:\/\/torrentapi.org\/redirect_to_info.php?token=i5cx7b9agd&p=8_6_4_4_6_7__0ed8bd1420"
|
||||
},
|
||||
{
|
||||
"title": "Comedy.Bang.Bang.S04E20.720p.HDTV.x264-YesTV[rartv]",
|
||||
"category": "TV HD Episodes",
|
||||
"download": "magnet:?xt=urn:btih:10257dee06327ba66cc2674e08d71b3bb2089b06&dn=Comedy.Bang.Bang.S04E20.720p.HDTV.x264-YesTV%5Brartv%5D&tr=http%3A%2F%2Ftracker.trackerfix.com%3A80%2Fannounce&tr=udp%3A%2F%2F9.rarbg.me%3A2710&tr=udp%3A%2F%2F9.rarbg.to%3A2710&tr=udp%3A%2F%2Fopen.demonii.com%3A1337%2Fannounce",
|
||||
"seeders": 22,
|
||||
"leechers": 6,
|
||||
"size": 514574549,
|
||||
"pubdate": "2015-06-05 17:33:49 +0000",
|
||||
"episode_info": {
|
||||
"imdb": "tt2176287",
|
||||
"tvrage": "31483",
|
||||
"tvdb": "258310",
|
||||
"airdate": "2015-06-05",
|
||||
"epnum": "20",
|
||||
"seasonnum": "4",
|
||||
"title": "Judd Apatow Wears a Polo and Blue Suede Shoes"
|
||||
},
|
||||
"ranked": 1,
|
||||
"info_page": "https:\/\/torrentapi.org\/redirect_to_info.php?token=i5cx7b9agd&p=8_6_4_4_6_8__10257dee06"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Indexers.Rarbg;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Test.Common;
|
||||
|
||||
namespace NzbDrone.Core.Test.IndexerTests.RarbgTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class RarbgFixture : CoreTest<Rarbg>
|
||||
{
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
Subject.Definition = new IndexerDefinition()
|
||||
{
|
||||
Name = "Rarbg",
|
||||
Settings = new RarbgSettings()
|
||||
};
|
||||
|
||||
Mocker.GetMock<IRarbgTokenProvider>()
|
||||
.Setup(v => v.GetToken(It.IsAny<RarbgSettings>()))
|
||||
.Returns("validtoken");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_parse_recent_feed_from_Rarbg()
|
||||
{
|
||||
var recentFeed = ReadAllText(@"Files/Indexers/Rarbg/RecentFeed_v2.json");
|
||||
|
||||
Mocker.GetMock<IHttpClient>()
|
||||
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
|
||||
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
|
||||
|
||||
var releases = Subject.FetchRecent();
|
||||
|
||||
releases.Should().HaveCount(4);
|
||||
releases.First().Should().BeOfType<TorrentInfo>();
|
||||
|
||||
var torrentInfo = releases.First() as TorrentInfo;
|
||||
|
||||
torrentInfo.Title.Should().Be("Sense8.S01E01.WEBRip.x264-FGT");
|
||||
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
||||
torrentInfo.DownloadUrl.Should().Be("magnet:?xt=urn:btih:d8bde635f573acb390c7d7e7efc1556965fdc802&dn=Sense8.S01E01.WEBRip.x264-FGT&tr=http%3A%2F%2Ftracker.trackerfix.com%3A80%2Fannounce&tr=udp%3A%2F%2F9.rarbg.me%3A2710&tr=udp%3A%2F%2F9.rarbg.to%3A2710&tr=udp%3A%2F%2Fopen.demonii.com%3A1337%2Fannounce");
|
||||
torrentInfo.InfoUrl.Should().Be("https://torrentapi.org/redirect_to_info.php?token=i5cx7b9agd&p=8_6_4_4_5_6__d8bde635f5&app_id=Radarr");
|
||||
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2015-06-05 16:58:11 +0000").ToUniversalTime());
|
||||
torrentInfo.Size.Should().Be(564198371);
|
||||
torrentInfo.InfoHash.Should().BeNull();
|
||||
torrentInfo.MagnetUrl.Should().BeNull();
|
||||
torrentInfo.Peers.Should().Be(304 + 200);
|
||||
torrentInfo.Seeders.Should().Be(304);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_parse_error_20_as_empty_results()
|
||||
{
|
||||
Mocker.GetMock<IHttpClient>()
|
||||
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
|
||||
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), "{ error_code: 20, error: \"some message\" }"));
|
||||
|
||||
var releases = Subject.FetchRecent();
|
||||
|
||||
releases.Should().HaveCount(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_warn_on_unknown_error()
|
||||
{
|
||||
Mocker.GetMock<IHttpClient>()
|
||||
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
|
||||
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), "{ error_code: 25, error: \"some message\" }"));
|
||||
|
||||
var releases = Subject.FetchRecent();
|
||||
|
||||
releases.Should().HaveCount(0);
|
||||
|
||||
ExceptionVerification.ExpectedWarns(1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_warn_and_record_failure_on_429_response()
|
||||
{
|
||||
Mocker.GetMock<IHttpClient>()
|
||||
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
|
||||
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), "", HttpStatusCode.TooManyRequests));
|
||||
|
||||
var releases = Subject.FetchRecent();
|
||||
|
||||
releases.Should().HaveCount(0);
|
||||
|
||||
ExceptionVerification.ExpectedWarns(1);
|
||||
|
||||
Mocker.GetMock<IIndexerStatusService>()
|
||||
.Verify(v => v.RecordFailure(It.IsAny<int>(), It.Is<TimeSpan>(t => t == TimeSpan.FromMinutes(2))));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_warn_and_record_failure_on_520_response()
|
||||
{
|
||||
Mocker.GetMock<IHttpClient>()
|
||||
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
|
||||
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), "", (HttpStatusCode)520));
|
||||
|
||||
var releases = Subject.FetchRecent();
|
||||
|
||||
releases.Should().HaveCount(0);
|
||||
|
||||
ExceptionVerification.ExpectedWarns(1);
|
||||
|
||||
Mocker.GetMock<IIndexerStatusService>()
|
||||
.Verify(v => v.RecordFailure(It.IsAny<int>(), It.Is<TimeSpan>(t => t == TimeSpan.FromMinutes(3))));
|
||||
}
|
||||
|
||||
// Uncomment when RarbgParser is updated
|
||||
// [Test]
|
||||
// public void should_warn_and_record_failure_on_200_response_with_rate_limit()
|
||||
// {
|
||||
// Mocker.GetMock<IHttpClient>()
|
||||
// .Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
|
||||
// .Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), "{ rate_limit: 1 }"));
|
||||
//
|
||||
// var releases = Subject.FetchRecent();
|
||||
//
|
||||
// releases.Should().HaveCount(0);
|
||||
//
|
||||
// ExceptionVerification.ExpectedWarns(1);
|
||||
//
|
||||
// Mocker.GetMock<IIndexerStatusService>()
|
||||
// .Verify(v => v.RecordFailure(It.IsAny<int>(), It.Is<TimeSpan>(t => t == TimeSpan.FromMinutes(5))));
|
||||
// }
|
||||
}
|
||||
}
|
||||
@@ -109,6 +109,7 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
[TestCase("Movie.Should.Not.Use.Dots.2022.1080p.BluRay.x265.10bit.Tigole", "Tigole")]
|
||||
[TestCase("Movie.Title.2005.2160p.UHD.BluRay.TrueHD 7.1.Atmos.x265 - HQMUX", "HQMUX")]
|
||||
[TestCase("Movie.Name.2022.1080p.BluRay.x264-VARYG (Blue Lock, Multi-Subs)", "VARYG")]
|
||||
[TestCase("Movie Title (2023) (1080p BluRay x265 SDR AAC 2.0 English Vyndros)", "Vyndros")]
|
||||
public void should_parse_exception_release_group(string title, string expected)
|
||||
{
|
||||
Parser.Parser.ParseReleaseGroup(title).Should().Be(expected);
|
||||
|
||||
43
src/NzbDrone.Core.Test/ParserTests/UrlFixture.cs
Normal file
43
src/NzbDrone.Core.Test/ParserTests/UrlFixture.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.ParserTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class UrlFixture : CoreTest
|
||||
{
|
||||
[TestCase("[www.test.com] - Movie.Title.2023.720p.HDTV.X264-DIMENSION", "Movie Title")]
|
||||
[TestCase("test.net - Movie.Title.2023.720p.HDTV.X264-DIMENSION", "Movie Title")]
|
||||
[TestCase("[www.test-hyphen.com] - Movie.Title.2023.720p.HDTV.X264-DIMENSION", "Movie Title")]
|
||||
[TestCase("www.test123.org - Movie.Title.2023.720p.HDTV.X264-DIMENSION", "Movie Title")]
|
||||
[TestCase("[test.co.uk] - Movie.Title.2023.720p.HDTV.X264-DIMENSION", "Movie Title")]
|
||||
[TestCase("www.test-hyphen.net.au - Movie.Title.2023.720p.HDTV.X264-DIMENSION", "Movie Title")]
|
||||
[TestCase("[www.test123.co.nz] - Movie.Title.2023.720p.HDTV.X264-DIMENSION", "Movie Title")]
|
||||
[TestCase("test-hyphen123.org.au - Movie.Title.2023.720p.HDTV.X264-DIMENSION", "Movie Title")]
|
||||
[TestCase("[www.test123.de] - Mad Movie Title 2023 [Bluray720p]", "Mad Movie Title")]
|
||||
[TestCase("www.test-hyphen.de - Mad Movie Title 2023 [Bluray1080p]", "Mad Movie Title")]
|
||||
[TestCase("www.test123.co.za - The Movie Title Bros. (2023)", "The Movie Title Bros.")]
|
||||
[TestCase("[www.test-hyphen.ca] - Movie Title (2023)", "Movie Title")]
|
||||
[TestCase("test123.ca - Movie Time 2023 720p HDTV x264 CRON", "Movie Time")]
|
||||
[TestCase("[www.test-hyphen123.co.za] - Movie Title 2023", "Movie Title")]
|
||||
public void should_not_parse_url_in_name(string postTitle, string title)
|
||||
{
|
||||
var result = Parser.Parser.ParseMovieTitle(postTitle).MovieTitle.CleanMovieTitle();
|
||||
result.Should().Be(title.CleanMovieTitle());
|
||||
}
|
||||
|
||||
[TestCase("Movie.2023.English.HDTV.XviD-LOL[www.abb.com]", "LOL")]
|
||||
[TestCase("Movie Title 2023 English HDTV XviD LOL[www.academy.org]", null)]
|
||||
[TestCase("Movie Title Now 2023 DVDRip XviD RUNNER[www.aetna.net]", null)]
|
||||
[TestCase("Movie.Title.2023.DVDRip.XviD-RUNNER[www.alfaromeo.io]", "RUNNER")]
|
||||
[TestCase("Movie.Title.2023.English.HDTV.XviD-LOL[www.abbott.gov]", "LOL")]
|
||||
[TestCase("Movie Title 2023 English HDTV XviD LOL[www.actor.org]", null)]
|
||||
[TestCase("Movie Title Future 2023 DVDRip XviD RUNNER[www.allstate.net]", null)]
|
||||
public void should_not_parse_url_in_group(string title, string expected)
|
||||
{
|
||||
Parser.Parser.ParseReleaseGroup(title).Should().Be(expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ namespace NzbDrone.Core.Annotations
|
||||
public string Label { get; set; }
|
||||
public string Unit { get; set; }
|
||||
public string HelpText { get; set; }
|
||||
public string HelpTextWarning { get; set; }
|
||||
public string HelpLink { get; set; }
|
||||
public FieldType Type { get; set; }
|
||||
public bool Advanced { get; set; }
|
||||
|
||||
14
src/NzbDrone.Core/Datastore/Migration/222_remove_rarbg.cs
Normal file
14
src/NzbDrone.Core/Datastore/Migration/222_remove_rarbg.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(222)]
|
||||
public class remove_rarbg : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Delete.FromTable("Indexers").Row(new { Implementation = "Rarbg" });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -251,6 +251,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
case "queuedDL": // queuing is enabled and torrent is queued for download
|
||||
case "checkingDL": // same as checkingUP, but torrent has NOT finished downloading
|
||||
case "checkingUP": // torrent has finished downloading and is being checked. Set when `recheck torrent on completion` is enabled. In the event the check fails we shouldn't treat it as completed.
|
||||
case "checkingResumeData": // torrent is checking resume data on load
|
||||
item.Status = DownloadItemStatus.Queued;
|
||||
break;
|
||||
|
||||
|
||||
@@ -48,13 +48,11 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
try
|
||||
{
|
||||
var status = client.GetStatus();
|
||||
var folders = status.OutputRootFolders;
|
||||
var folders = status.OutputRootFolders.Where(folder => rootFolders.Any(r => r.Path.PathEquals(folder.FullPath)));
|
||||
|
||||
foreach (var folder in folders)
|
||||
{
|
||||
if (rootFolders.Any(r => r.Path.PathEquals(folder.FullPath)))
|
||||
{
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Warning, string.Format(_localizationService.GetLocalizedString("DownloadClientCheckDownloadingToRoot"), client.Definition.Name, folder.FullPath), "#downloads-in-root-folder");
|
||||
}
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Warning, string.Format(_localizationService.GetLocalizedString("DownloadClientCheckDownloadingToRoot"), client.Definition.Name, folder.FullPath), "#downloads-in-root-folder");
|
||||
}
|
||||
}
|
||||
catch (DownloadClientException ex)
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
|
||||
public override HealthCheck Check()
|
||||
{
|
||||
var clients = _downloadClientProvider.GetDownloadClients();
|
||||
var clients = _downloadClientProvider.GetDownloadClients(true);
|
||||
|
||||
foreach (var client in clients)
|
||||
{
|
||||
|
||||
@@ -23,12 +23,14 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
|
||||
public override HealthCheck Check()
|
||||
{
|
||||
var jackettAllProviders = _providerFactory.All().Where(
|
||||
i => i.ConfigContract.Equals("TorznabSettings") &&
|
||||
((i.Settings as TorznabSettings).BaseUrl.Contains("/torznab/all/api", StringComparison.InvariantCultureIgnoreCase) ||
|
||||
(i.Settings as TorznabSettings).BaseUrl.Contains("/api/v2.0/indexers/all/results/torznab", StringComparison.InvariantCultureIgnoreCase) ||
|
||||
(i.Settings as TorznabSettings).ApiPath.Contains("/torznab/all/api", StringComparison.InvariantCultureIgnoreCase) ||
|
||||
(i.Settings as TorznabSettings).ApiPath.Contains("/api/v2.0/indexers/all/results/torznab", StringComparison.InvariantCultureIgnoreCase)));
|
||||
var jackettAllProviders = _providerFactory.All()
|
||||
.Where(
|
||||
i => i.ConfigContract.Equals("TorznabSettings") &&
|
||||
(((TorznabSettings)i.Settings).BaseUrl.Contains("/torznab/all/api", StringComparison.InvariantCultureIgnoreCase) ||
|
||||
((TorznabSettings)i.Settings).BaseUrl.Contains("/api/v2.0/indexers/all/results/torznab", StringComparison.InvariantCultureIgnoreCase) ||
|
||||
((TorznabSettings)i.Settings).ApiPath.Contains("/torznab/all/api", StringComparison.InvariantCultureIgnoreCase) ||
|
||||
((TorznabSettings)i.Settings).ApiPath.Contains("/api/v2.0/indexers/all/results/torznab", StringComparison.InvariantCultureIgnoreCase)))
|
||||
.ToArray();
|
||||
|
||||
if (jackettAllProviders.Empty())
|
||||
{
|
||||
|
||||
@@ -28,13 +28,12 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
{
|
||||
var enabledProviders = _providerFactory.GetAvailableProviders();
|
||||
var backOffProviders = enabledProviders.Join(_providerStatusService.GetBlockedProviders(),
|
||||
i => i.Definition.Id,
|
||||
s => s.ProviderId,
|
||||
(i, s) => new { Provider = i, Status = s })
|
||||
.Where(p => p.Status.InitialFailure.HasValue &&
|
||||
p.Status.InitialFailure.Value.Before(
|
||||
DateTime.UtcNow.AddHours(-6)))
|
||||
.ToList();
|
||||
i => i.Definition.Id,
|
||||
s => s.ProviderId,
|
||||
(i, s) => new { Provider = i, Status = s })
|
||||
.Where(p => p.Status.InitialFailure.HasValue &&
|
||||
p.Status.InitialFailure.Value.Before(DateTime.UtcNow.AddHours(-6)))
|
||||
.ToList();
|
||||
|
||||
if (backOffProviders.Empty())
|
||||
{
|
||||
|
||||
@@ -26,13 +26,12 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
{
|
||||
var enabledProviders = _providerFactory.GetAvailableProviders();
|
||||
var backOffProviders = enabledProviders.Join(_providerStatusService.GetBlockedProviders(),
|
||||
i => i.Definition.Id,
|
||||
s => s.ProviderId,
|
||||
(i, s) => new { Provider = i, Status = s })
|
||||
.Where(p => p.Status.InitialFailure.HasValue &&
|
||||
p.Status.InitialFailure.Value.After(
|
||||
DateTime.UtcNow.AddHours(-6)))
|
||||
.ToList();
|
||||
i => i.Definition.Id,
|
||||
s => s.ProviderId,
|
||||
(i, s) => new { Provider = i, Status = s })
|
||||
.Where(p => p.Status.InitialFailure.HasValue &&
|
||||
p.Status.InitialFailure.Value.After(DateTime.UtcNow.AddHours(-6)))
|
||||
.ToList();
|
||||
|
||||
if (backOffProviders.Empty())
|
||||
{
|
||||
|
||||
@@ -21,10 +21,10 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
{
|
||||
// Not best for optimization but due to possible symlinks and junctions, we get mounts based on series path so internals can handle mount resolution.
|
||||
var mounts = _movieService.AllMoviePaths()
|
||||
.Select(p => _diskProvider.GetMount(p.Value))
|
||||
.Where(m => m != null && m.MountOptions != null && m.MountOptions.IsReadOnly)
|
||||
.DistinctBy(m => m.RootDirectory)
|
||||
.ToList();
|
||||
.Select(p => _diskProvider.GetMount(p.Value))
|
||||
.Where(m => m is { MountOptions.IsReadOnly: true })
|
||||
.DistinctBy(m => m.RootDirectory)
|
||||
.ToList();
|
||||
|
||||
if (mounts.Any())
|
||||
{
|
||||
|
||||
@@ -18,10 +18,12 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
|
||||
public override HealthCheck Check()
|
||||
{
|
||||
var ptpIndexers = _indexerFactory.All().Where(i => i.Settings.GetType() == typeof(PassThePopcornSettings));
|
||||
var ptpIndexers = _indexerFactory.All()
|
||||
.Where(i => i.Settings.GetType() == typeof(PassThePopcornSettings));
|
||||
|
||||
var ptpIndexerOldSettings = ptpIndexers
|
||||
.Where(i => (i.Settings as PassThePopcornSettings).APIUser.IsNullOrWhiteSpace()).Select(i => i.Name);
|
||||
.Where(i => ((PassThePopcornSettings)i.Settings).APIUser.IsNullOrWhiteSpace()).Select(i => i.Name)
|
||||
.ToList();
|
||||
|
||||
if (ptpIndexerOldSettings.Any())
|
||||
{
|
||||
|
||||
@@ -31,35 +31,38 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
|
||||
public override HealthCheck Check()
|
||||
{
|
||||
if (_configService.ProxyEnabled)
|
||||
if (!_configService.ProxyEnabled)
|
||||
{
|
||||
var addresses = Dns.GetHostAddresses(_configService.ProxyHostname);
|
||||
if (!addresses.Any())
|
||||
{
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("ProxyCheckResolveIpMessage"), _configService.ProxyHostname), "#proxy-failed-resolve-ip");
|
||||
}
|
||||
return new HealthCheck(GetType());
|
||||
}
|
||||
|
||||
var request = _cloudRequestBuilder.Create()
|
||||
.Resource("/ping")
|
||||
.Build();
|
||||
var addresses = Dns.GetHostAddresses(_configService.ProxyHostname);
|
||||
|
||||
try
|
||||
{
|
||||
var response = _client.Execute(request);
|
||||
if (!addresses.Any())
|
||||
{
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("ProxyCheckResolveIpMessage"), _configService.ProxyHostname), "#proxy-failed-resolve-ip");
|
||||
}
|
||||
|
||||
// We only care about 400 responses, other error codes can be ignored
|
||||
if (response.StatusCode == HttpStatusCode.BadRequest)
|
||||
{
|
||||
_logger.Error("Proxy Health Check failed: {0}", response.StatusCode);
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("ProxyCheckBadRequestMessage"), response.StatusCode), "#proxy-failed-test");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
var request = _cloudRequestBuilder.Create()
|
||||
.Resource("/ping")
|
||||
.Build();
|
||||
|
||||
try
|
||||
{
|
||||
var response = _client.Execute(request);
|
||||
|
||||
// We only care about 400 responses, other error codes can be ignored
|
||||
if (response.StatusCode == HttpStatusCode.BadRequest)
|
||||
{
|
||||
_logger.Error(ex, "Proxy Health Check failed");
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("ProxyCheckFailedToTestMessage"), request.Url), "#proxy-failed-test");
|
||||
_logger.Error("Proxy Health Check failed: {0}", response.StatusCode);
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("ProxyCheckBadRequestMessage"), response.StatusCode), "#proxy-failed-test");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Proxy Health Check failed");
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("ProxyCheckFailedToTestMessage"), request.Url), "#proxy-failed-test");
|
||||
}
|
||||
|
||||
return new HealthCheck(GetType());
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
{
|
||||
var status = client.GetStatus();
|
||||
var folders = status.OutputRootFolders;
|
||||
|
||||
foreach (var folder in folders)
|
||||
{
|
||||
if (!folder.IsValid)
|
||||
@@ -70,14 +71,13 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
{
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckWrongOSPath"), client.Definition.Name, folder.FullPath, _osInfo.Name), "#bad-remote-path-mapping");
|
||||
}
|
||||
else if (_osInfo.IsDocker)
|
||||
|
||||
if (_osInfo.IsDocker)
|
||||
{
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckBadDockerPath"), client.Definition.Name, folder.FullPath, _osInfo.Name), "#docker-bad-remote-path-mapping");
|
||||
}
|
||||
else
|
||||
{
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckLocalWrongOSPath"), client.Definition.Name, folder.FullPath, _osInfo.Name), "#bad-download-client-settings");
|
||||
}
|
||||
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckLocalWrongOSPath"), client.Definition.Name, folder.FullPath, _osInfo.Name), "#bad-download-client-settings");
|
||||
}
|
||||
|
||||
if (!_diskProvider.FolderExists(folder.FullPath))
|
||||
@@ -86,14 +86,13 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
{
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckDockerFolderMissing"), client.Definition.Name, folder.FullPath), "#docker-bad-remote-path-mapping");
|
||||
}
|
||||
else if (!status.IsLocalhost)
|
||||
|
||||
if (!status.IsLocalhost)
|
||||
{
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckLocalFolderMissing"), client.Definition.Name, folder.FullPath), "#bad-remote-path-mapping");
|
||||
}
|
||||
else
|
||||
{
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckGenericPermissions"), client.Definition.Name, folder.FullPath), "#permissions-error");
|
||||
}
|
||||
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckGenericPermissions"), client.Definition.Name, folder.FullPath), "#permissions-error");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -122,24 +121,21 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
return new HealthCheck(GetType());
|
||||
}
|
||||
|
||||
if (typeof(MovieImportFailedEvent).IsAssignableFrom(message.GetType()))
|
||||
if (message is MovieImportFailedEvent failureMessage)
|
||||
{
|
||||
var failureMessage = (MovieImportFailedEvent)message;
|
||||
|
||||
// if we can see the file exists but the import failed then likely a permissions issue
|
||||
if (failureMessage.MovieInfo != null)
|
||||
{
|
||||
var moviePath = failureMessage.MovieInfo.Path;
|
||||
|
||||
if (_diskProvider.FileExists(moviePath))
|
||||
{
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckDownloadPermissions"), moviePath), "#permissions-error");
|
||||
}
|
||||
else
|
||||
{
|
||||
// If the file doesn't exist but MovieInfo is not null then the message is coming from
|
||||
// ImportApprovedMovies and the file must have been removed part way through processing
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckFileRemoved"), moviePath), "#remote-path-file-removed");
|
||||
}
|
||||
|
||||
// If the file doesn't exist but MovieInfo is not null then the message is coming from
|
||||
// ImportApprovedMovies and the file must have been removed part way through processing
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckFileRemoved"), moviePath), "#remote-path-file-removed");
|
||||
}
|
||||
|
||||
// If the previous case did not match then the failure occured in DownloadedMovieImportService,
|
||||
@@ -170,14 +166,13 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
{
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckFilesWrongOSPath"), client.Definition.Name, dlpath, _osInfo.Name), "#bad-remote-path-mapping");
|
||||
}
|
||||
else if (_osInfo.IsDocker)
|
||||
|
||||
if (_osInfo.IsDocker)
|
||||
{
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckFilesBadDockerPath"), client.Definition.Name, dlpath, _osInfo.Name), "#docker-bad-remote-path-mapping");
|
||||
}
|
||||
else
|
||||
{
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckFilesLocalWrongOSPath"), client.Definition.Name, dlpath, _osInfo.Name), "#bad-download-client-settings");
|
||||
}
|
||||
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckFilesLocalWrongOSPath"), client.Definition.Name, dlpath, _osInfo.Name), "#bad-download-client-settings");
|
||||
}
|
||||
|
||||
if (_diskProvider.FolderExists(dlpath))
|
||||
@@ -190,15 +185,14 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
{
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckFolderPermissions"), client.Definition.Name, dlpath), "#docker-bad-remote-path-mapping");
|
||||
}
|
||||
else if (!status.IsLocalhost)
|
||||
|
||||
if (!status.IsLocalhost)
|
||||
{
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckRemoteDownloadClient"), client.Definition.Name, dlpath), "#bad-remote-path-mapping");
|
||||
}
|
||||
else
|
||||
{
|
||||
// path mappings shouldn't be needed locally so probably a permissions issue
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckFilesGenericPermissions"), client.Definition.Name, dlpath), "#permissions-error");
|
||||
}
|
||||
|
||||
// path mappings shouldn't be needed locally so probably a permissions issue
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckFilesGenericPermissions"), client.Definition.Name, dlpath), "#permissions-error");
|
||||
}
|
||||
catch (DownloadClientException ex)
|
||||
{
|
||||
@@ -215,10 +209,8 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
|
||||
return new HealthCheck(GetType());
|
||||
}
|
||||
else
|
||||
{
|
||||
return Check();
|
||||
}
|
||||
|
||||
return Check();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,11 +29,11 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
public override HealthCheck Check()
|
||||
{
|
||||
var rootFolders = _movieService.AllMoviePaths()
|
||||
.Select(s => _rootFolderService.GetBestRootFolderPath(s.Value))
|
||||
.Distinct();
|
||||
.Select(s => _rootFolderService.GetBestRootFolderPath(s.Value))
|
||||
.Distinct();
|
||||
|
||||
var missingRootFolders = rootFolders.Where(s => !_diskProvider.FolderExists(s))
|
||||
.ToList();
|
||||
.ToList();
|
||||
|
||||
if (missingRootFolders.Any())
|
||||
{
|
||||
|
||||
@@ -22,7 +22,9 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
|
||||
public override HealthCheck Check()
|
||||
{
|
||||
var discordSlackNotifications = _notificationFactory.GetAvailableProviders().Where(n => n.ConfigContract.Equals("SlackSettings") && (n.Definition.Settings as SlackSettings).WebHookUrl.Contains("discord"));
|
||||
var discordSlackNotifications = _notificationFactory.GetAvailableProviders()
|
||||
.Where(n => n.ConfigContract.Equals("SlackSettings") && ((SlackSettings)n.Definition.Settings).WebHookUrl.Contains("discord"))
|
||||
.ToList();
|
||||
|
||||
if (discordSlackNotifications.Empty())
|
||||
{
|
||||
@@ -31,8 +33,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
|
||||
return new HealthCheck(GetType(),
|
||||
HealthCheckResult.Warning,
|
||||
string.Format(_localizationService.GetLocalizedString("DiscordUrlInSlackNotification"),
|
||||
string.Join(", ", discordSlackNotifications.Select(n => n.Name))),
|
||||
string.Format(_localizationService.GetLocalizedString("DiscordUrlInSlackNotification"), string.Join(", ", discordSlackNotifications.Select(n => n.Name))),
|
||||
"#discord-as-slack-notification");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,8 +24,8 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
public override HealthCheck Check()
|
||||
{
|
||||
var request = _cloudRequestBuilder.Create()
|
||||
.Resource("/time")
|
||||
.Build();
|
||||
.Resource("/time")
|
||||
.Build();
|
||||
|
||||
var response = _client.Execute(request);
|
||||
var result = Json.Deserialize<ServiceTimeResponse>(response.Content);
|
||||
|
||||
@@ -76,7 +76,8 @@ namespace NzbDrone.Core.History
|
||||
|
||||
protected override SqlBuilder PagedBuilder() => new SqlBuilder(_database.DatabaseType)
|
||||
.Join<MovieHistory, Movie>((h, m) => h.MovieId == m.Id)
|
||||
.Join<Movie, Profile>((m, p) => m.ProfileId == p.Id);
|
||||
.Join<Movie, Profile>((m, p) => m.ProfileId == p.Id)
|
||||
.LeftJoin<Movie, MovieMetadata>((m, mm) => m.MovieMetadataId == mm.Id);
|
||||
|
||||
protected override IEnumerable<MovieHistory> PagedQuery(SqlBuilder sql) =>
|
||||
_database.QueryJoined<MovieHistory, Movie, Profile>(sql, (hist, movie, profile) =>
|
||||
|
||||
@@ -14,34 +14,32 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
|
||||
|
||||
public void Clean()
|
||||
{
|
||||
using (var mapper = _database.OpenConnection())
|
||||
using var mapper = _database.OpenConnection();
|
||||
if (_database.DatabaseType == DatabaseType.PostgreSQL)
|
||||
{
|
||||
if (_database.DatabaseType == DatabaseType.PostgreSQL)
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM ""MetadataFiles""
|
||||
WHERE ""Id"" = ANY (
|
||||
SELECT ""Id"" FROM ""MetadataFiles""
|
||||
WHERE ""RelativePath""
|
||||
LIKE '_:\\%'
|
||||
OR ""RelativePath""
|
||||
LIKE '\\%'
|
||||
OR ""RelativePath""
|
||||
LIKE '/%'
|
||||
)");
|
||||
}
|
||||
else
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM ""MetadataFiles""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""Id"" FROM ""MetadataFiles""
|
||||
WHERE ""RelativePath""
|
||||
LIKE '_:\%'
|
||||
OR ""RelativePath""
|
||||
LIKE '\%'
|
||||
OR ""RelativePath""
|
||||
LIKE '/%'
|
||||
)");
|
||||
}
|
||||
mapper.Execute(@"DELETE FROM ""MetadataFiles""
|
||||
WHERE ""Id"" = ANY (
|
||||
SELECT ""Id"" FROM ""MetadataFiles""
|
||||
WHERE ""RelativePath""
|
||||
LIKE '_:\\%'
|
||||
OR ""RelativePath""
|
||||
LIKE '\\%'
|
||||
OR ""RelativePath""
|
||||
LIKE '/%'
|
||||
)");
|
||||
}
|
||||
else
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM ""MetadataFiles""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""Id"" FROM ""MetadataFiles""
|
||||
WHERE ""RelativePath""
|
||||
LIKE '_:\%'
|
||||
OR ""RelativePath""
|
||||
LIKE '\%'
|
||||
OR ""RelativePath""
|
||||
LIKE '/%'
|
||||
)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,13 +14,11 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
|
||||
|
||||
public void Clean()
|
||||
{
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM ""NamingConfig""
|
||||
WHERE ""Id"" NOT IN (
|
||||
SELECT ""Id"" FROM ""NamingConfig""
|
||||
LIMIT 1)");
|
||||
}
|
||||
using var mapper = _database.OpenConnection();
|
||||
mapper.Execute(@"DELETE FROM ""NamingConfig""
|
||||
WHERE ""Id"" NOT IN (
|
||||
SELECT ""Id"" FROM ""NamingConfig""
|
||||
LIMIT 1)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,13 +14,11 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
|
||||
|
||||
public void Clean()
|
||||
{
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM ""Users""
|
||||
WHERE ""Id"" NOT IN (
|
||||
SELECT ""Id"" FROM ""Users""
|
||||
LIMIT 1)");
|
||||
}
|
||||
using var mapper = _database.OpenConnection();
|
||||
mapper.Execute(@"DELETE FROM ""Users""
|
||||
WHERE ""Id"" NOT IN (
|
||||
SELECT ""Id"" FROM ""Users""
|
||||
LIMIT 1)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,29 +16,28 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
|
||||
|
||||
public void Clean()
|
||||
{
|
||||
var mapper = _database.OpenConnection();
|
||||
|
||||
using var mapper = _database.OpenConnection();
|
||||
if (_database.DatabaseType == DatabaseType.PostgreSQL)
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM ""PendingReleases""
|
||||
WHERE ""Added"" < @TwoWeeksAgo
|
||||
AND ""Reason"" = ANY (@Reasons)",
|
||||
new
|
||||
{
|
||||
TwoWeeksAgo = DateTime.UtcNow.AddDays(-14),
|
||||
Reasons = new[] { (int)PendingReleaseReason.DownloadClientUnavailable, (int)PendingReleaseReason.Fallback }
|
||||
});
|
||||
WHERE ""Added"" < @TwoWeeksAgo
|
||||
AND ""Reason"" = ANY (@Reasons)",
|
||||
new
|
||||
{
|
||||
TwoWeeksAgo = DateTime.UtcNow.AddDays(-14),
|
||||
Reasons = new[] { (int)PendingReleaseReason.DownloadClientUnavailable, (int)PendingReleaseReason.Fallback }
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM ""PendingReleases""
|
||||
WHERE ""Added"" < @TwoWeeksAgo
|
||||
AND ""REASON"" IN @Reasons",
|
||||
new
|
||||
{
|
||||
TwoWeeksAgo = DateTime.UtcNow.AddDays(-14),
|
||||
Reasons = new[] { (int)PendingReleaseReason.DownloadClientUnavailable, (int)PendingReleaseReason.Fallback }
|
||||
});
|
||||
mapper.Execute(@"DELETE FROM ""PendingReleases""
|
||||
WHERE ""Added"" < @TwoWeeksAgo
|
||||
AND ""REASON"" IN @Reasons",
|
||||
new
|
||||
{
|
||||
TwoWeeksAgo = DateTime.UtcNow.AddDays(-14),
|
||||
Reasons = new[] { (int)PendingReleaseReason.DownloadClientUnavailable, (int)PendingReleaseReason.Fallback }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,30 +20,26 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
|
||||
|
||||
private void DeleteDuplicateMovieMetadata()
|
||||
{
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM ""MetadataFiles""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT MIN(""Id"") FROM ""MetadataFiles""
|
||||
WHERE ""Type"" = 1
|
||||
GROUP BY ""MovieId"", ""Consumer""
|
||||
HAVING COUNT(""MovieId"") > 1
|
||||
)");
|
||||
}
|
||||
using var mapper = _database.OpenConnection();
|
||||
mapper.Execute(@"DELETE FROM ""MetadataFiles""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT MIN(""Id"") FROM ""MetadataFiles""
|
||||
WHERE ""Type"" = 1
|
||||
GROUP BY ""MovieId"", ""Consumer""
|
||||
HAVING COUNT(""MovieId"") > 1
|
||||
)");
|
||||
}
|
||||
|
||||
private void DeleteDuplicateMovieFileMetadata()
|
||||
{
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM ""MetadataFiles""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT MIN(""Id"") FROM ""MetadataFiles""
|
||||
WHERE ""Type"" = 1
|
||||
GROUP BY ""MovieFileId"", ""Consumer""
|
||||
HAVING COUNT(""MovieFileId"") > 1
|
||||
)");
|
||||
}
|
||||
using var mapper = _database.OpenConnection();
|
||||
mapper.Execute(@"DELETE FROM ""MetadataFiles""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT MIN(""Id"") FROM ""MetadataFiles""
|
||||
WHERE ""Type"" = 1
|
||||
GROUP BY ""MovieFileId"", ""Consumer""
|
||||
HAVING COUNT(""MovieFileId"") > 1
|
||||
)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,15 +14,13 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
|
||||
|
||||
public void Clean()
|
||||
{
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM ""AlternativeTitles""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""AlternativeTitles"".""Id"" FROM ""AlternativeTitles""
|
||||
LEFT OUTER JOIN ""MovieMetadata""
|
||||
ON ""AlternativeTitles"".""MovieMetadataId"" = ""MovieMetadata"".""Id""
|
||||
WHERE ""MovieMetadata"".""Id"" IS NULL)");
|
||||
}
|
||||
using var mapper = _database.OpenConnection();
|
||||
mapper.Execute(@"DELETE FROM ""AlternativeTitles""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""AlternativeTitles"".""Id"" FROM ""AlternativeTitles""
|
||||
LEFT OUTER JOIN ""MovieMetadata""
|
||||
ON ""AlternativeTitles"".""MovieMetadataId"" = ""MovieMetadata"".""Id""
|
||||
WHERE ""MovieMetadata"".""Id"" IS NULL)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,15 +14,13 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
|
||||
|
||||
public void Clean()
|
||||
{
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM ""Blocklist""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""Blocklist"".""Id"" FROM ""Blocklist""
|
||||
LEFT OUTER JOIN ""Movies""
|
||||
ON ""Blocklist"".""MovieId"" = ""Movies"".""Id""
|
||||
WHERE ""Movies"".""Id"" IS NULL)");
|
||||
}
|
||||
using var mapper = _database.OpenConnection();
|
||||
mapper.Execute(@"DELETE FROM ""Blocklist""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""Blocklist"".""Id"" FROM ""Blocklist""
|
||||
LEFT OUTER JOIN ""Movies""
|
||||
ON ""Blocklist"".""MovieId"" = ""Movies"".""Id""
|
||||
WHERE ""Movies"".""Id"" IS NULL)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,13 +14,11 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
|
||||
|
||||
public void Clean()
|
||||
{
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM ""Collections"" WHERE ""TmdbId"" IN (SELECT ""X"".""TmdbId"" FROM (SELECT ""Collections"".""TmdbId"", COUNT(""Movies"".""Id"") as ""MovieCount"" FROM ""Collections""
|
||||
LEFT OUTER JOIN ""MovieMetadata"" ON ""Collections"".""TmdbId"" = ""MovieMetadata"".""CollectionTmdbId""
|
||||
LEFT OUTER JOIN ""Movies"" ON ""Movies"".""MovieMetadataId"" = ""MovieMetadata"".""Id""
|
||||
GROUP BY ""Collections"".""Id"") AS ""X"" WHERE ""X"".""MovieCount"" = 0)");
|
||||
}
|
||||
using var mapper = _database.OpenConnection();
|
||||
mapper.Execute(@"DELETE FROM ""Collections"" WHERE ""TmdbId"" IN (SELECT ""X"".""TmdbId"" FROM (SELECT ""Collections"".""TmdbId"", COUNT(""Movies"".""Id"") as ""MovieCount"" FROM ""Collections""
|
||||
LEFT OUTER JOIN ""MovieMetadata"" ON ""Collections"".""TmdbId"" = ""MovieMetadata"".""CollectionTmdbId""
|
||||
LEFT OUTER JOIN ""Movies"" ON ""Movies"".""MovieMetadataId"" = ""MovieMetadata"".""Id""
|
||||
GROUP BY ""Collections"".""Id"") AS ""X"" WHERE ""X"".""MovieCount"" = 0)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,15 +14,13 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
|
||||
|
||||
public void Clean()
|
||||
{
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM ""Credits""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""Credits"".""Id"" FROM ""Credits""
|
||||
LEFT OUTER JOIN ""MovieMetadata""
|
||||
ON ""Credits"".""MovieMetadataId"" = ""MovieMetadata"".""Id""
|
||||
WHERE ""MovieMetadata"".""Id"" IS NULL)");
|
||||
}
|
||||
using var mapper = _database.OpenConnection();
|
||||
mapper.Execute(@"DELETE FROM ""Credits""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""Credits"".""Id"" FROM ""Credits""
|
||||
LEFT OUTER JOIN ""MovieMetadata""
|
||||
ON ""Credits"".""MovieMetadataId"" = ""MovieMetadata"".""Id""
|
||||
WHERE ""MovieMetadata"".""Id"" IS NULL)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,14 +14,13 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
|
||||
|
||||
public void Clean()
|
||||
{
|
||||
var mapper = _database.OpenConnection();
|
||||
|
||||
using var mapper = _database.OpenConnection();
|
||||
mapper.Execute(@"DELETE FROM ""DownloadClientStatus""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""DownloadClientStatus"".""Id"" FROM ""DownloadClientStatus""
|
||||
LEFT OUTER JOIN ""DownloadClients""
|
||||
ON ""DownloadClientStatus"".""ProviderId"" = ""DownloadClients"".""Id""
|
||||
WHERE ""DownloadClients"".""Id"" IS NULL)");
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""DownloadClientStatus"".""Id"" FROM ""DownloadClientStatus""
|
||||
LEFT OUTER JOIN ""DownloadClients""
|
||||
ON ""DownloadClientStatus"".""ProviderId"" = ""DownloadClients"".""Id""
|
||||
WHERE ""DownloadClients"".""Id"" IS NULL)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,29 +20,25 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
|
||||
|
||||
private void DeleteOrphanedByMovie()
|
||||
{
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM ""ExtraFiles""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""ExtraFiles"".""Id"" FROM ""ExtraFiles""
|
||||
LEFT OUTER JOIN ""Movies""
|
||||
ON ""ExtraFiles"".""MovieId"" = ""Movies"".""Id""
|
||||
WHERE ""Movies"".""Id"" IS NULL)");
|
||||
}
|
||||
using var mapper = _database.OpenConnection();
|
||||
mapper.Execute(@"DELETE FROM ""ExtraFiles""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""ExtraFiles"".""Id"" FROM ""ExtraFiles""
|
||||
LEFT OUTER JOIN ""Movies""
|
||||
ON ""ExtraFiles"".""MovieId"" = ""Movies"".""Id""
|
||||
WHERE ""Movies"".""Id"" IS NULL)");
|
||||
}
|
||||
|
||||
private void DeleteOrphanedByMovieFile()
|
||||
{
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM ""ExtraFiles""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""ExtraFiles"".""Id"" FROM ""ExtraFiles""
|
||||
LEFT OUTER JOIN ""MovieFiles""
|
||||
ON ""ExtraFiles"".""MovieFileId"" = ""MovieFiles"".""Id""
|
||||
WHERE ""ExtraFiles"".""MovieFileId"" > 0
|
||||
AND ""MovieFiles"".""Id"" IS NULL)");
|
||||
}
|
||||
using var mapper = _database.OpenConnection();
|
||||
mapper.Execute(@"DELETE FROM ""ExtraFiles""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""ExtraFiles"".""Id"" FROM ""ExtraFiles""
|
||||
LEFT OUTER JOIN ""MovieFiles""
|
||||
ON ""ExtraFiles"".""MovieFileId"" = ""MovieFiles"".""Id""
|
||||
WHERE ""ExtraFiles"".""MovieFileId"" > 0
|
||||
AND ""MovieFiles"".""Id"" IS NULL)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,15 +19,13 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
|
||||
|
||||
private void CleanupOrphanedByMovie()
|
||||
{
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM ""History""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""History"".""Id"" FROM ""History""
|
||||
LEFT OUTER JOIN ""Movies""
|
||||
ON ""History"".""MovieId"" = ""Movies"".""Id""
|
||||
WHERE ""Movies"".""Id"" IS NULL)");
|
||||
}
|
||||
using var mapper = _database.OpenConnection();
|
||||
mapper.Execute(@"DELETE FROM ""History""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""History"".""Id"" FROM ""History""
|
||||
LEFT OUTER JOIN ""Movies""
|
||||
ON ""History"".""MovieId"" = ""Movies"".""Id""
|
||||
WHERE ""Movies"".""Id"" IS NULL)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,15 +14,13 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
|
||||
|
||||
public void Clean()
|
||||
{
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM ""IndexerStatus""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""IndexerStatus"".""Id"" FROM ""IndexerStatus""
|
||||
LEFT OUTER JOIN ""Indexers""
|
||||
ON ""IndexerStatus"".""ProviderId"" = ""Indexers"".""Id""
|
||||
WHERE ""Indexers"".""Id"" IS NULL)");
|
||||
}
|
||||
using var mapper = _database.OpenConnection();
|
||||
mapper.Execute(@"DELETE FROM ""IndexerStatus""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""IndexerStatus"".""Id"" FROM ""IndexerStatus""
|
||||
LEFT OUTER JOIN ""Indexers""
|
||||
ON ""IndexerStatus"".""ProviderId"" = ""Indexers"".""Id""
|
||||
WHERE ""Indexers"".""Id"" IS NULL)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,41 +21,35 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
|
||||
|
||||
private void DeleteOrphanedByMovie()
|
||||
{
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM ""MetadataFiles""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""MetadataFiles"".""Id"" FROM ""MetadataFiles""
|
||||
LEFT OUTER JOIN ""Movies""
|
||||
ON ""MetadataFiles"".""MovieId"" = ""Movies"".""Id""
|
||||
WHERE ""Movies"".""Id"" IS NULL)");
|
||||
}
|
||||
using var mapper = _database.OpenConnection();
|
||||
mapper.Execute(@"DELETE FROM ""MetadataFiles""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""MetadataFiles"".""Id"" FROM ""MetadataFiles""
|
||||
LEFT OUTER JOIN ""Movies""
|
||||
ON ""MetadataFiles"".""MovieId"" = ""Movies"".""Id""
|
||||
WHERE ""Movies"".""Id"" IS NULL)");
|
||||
}
|
||||
|
||||
private void DeleteOrphanedByMovieFile()
|
||||
{
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM ""MetadataFiles""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""MetadataFiles"".""Id"" FROM ""MetadataFiles""
|
||||
LEFT OUTER JOIN ""MovieFiles""
|
||||
ON ""MetadataFiles"".""MovieFileId"" = ""MovieFiles"".""Id""
|
||||
WHERE ""MetadataFiles"".""MovieFileId"" > 0
|
||||
AND ""MovieFiles"".""Id"" IS NULL)");
|
||||
}
|
||||
using var mapper = _database.OpenConnection();
|
||||
mapper.Execute(@"DELETE FROM ""MetadataFiles""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""MetadataFiles"".""Id"" FROM ""MetadataFiles""
|
||||
LEFT OUTER JOIN ""MovieFiles""
|
||||
ON ""MetadataFiles"".""MovieFileId"" = ""MovieFiles"".""Id""
|
||||
WHERE ""MetadataFiles"".""MovieFileId"" > 0
|
||||
AND ""MovieFiles"".""Id"" IS NULL)");
|
||||
}
|
||||
|
||||
private void DeleteWhereMovieFileIsZero()
|
||||
{
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM ""MetadataFiles""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""Id"" FROM ""MetadataFiles""
|
||||
WHERE ""Type"" IN (1, 2)
|
||||
AND ""MovieFileId"" = 0)");
|
||||
}
|
||||
using var mapper = _database.OpenConnection();
|
||||
mapper.Execute(@"DELETE FROM ""MetadataFiles""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""Id"" FROM ""MetadataFiles""
|
||||
WHERE ""Type"" IN (1, 2)
|
||||
AND ""MovieFileId"" = 0)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,15 +14,13 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
|
||||
|
||||
public void Clean()
|
||||
{
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM ""MovieFiles""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""MovieFiles"".""Id"" FROM ""MovieFiles""
|
||||
LEFT OUTER JOIN ""Movies""
|
||||
ON ""MovieFiles"".""Id"" = ""Movies"".""MovieFileId""
|
||||
WHERE ""Movies"".""Id"" IS NULL)");
|
||||
}
|
||||
using var mapper = _database.OpenConnection();
|
||||
mapper.Execute(@"DELETE FROM ""MovieFiles""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""MovieFiles"".""Id"" FROM ""MovieFiles""
|
||||
LEFT OUTER JOIN ""Movies""
|
||||
ON ""MovieFiles"".""Id"" = ""Movies"".""MovieFileId""
|
||||
WHERE ""Movies"".""Id"" IS NULL)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,16 +14,14 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
|
||||
|
||||
public void Clean()
|
||||
{
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM ""MovieMetadata""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""MovieMetadata"".""Id"" FROM ""MovieMetadata""
|
||||
LEFT OUTER JOIN ""Movies"" ON ""Movies"".""MovieMetadataId"" = ""MovieMetadata"".""Id""
|
||||
LEFT OUTER JOIN ""Collections"" ON ""Collections"".""TmdbId"" = ""MovieMetadata"".""CollectionTmdbId""
|
||||
LEFT OUTER JOIN ""ImportListMovies"" ON ""ImportListMovies"".""MovieMetadataId"" = ""MovieMetadata"".""Id""
|
||||
WHERE ""Movies"".""Id"" IS NULL AND ""ImportListMovies"".""Id"" IS NULL AND ""Collections"".""Id"" IS NULL)");
|
||||
}
|
||||
using var mapper = _database.OpenConnection();
|
||||
mapper.Execute(@"DELETE FROM ""MovieMetadata""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""MovieMetadata"".""Id"" FROM ""MovieMetadata""
|
||||
LEFT OUTER JOIN ""Movies"" ON ""Movies"".""MovieMetadataId"" = ""MovieMetadata"".""Id""
|
||||
LEFT OUTER JOIN ""Collections"" ON ""Collections"".""TmdbId"" = ""MovieMetadata"".""CollectionTmdbId""
|
||||
LEFT OUTER JOIN ""ImportListMovies"" ON ""ImportListMovies"".""MovieMetadataId"" = ""MovieMetadata"".""Id""
|
||||
WHERE ""Movies"".""Id"" IS NULL AND ""ImportListMovies"".""Id"" IS NULL AND ""Collections"".""Id"" IS NULL)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,16 +14,14 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
|
||||
|
||||
public void Clean()
|
||||
{
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"UPDATE ""Movies""
|
||||
SET ""MovieFileId"" = 0
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""Movies"".""Id"" FROM ""Movies""
|
||||
LEFT OUTER JOIN ""MovieFiles""
|
||||
ON ""Movies"".""MovieFileId"" = ""MovieFiles"".""Id""
|
||||
WHERE ""MovieFiles"".""Id"" IS NULL)");
|
||||
}
|
||||
using var mapper = _database.OpenConnection();
|
||||
mapper.Execute(@"UPDATE ""Movies""
|
||||
SET ""MovieFileId"" = 0
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""Movies"".""Id"" FROM ""Movies""
|
||||
LEFT OUTER JOIN ""MovieFiles""
|
||||
ON ""Movies"".""MovieFileId"" = ""MovieFiles"".""Id""
|
||||
WHERE ""MovieFiles"".""Id"" IS NULL)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,15 +14,13 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
|
||||
|
||||
public void Clean()
|
||||
{
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM ""MovieTranslations""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""MovieTranslations"".""Id"" FROM ""MovieTranslations""
|
||||
LEFT OUTER JOIN ""MovieMetadata""
|
||||
ON ""MovieTranslations"".""MovieMetadataId"" = ""MovieMetadata"".""Id""
|
||||
WHERE ""MovieMetadata"".""Id"" IS NULL)");
|
||||
}
|
||||
using var mapper = _database.OpenConnection();
|
||||
mapper.Execute(@"DELETE FROM ""MovieTranslations""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""MovieTranslations"".""Id"" FROM ""MovieTranslations""
|
||||
LEFT OUTER JOIN ""MovieMetadata""
|
||||
ON ""MovieTranslations"".""MovieMetadataId"" = ""MovieMetadata"".""Id""
|
||||
WHERE ""MovieMetadata"".""Id"" IS NULL)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,15 +14,13 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
|
||||
|
||||
public void Clean()
|
||||
{
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM ""PendingReleases""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""PendingReleases"".""Id"" FROM ""PendingReleases""
|
||||
LEFT OUTER JOIN ""Movies""
|
||||
ON ""PendingReleases"".""MovieId"" = ""Movies"".""Id""
|
||||
WHERE ""Movies"".""Id"" IS NULL)");
|
||||
}
|
||||
using var mapper = _database.OpenConnection();
|
||||
mapper.Execute(@"DELETE FROM ""PendingReleases""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""PendingReleases"".""Id"" FROM ""PendingReleases""
|
||||
LEFT OUTER JOIN ""Movies""
|
||||
ON ""PendingReleases"".""MovieId"" = ""Movies"".""Id""
|
||||
WHERE ""Movies"".""Id"" IS NULL)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,29 +20,25 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
|
||||
|
||||
private void DeleteOrphanedByMovie()
|
||||
{
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM ""SubtitleFiles""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""SubtitleFiles"".""Id"" FROM ""SubtitleFiles""
|
||||
LEFT OUTER JOIN ""Movies""
|
||||
ON ""SubtitleFiles"".""MovieId"" = ""Movies"".""Id""
|
||||
WHERE ""Movies"".""Id"" IS NULL)");
|
||||
}
|
||||
using var mapper = _database.OpenConnection();
|
||||
mapper.Execute(@"DELETE FROM ""SubtitleFiles""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""SubtitleFiles"".""Id"" FROM ""SubtitleFiles""
|
||||
LEFT OUTER JOIN ""Movies""
|
||||
ON ""SubtitleFiles"".""MovieId"" = ""Movies"".""Id""
|
||||
WHERE ""Movies"".""Id"" IS NULL)");
|
||||
}
|
||||
|
||||
private void DeleteOrphanedByMovieFile()
|
||||
{
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM ""SubtitleFiles""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""SubtitleFiles"".""Id"" FROM ""SubtitleFiles""
|
||||
LEFT OUTER JOIN ""MovieFiles""
|
||||
ON ""SubtitleFiles"".""MovieFileId"" = ""MovieFiles"".""Id""
|
||||
WHERE ""SubtitleFiles"".""MovieFileId"" > 0
|
||||
AND ""MovieFiles"".""Id"" IS NULL)");
|
||||
}
|
||||
using var mapper = _database.OpenConnection();
|
||||
mapper.Execute(@"DELETE FROM ""SubtitleFiles""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""SubtitleFiles"".""Id"" FROM ""SubtitleFiles""
|
||||
LEFT OUTER JOIN ""MovieFiles""
|
||||
ON ""SubtitleFiles"".""MovieFileId"" = ""MovieFiles"".""Id""
|
||||
WHERE ""SubtitleFiles"".""MovieFileId"" > 0
|
||||
AND ""MovieFiles"".""Id"" IS NULL)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,7 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
|
||||
|
||||
public void Clean()
|
||||
{
|
||||
var mapper = _database.OpenConnection();
|
||||
|
||||
using var mapper = _database.OpenConnection();
|
||||
var usedTags = new[] { "Movies", "Notifications", "DelayProfiles", "Restrictions", "ImportLists", "Indexers" }
|
||||
.SelectMany(v => GetUsedTags(v, mapper))
|
||||
.Distinct()
|
||||
|
||||
@@ -24,13 +24,11 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
|
||||
_logger.Debug("Not running scheduled task last execution cleanup during debug");
|
||||
}
|
||||
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"UPDATE ""ScheduledTasks""
|
||||
SET ""LastExecution"" = @time
|
||||
WHERE ""LastExecution"" > @time",
|
||||
new { time = DateTime.UtcNow });
|
||||
}
|
||||
using var mapper = _database.OpenConnection();
|
||||
mapper.Execute(@"UPDATE ""ScheduledTasks""
|
||||
SET ""LastExecution"" = @time
|
||||
WHERE ""LastExecution"" > @time",
|
||||
new { time = DateTime.UtcNow });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,11 +95,11 @@ namespace NzbDrone.Core.ImportLists.Radarr
|
||||
return new
|
||||
{
|
||||
options = devices.OrderBy(d => d.Name, StringComparer.InvariantCultureIgnoreCase)
|
||||
.Select(d => new
|
||||
{
|
||||
Value = d.Id,
|
||||
Name = d.Name
|
||||
})
|
||||
.Select(d => new
|
||||
{
|
||||
Value = d.Id,
|
||||
Name = d.Name
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
@@ -110,23 +110,21 @@ namespace NzbDrone.Core.ImportLists.Radarr
|
||||
return new
|
||||
{
|
||||
options = devices.OrderBy(d => d.Label, StringComparer.InvariantCultureIgnoreCase)
|
||||
.Select(d => new
|
||||
{
|
||||
Value = d.Id,
|
||||
Name = d.Label
|
||||
})
|
||||
.Select(d => new
|
||||
{
|
||||
Value = d.Id,
|
||||
Name = d.Label
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
if (action == "getRootFolders")
|
||||
{
|
||||
Settings.Validate().Filter("ApiKey").ThrowOnError();
|
||||
|
||||
var remoteRootfolders = _radarrV3Proxy.GetRootFolders(Settings);
|
||||
var remoteRootFolders = _radarrV3Proxy.GetRootFolders(Settings);
|
||||
|
||||
return new
|
||||
{
|
||||
options = remoteRootfolders.OrderBy(d => d.Path, StringComparer.InvariantCultureIgnoreCase)
|
||||
options = remoteRootFolders.OrderBy(d => d.Path, StringComparer.InvariantCultureIgnoreCase)
|
||||
.Select(d => new
|
||||
{
|
||||
Value = d.Path,
|
||||
|
||||
@@ -63,6 +63,12 @@ namespace NzbDrone.Core.ImportLists.Radarr
|
||||
return new ValidationFailure("ApiKey", "API Key is invalid");
|
||||
}
|
||||
|
||||
if (ex.Response.HasHttpRedirect)
|
||||
{
|
||||
_logger.Error(ex, "Radarr returned redirect and is invalid");
|
||||
return new ValidationFailure("BaseUrl", "Radarr URL is invalid, are you missing a URL base?");
|
||||
}
|
||||
|
||||
_logger.Error(ex, "Unable to connect to import list.");
|
||||
return new ValidationFailure(string.Empty, $"Unable to connect to import list: {ex.Message}. Check the log surrounding this error for details.");
|
||||
}
|
||||
@@ -84,11 +90,18 @@ namespace NzbDrone.Core.ImportLists.Radarr
|
||||
|
||||
var baseUrl = settings.BaseUrl.TrimEnd('/');
|
||||
|
||||
var request = new HttpRequestBuilder(baseUrl).Resource(resource).Accept(HttpAccept.Json)
|
||||
.SetHeader("X-Api-Key", settings.ApiKey).Build();
|
||||
var request = new HttpRequestBuilder(baseUrl).Resource(resource)
|
||||
.Accept(HttpAccept.Json)
|
||||
.SetHeader("X-Api-Key", settings.ApiKey)
|
||||
.Build();
|
||||
|
||||
var response = _httpClient.Get(request);
|
||||
|
||||
if ((int)response.StatusCode >= 300)
|
||||
{
|
||||
throw new HttpException(response);
|
||||
}
|
||||
|
||||
var results = JsonConvert.DeserializeObject<List<TResource>>(response.Content);
|
||||
|
||||
return results;
|
||||
|
||||
@@ -1,30 +1,58 @@
|
||||
using NzbDrone.Core.Annotations;
|
||||
|
||||
namespace NzbDrone.Core.ImportLists.TMDb
|
||||
{
|
||||
public enum TMDbLanguageCodes
|
||||
{
|
||||
[FieldOption(Hint = "Danish")]
|
||||
da,
|
||||
[FieldOption(Hint = "Dutch")]
|
||||
nl,
|
||||
[FieldOption(Hint = "English")]
|
||||
en,
|
||||
[FieldOption(Hint = "Finnish")]
|
||||
fi,
|
||||
[FieldOption(Hint = "French")]
|
||||
fr,
|
||||
[FieldOption(Hint = "German")]
|
||||
de,
|
||||
[FieldOption(Hint = "Greek")]
|
||||
el,
|
||||
[FieldOption(Hint = "Hungarian")]
|
||||
hu,
|
||||
[FieldOption(Hint = "Italian")]
|
||||
it,
|
||||
[FieldOption(Hint = "Japanese")]
|
||||
ja,
|
||||
[FieldOption(Hint = "Korean")]
|
||||
ko,
|
||||
[FieldOption(Hint = "Norwegian")]
|
||||
no,
|
||||
[FieldOption(Hint = "Polish")]
|
||||
pl,
|
||||
[FieldOption(Hint = "Portuguese")]
|
||||
pt,
|
||||
[FieldOption(Hint = "Russian")]
|
||||
ru,
|
||||
[FieldOption(Hint = "Spanish")]
|
||||
es,
|
||||
[FieldOption(Hint = "Swedish")]
|
||||
sv,
|
||||
[FieldOption(Hint = "Turkish")]
|
||||
tr,
|
||||
[FieldOption(Hint = "Vietnamese")]
|
||||
vi,
|
||||
[FieldOption(Hint = "Chinese")]
|
||||
zh,
|
||||
[FieldOption(Hint = "Tamil")]
|
||||
ta,
|
||||
[FieldOption(Hint = "Telugu")]
|
||||
te,
|
||||
[FieldOption(Hint = "Hindi")]
|
||||
hi,
|
||||
bn
|
||||
[FieldOption(Hint = "Bengali")]
|
||||
bn,
|
||||
[FieldOption(Hint = "Romanian")]
|
||||
ro
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,9 +39,8 @@ namespace NzbDrone.Core.ImportLists.TMDb
|
||||
Title = movieResult.Title,
|
||||
};
|
||||
|
||||
if (movieResult.ReleaseDate.IsNotNullOrWhiteSpace())
|
||||
if (movieResult.ReleaseDate.IsNotNullOrWhiteSpace() && DateTime.TryParse(movieResult.ReleaseDate, out var releaseDate))
|
||||
{
|
||||
DateTime.TryParse(movieResult.ReleaseDate, out var releaseDate);
|
||||
movie.Year = releaseDate.Year;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using FluentValidation.Results;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
@@ -94,6 +96,7 @@ namespace NzbDrone.Core.Indexers
|
||||
{
|
||||
var releases = new List<ReleaseInfo>();
|
||||
var url = string.Empty;
|
||||
var minimumBackoff = TimeSpan.FromHours(1);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -190,8 +193,7 @@ namespace NzbDrone.Core.Indexers
|
||||
}
|
||||
catch (WebException webException)
|
||||
{
|
||||
if (webException.Status == WebExceptionStatus.NameResolutionFailure ||
|
||||
webException.Status == WebExceptionStatus.ConnectFailure)
|
||||
if (webException.Status is WebExceptionStatus.NameResolutionFailure or WebExceptionStatus.ConnectFailure)
|
||||
{
|
||||
_indexerStatusService.RecordConnectionFailure(Definition.Id);
|
||||
}
|
||||
@@ -201,7 +203,7 @@ namespace NzbDrone.Core.Indexers
|
||||
}
|
||||
|
||||
if (webException.Message.Contains("502") || webException.Message.Contains("503") ||
|
||||
webException.Message.Contains("timed out"))
|
||||
webException.Message.Contains("504") || webException.Message.Contains("timed out"))
|
||||
{
|
||||
_logger.Warn("{0} server is currently unavailable. {1} {2}", this, url, webException.Message);
|
||||
}
|
||||
@@ -212,34 +214,29 @@ namespace NzbDrone.Core.Indexers
|
||||
}
|
||||
catch (TooManyRequestsException ex)
|
||||
{
|
||||
if (ex.RetryAfter != TimeSpan.Zero)
|
||||
{
|
||||
_indexerStatusService.RecordFailure(Definition.Id, ex.RetryAfter);
|
||||
}
|
||||
else
|
||||
{
|
||||
_indexerStatusService.RecordFailure(Definition.Id, TimeSpan.FromHours(1));
|
||||
}
|
||||
var retryTime = ex.RetryAfter != TimeSpan.Zero ? ex.RetryAfter : minimumBackoff;
|
||||
_indexerStatusService.RecordFailure(Definition.Id, retryTime);
|
||||
|
||||
_logger.Warn("API Request Limit reached for {0}", this);
|
||||
_logger.Warn("API Request Limit reached for {0}. Disabled for {1}", this, retryTime);
|
||||
}
|
||||
catch (HttpException ex)
|
||||
{
|
||||
_indexerStatusService.RecordFailure(Definition.Id);
|
||||
_logger.Warn("{0} {1}", this, ex.Message);
|
||||
}
|
||||
catch (RequestLimitReachedException ex)
|
||||
{
|
||||
if (ex.RetryAfter != TimeSpan.Zero)
|
||||
if (ex.Response.HasHttpServerError)
|
||||
{
|
||||
_indexerStatusService.RecordFailure(Definition.Id, ex.RetryAfter);
|
||||
_logger.Warn("Unable to connect to {0} at [{1}]. Indexer's server is unavailable. Try again later. {2}", this, url, ex.Message);
|
||||
}
|
||||
else
|
||||
{
|
||||
_indexerStatusService.RecordFailure(Definition.Id, TimeSpan.FromHours(1));
|
||||
_logger.Warn("{0} {1}", this, ex.Message);
|
||||
}
|
||||
}
|
||||
catch (RequestLimitReachedException ex)
|
||||
{
|
||||
var retryTime = ex.RetryAfter != TimeSpan.Zero ? ex.RetryAfter : minimumBackoff;
|
||||
_indexerStatusService.RecordFailure(Definition.Id, retryTime);
|
||||
|
||||
_logger.Warn("API Request Limit reached for {0}", this);
|
||||
_logger.Warn("API Request Limit reached for {0}. Disabled for {1}", this, retryTime);
|
||||
}
|
||||
catch (ApiKeyException)
|
||||
{
|
||||
@@ -259,6 +256,11 @@ namespace NzbDrone.Core.Indexers
|
||||
_logger.Error(ex, "CAPTCHA token required for {0}, check indexer settings.", this);
|
||||
}
|
||||
}
|
||||
catch (TaskCanceledException ex)
|
||||
{
|
||||
_indexerStatusService.RecordFailure(Definition.Id);
|
||||
_logger.Warn(ex, "Unable to connect to indexer, possibly due to a timeout. {0}", url);
|
||||
}
|
||||
catch (IndexerException ex)
|
||||
{
|
||||
_indexerStatusService.RecordFailure(Definition.Id);
|
||||
@@ -360,6 +362,8 @@ namespace NzbDrone.Core.Indexers
|
||||
catch (RequestLimitReachedException ex)
|
||||
{
|
||||
_logger.Warn("Request limit reached: " + ex.Message);
|
||||
|
||||
return new ValidationFailure(string.Empty, "Request limit reached: " + ex.Message);
|
||||
}
|
||||
catch (CloudFlareCaptchaException ex)
|
||||
{
|
||||
@@ -392,11 +396,45 @@ namespace NzbDrone.Core.Indexers
|
||||
_logger.Warn(ex, "Indexer does not support the query");
|
||||
return new ValidationFailure(string.Empty, "Indexer does not support the current query. Check if the categories and or searching for movies are supported. Check the log for more details.");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Warn(ex, "Unable to connect to indexer");
|
||||
|
||||
return new ValidationFailure(string.Empty, "Unable to connect to indexer. " + ex.Message);
|
||||
_logger.Warn(ex, "Unable to connect to indexer");
|
||||
if (ex.Response.HasHttpServerError)
|
||||
{
|
||||
return new ValidationFailure(string.Empty, "Unable to connect to indexer, indexer's server is unavailable. Try again later. " + ex.Message);
|
||||
}
|
||||
|
||||
if (ex.Response.StatusCode is HttpStatusCode.Forbidden or HttpStatusCode.Unauthorized)
|
||||
{
|
||||
return new ValidationFailure(string.Empty, "Unable to connect to indexer, invalid credentials. " + ex.Message);
|
||||
}
|
||||
|
||||
return new ValidationFailure(string.Empty, "Unable to connect to indexer, check the log above the ValidationFailure for more details. " + ex.Message);
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.Warn(ex, "Unable to connect to indexer");
|
||||
|
||||
return new ValidationFailure(string.Empty, "Unable to connect to indexer, please check your DNS settings and ensure IPv6 is working or disabled. " + ex.Message);
|
||||
}
|
||||
catch (TaskCanceledException ex)
|
||||
{
|
||||
_logger.Warn(ex, "Unable to connect to indexer");
|
||||
|
||||
return new ValidationFailure(string.Empty, "Unable to connect to indexer, possibly due to a timeout. Try again or check your network settings. " + ex.Message);
|
||||
}
|
||||
catch (WebException webException)
|
||||
{
|
||||
_logger.Warn("Unable to connect to indexer.");
|
||||
|
||||
if (webException.Status is WebExceptionStatus.NameResolutionFailure or WebExceptionStatus.ConnectFailure)
|
||||
{
|
||||
return new ValidationFailure(string.Empty, "Unable to connect to indexer connection failure. Check your connection to the indexer's server and DNS." + webException.Message);
|
||||
}
|
||||
|
||||
if (webException.Message.Contains("502") || webException.Message.Contains("503") ||
|
||||
webException.Message.Contains("504") || webException.Message.Contains("timed out"))
|
||||
{
|
||||
return new ValidationFailure(string.Empty, "Unable to connect to indexer, indexer's server is unavailable. Try again later. " + webException.Message);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NLog;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Exceptions;
|
||||
using NzbDrone.Core.Http.CloudFlare;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Rarbg
|
||||
{
|
||||
public class Rarbg : HttpIndexerBase<RarbgSettings>
|
||||
{
|
||||
private readonly IRarbgTokenProvider _tokenProvider;
|
||||
|
||||
public override string Name => "Rarbg";
|
||||
|
||||
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
|
||||
public override TimeSpan RateLimit => TimeSpan.FromSeconds(4);
|
||||
|
||||
public Rarbg(IRarbgTokenProvider tokenProvider, IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger)
|
||||
: base(httpClient, indexerStatusService, configService, parsingService, logger)
|
||||
{
|
||||
_tokenProvider = tokenProvider;
|
||||
}
|
||||
|
||||
public override IIndexerRequestGenerator GetRequestGenerator()
|
||||
{
|
||||
return new RarbgRequestGenerator(_tokenProvider) { Settings = Settings };
|
||||
}
|
||||
|
||||
public override IParseIndexerResponse GetParser()
|
||||
{
|
||||
return new RarbgParser();
|
||||
}
|
||||
|
||||
public override object RequestAction(string action, IDictionary<string, string> query)
|
||||
{
|
||||
if (action == "checkCaptcha")
|
||||
{
|
||||
Settings.Validate().Filter("BaseUrl").ThrowOnError();
|
||||
|
||||
try
|
||||
{
|
||||
var request = new HttpRequestBuilder(Settings.BaseUrl.Trim('/'))
|
||||
.Resource($"/pubapi_v2.php?get_token=get_token&app_id={BuildInfo.AppName}")
|
||||
.Accept(HttpAccept.Json)
|
||||
.Build();
|
||||
|
||||
_httpClient.Get(request);
|
||||
}
|
||||
catch (CloudFlareCaptchaException ex)
|
||||
{
|
||||
return new
|
||||
{
|
||||
captchaRequest = new
|
||||
{
|
||||
host = ex.CaptchaRequest.Host,
|
||||
ray = ex.CaptchaRequest.Ray,
|
||||
siteKey = ex.CaptchaRequest.SiteKey,
|
||||
secretToken = ex.CaptchaRequest.SecretToken,
|
||||
responseUrl = ex.CaptchaRequest.ResponseUrl.FullUri,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return new
|
||||
{
|
||||
captchaToken = ""
|
||||
};
|
||||
}
|
||||
else if (action == "getCaptchaCookie")
|
||||
{
|
||||
if (query["responseUrl"].IsNullOrWhiteSpace())
|
||||
{
|
||||
throw new BadRequestException("QueryParam responseUrl invalid.");
|
||||
}
|
||||
|
||||
if (query["ray"].IsNullOrWhiteSpace())
|
||||
{
|
||||
throw new BadRequestException("QueryParam ray invalid.");
|
||||
}
|
||||
|
||||
if (query["captchaResponse"].IsNullOrWhiteSpace())
|
||||
{
|
||||
throw new BadRequestException("QueryParam captchaResponse invalid.");
|
||||
}
|
||||
|
||||
var request = new HttpRequestBuilder(query["responseUrl"])
|
||||
.AddQueryParam("id", query["ray"])
|
||||
.AddQueryParam("g-recaptcha-response", query["captchaResponse"])
|
||||
.Build();
|
||||
|
||||
request.UseSimplifiedUserAgent = true;
|
||||
request.AllowAutoRedirect = false;
|
||||
|
||||
var response = _httpClient.Get(request);
|
||||
|
||||
var cfClearanceCookie = response.GetCookies()["cf_clearance"];
|
||||
|
||||
return new
|
||||
{
|
||||
captchaToken = cfClearanceCookie
|
||||
};
|
||||
}
|
||||
|
||||
return new { };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Text.RegularExpressions;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Indexers.Exceptions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Rarbg
|
||||
{
|
||||
public class RarbgParser : IParseIndexerResponse
|
||||
{
|
||||
private static readonly Regex RegexGuid = new Regex(@"^magnet:\?xt=urn:btih:([a-f0-9]+)", RegexOptions.Compiled);
|
||||
|
||||
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
||||
{
|
||||
var results = new List<ReleaseInfo>();
|
||||
|
||||
switch (indexerResponse.HttpResponse.StatusCode)
|
||||
{
|
||||
case HttpStatusCode.TooManyRequests:
|
||||
throw new RequestLimitReachedException("Indexer API limit reached", TimeSpan.FromMinutes(2));
|
||||
case (HttpStatusCode)520:
|
||||
throw new RequestLimitReachedException("Indexer API error. Likely rate limited by origin server", TimeSpan.FromMinutes(3));
|
||||
default:
|
||||
if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK)
|
||||
{
|
||||
throw new IndexerException(indexerResponse, "Indexer API call returned an unexpected status code [{0}]", indexerResponse.HttpResponse.StatusCode);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
var jsonResponse = new HttpResponse<RarbgResponse>(indexerResponse.HttpResponse);
|
||||
|
||||
if (jsonResponse.Resource.error_code.HasValue)
|
||||
{
|
||||
if (jsonResponse.Resource.error_code == 5 || jsonResponse.Resource.error_code == 8
|
||||
|| jsonResponse.Resource.error_code == 9 || jsonResponse.Resource.error_code == 10
|
||||
|| jsonResponse.Resource.error_code == 13 || jsonResponse.Resource.error_code == 14
|
||||
|| jsonResponse.Resource.error_code == 20)
|
||||
{
|
||||
// No results, rate limit, tmdbid not found/invalid, or imdbid not found/invalid
|
||||
return results;
|
||||
}
|
||||
|
||||
throw new IndexerException(indexerResponse, "Indexer API call returned error {0}: {1}", jsonResponse.Resource.error_code, jsonResponse.Resource.error);
|
||||
}
|
||||
|
||||
if (jsonResponse.Resource.torrent_results == null)
|
||||
{
|
||||
// Despite this being the requested behaviour it appears to be problematic, commenting it out for now
|
||||
// if (jsonResponse.Resource.rate_limit == 1)
|
||||
// {
|
||||
// throw new RequestLimitReachedException("Indexer API limit reached", TimeSpan.FromMinutes(5));
|
||||
// }
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
foreach (var torrent in jsonResponse.Resource.torrent_results)
|
||||
{
|
||||
var torrentInfo = new TorrentInfo();
|
||||
|
||||
torrentInfo.Guid = GetGuid(torrent);
|
||||
torrentInfo.Title = torrent.title;
|
||||
torrentInfo.Size = torrent.size;
|
||||
torrentInfo.DownloadUrl = torrent.download;
|
||||
torrentInfo.InfoUrl = $"{torrent.info_page}&app_id={BuildInfo.AppName}";
|
||||
torrentInfo.PublishDate = torrent.pubdate.ToUniversalTime();
|
||||
torrentInfo.Seeders = torrent.seeders;
|
||||
torrentInfo.Peers = torrent.leechers + torrent.seeders;
|
||||
|
||||
if (torrent.episode_info != null)
|
||||
{
|
||||
if (torrent.episode_info.imdb != null)
|
||||
{
|
||||
torrentInfo.ImdbId = int.Parse(torrent.episode_info.imdb.Substring(2));
|
||||
}
|
||||
|
||||
if (torrent.episode_info.themoviedb != null)
|
||||
{
|
||||
torrentInfo.TmdbId = torrent.episode_info.themoviedb.Value;
|
||||
}
|
||||
}
|
||||
|
||||
results.Add(torrentInfo);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
|
||||
private string GetGuid(RarbgTorrent torrent)
|
||||
{
|
||||
var match = RegexGuid.Match(torrent.download);
|
||||
|
||||
if (match.Success)
|
||||
{
|
||||
return string.Format("rarbg-{0}", match.Groups[1].Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
return string.Format("rarbg-{0}", torrent.download);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user