Compare commits

..

1 Commits

Author SHA1 Message Date
Mark McDowall
17f28a10d5 Fixed: Normalize unicode characters when comparing paths for equality
(cherry picked from commit ceeec091f85d0094e07537b7f62f18292655a710)
2024-11-15 03:02:58 +00:00
182 changed files with 1952 additions and 3704 deletions

View File

@@ -1,23 +1,3 @@
# Announcement: Retirement of Readarr
We would like to announce that the [Readarr project](<https://github.com/Readarr/Readarr>) has been retired. This difficult decision was made due to a combination of factors: the project's metadata has become unusable, we no longer have the time to remake or repair it, and the community effort to transition to using Open Library as the source has stalled without much progress.
Third-party metadata mirrors exist, but as we're not involved with them at all, we cannot provide support for them. Use of them is entirely at your own risk. The most popular mirror appears to be [rreading-glasses](<https://github.com/blampe/rreading-glasses>).
Without anyone to take over Readarr development, we expect it to wither away, so we still encourage you to seek alternatives to Readarr.
## Key Points:
- **Effective Immediately**: The retirement takes effect immediately. Please stay tuned for any possible further communications.
- **Support Window**: We will provide support during a brief transition period to help with troubleshooting non metadata related issues.
- **Alternative Solutions**: Users are encouraged to explore and adopt any other possible solutions as alternatives to Readarr.
- **Opportunities for Revival**: We are open to someone taking over and revitalizing the project. If you are interested, please get in touch.
- **Gratitude**: We extend our deepest gratitude to all the contributors and community members who supported Readarr over the years.
Thank you for being part of the Readarr journey. For any inquiries or assistance during this transition, please contact our team.
Sincerely,
The Servarr Team
# Readarr
[![Build Status](https://dev.azure.com/Readarr/Readarr/_apis/build/status/Readarr.Readarr?branchName=develop)](https://dev.azure.com/Readarr/Readarr/_build/latest?definitionId=1&branchName=develop)

View File

@@ -9,7 +9,7 @@ variables:
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '0.4.19'
majorVersion: '0.4.4'
minorVersion: $[counter('minorVersion', 1)]
readarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(readarrVersion)'
@@ -19,7 +19,7 @@ variables:
nodeVersion: '20.X'
innoVersion: '6.2.0'
windowsImage: 'windows-2022'
linuxImage: 'ubuntu-22.04'
linuxImage: 'ubuntu-20.04'
macImage: 'macOS-13'
trigger:
@@ -1102,19 +1102,19 @@ stages:
vmImage: ${{ variables.windowsImage }}
steps:
- checkout: self # Need history for Sonar analysis
- task: SonarCloudPrepare@3
- task: SonarCloudPrepare@2
env:
SONAR_SCANNER_OPTS: ''
inputs:
SonarCloud: 'SonarCloud'
organization: 'readarr'
scannerMode: 'cli'
scannerMode: 'CLI'
configMode: 'manual'
cliProjectKey: 'readarrui'
cliProjectName: 'ReadarrUI'
cliProjectVersion: '$(readarrVersion)'
cliSources: './frontend'
- task: SonarCloudAnalyze@3
- task: SonarCloudAnalyze@2
- job: Api_Docs
displayName: API Docs
@@ -1190,12 +1190,12 @@ stages:
submodules: true
- powershell: Set-Service SCardSvr -StartupType Manual
displayName: Enable Windows Test Service
- task: SonarCloudPrepare@3
- task: SonarCloudPrepare@2
condition: eq(variables['System.PullRequest.IsFork'], 'False')
inputs:
SonarCloud: 'SonarCloud'
organization: 'readarr'
scannerMode: 'dotnet'
scannerMode: 'MSBuild'
projectKey: 'Readarr_Readarr'
projectName: 'Readarr'
projectVersion: '$(readarrVersion)'
@@ -1208,10 +1208,10 @@ stages:
./build.sh --backend -f net6.0 -r win-x64
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
displayName: Coverage Unit Tests
- task: SonarCloudAnalyze@3
- task: SonarCloudAnalyze@2
condition: eq(variables['System.PullRequest.IsFork'], 'False')
displayName: Publish SonarCloud Results
- task: reportgenerator@5.3.11
- task: reportgenerator@5
displayName: Generate Coverage Report
inputs:
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'

View File

@@ -26,7 +26,6 @@ module.exports = (env) => {
const config = {
mode: isProduction ? 'production' : 'development',
devtool: isProduction ? 'source-map' : 'eval-source-map',
target: 'web',
stats: {
children: false
@@ -182,7 +181,7 @@ module.exports = (env) => {
loose: true,
debug: false,
useBuiltIns: 'entry',
corejs: '3.39'
corejs: 3
}
]
]

View File

@@ -165,8 +165,7 @@ function HistoryDetails(props) {
if (eventType === 'downloadFailed') {
const {
message,
indexer
message
} = data;
return (
@@ -178,21 +177,11 @@ function HistoryDetails(props) {
/>
{
indexer ?
<DescriptionListItem
title={translate('Indexer')}
data={indexer}
/> :
null
}
{
message ?
!!message &&
<DescriptionListItem
title={translate('Message')}
data={message}
/> :
null
/>
}
</DescriptionList>
);

View File

@@ -1,7 +1,5 @@
import ModelBase from 'App/ModelBase';
export type AuthorStatus = 'continuing' | 'ended';
interface Author extends ModelBase {
added: string;
genres: string[];
@@ -12,7 +10,6 @@ interface Author extends ModelBase {
metadataProfileId: number;
rootFolderPath: string;
sortName: string;
status: AuthorStatus;
tags: number[];
authorName: string;
isSaving?: boolean;

View File

@@ -1,21 +0,0 @@
import { AuthorStatus } from 'Author/Author';
import { icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
export function getAuthorStatusDetails(status: AuthorStatus) {
let statusDetails = {
icon: icons.AUTHOR_CONTINUING,
title: translate('StatusEndedContinuing'),
message: translate('ContinuingMoreBooksAreExpected'),
};
if (status === 'ended') {
statusDetails = {
icon: icons.AUTHOR_ENDED,
title: translate('StatusEndedEnded'),
message: translate('ContinuingNoAdditionalBooksAreExpected'),
};
}
return statusDetails;
}

View File

@@ -7,7 +7,6 @@ import AuthorHistoryTable from 'Author/History/AuthorHistoryTable';
import MonitoringOptionsModal from 'Author/MonitoringOptions/MonitoringOptionsModal';
import BookEditorFooter from 'Book/Editor/BookEditorFooter';
import BookFileEditorTable from 'BookFile/Editor/BookFileEditorTable';
import Alert from 'Components/Alert';
import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
@@ -18,7 +17,7 @@ import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import SwipeHeaderConnector from 'Components/Swipe/SwipeHeaderConnector';
import { align, icons, kinds } from 'Helpers/Props';
import { align, icons } from 'Helpers/Props';
import InteractiveSearchFilterMenuConnector from 'InteractiveSearch/InteractiveSearchFilterMenuConnector';
import InteractiveSearchTable from 'InteractiveSearch/InteractiveSearchTable';
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
@@ -413,25 +412,22 @@ class AuthorDetails extends Component {
<div className={styles.contentContainer}>
{
!isPopulated && !booksError && !bookFilesError ?
<LoadingIndicator /> :
null
!isPopulated && !booksError && !bookFilesError &&
<LoadingIndicator />
}
{
!isFetching && booksError ?
<Alert kind={kinds.DANGER}>
!isFetching && booksError &&
<div>
{translate('LoadingBooksFailed')}
</Alert> :
null
</div>
}
{
!isFetching && bookFilesError ?
<Alert kind={kinds.DANGER}>
!isFetching && bookFilesError &&
<div>
{translate('LoadingBookFilesFailed')}
</Alert> :
null
</div>
}
{

View File

@@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import TextTruncate from 'react-text-truncate';
import AuthorPoster from 'Author/AuthorPoster';
import { getAuthorStatusDetails } from 'Author/AuthorStatus';
import HeartRating from 'Components/HeartRating';
import Icon from 'Components/Icon';
import Label from 'Components/Label';
@@ -12,7 +11,7 @@ import MonitorToggleButton from 'Components/MonitorToggleButton';
import Popover from 'Components/Tooltip/Popover';
import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
import QualityProfileName from 'Settings/Profiles/Quality/QualityProfileName';
import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector';
import fonts from 'Styles/Variables/fonts';
import formatBytes from 'Utilities/Number/formatBytes';
import stripHtml from 'Utilities/String/stripHtml';
@@ -88,11 +87,11 @@ class AuthorDetailsHeader extends Component {
titleWidth
} = this.state;
const statusDetails = getAuthorStatusDetails(status);
const fanartUrl = getFanartUrl(images);
const marqueeWidth = titleWidth - (isSmallScreen ? 85 : 160);
const continuing = status === 'continuing';
let bookFilesCountMessage = translate('BookFilesCountMessage');
if (bookFileCount === 1) {
@@ -214,7 +213,7 @@ class AuthorDetailsHeader extends Component {
<span className={styles.qualityProfileName}>
{
<QualityProfileName
<QualityProfileNameConnector
qualityProfileId={qualityProfileId}
/>
}
@@ -237,16 +236,16 @@ class AuthorDetailsHeader extends Component {
<Label
className={styles.detailsLabel}
title={statusDetails.message}
title={continuing ? translate('ContinuingMoreBooksAreExpected') : translate('ContinuingNoAdditionalBooksAreExpected')}
size={sizes.LARGE}
>
<Icon
name={statusDetails.icon}
name={continuing ? icons.AUTHOR_CONTINUING : icons.AUTHOR_ENDED}
size={17}
/>
<span className={styles.qualityProfileName}>
{statusDetails.title}
{continuing ? 'Continuing' : 'Deceased'}
</span>
</Label>

View File

@@ -1,6 +1,5 @@
import PropTypes from 'prop-types';
import React from 'react';
import { getAuthorStatusDetails } from 'Author/AuthorStatus';
import Icon from 'Components/Icon';
import VirtualTableRowCell from 'Components/Table/Cells/TableRowCell';
import { icons } from 'Helpers/Props';
@@ -16,8 +15,6 @@ function AuthorStatusCell(props) {
...otherProps
} = props;
const statusDetails = getAuthorStatusDetails(status);
return (
<Component
className={className}
@@ -31,8 +28,8 @@ function AuthorStatusCell(props) {
<Icon
className={styles.statusIcon}
name={statusDetails.icon}
title={`${statusDetails.title}: ${statusDetails.message}`}
name={status === 'ended' ? icons.AUTHOR_ENDED : icons.AUTHOR_CONTINUING}
title={status === 'ended' ? translate('StatusEndedDeceased') : translate('StatusEndedContinuing')}
/>
</Component>
);

View File

@@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
import React from 'react';
import Button from 'Components/Link/Button';
import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './NoAuthor.css';
function NoAuthor(props) {
@@ -32,7 +31,7 @@ function NoAuthor(props) {
to="/settings/mediamanagement"
kind={kinds.PRIMARY}
>
{translate('AddRootFolder')}
Add Root Folder
</Button>
</div>
@@ -41,7 +40,7 @@ function NoAuthor(props) {
to="/add/search"
kind={kinds.PRIMARY}
>
{translate('AddNewAuthor')}
Add New Author
</Button>
</div>
</div>

View File

@@ -1,11 +1,12 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import AuthorNameLink from 'Author/AuthorNameLink';
import { getAuthorStatusDetails } from 'Author/AuthorStatus';
import Icon from 'Components/Icon';
import MonitorToggleButton from 'Components/MonitorToggleButton';
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
import { icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import BookshelfBook from './BookshelfBook';
import styles from './BookshelfRow.css';
@@ -29,8 +30,6 @@ class BookshelfRow extends Component {
onBookMonitoredPress
} = this.props;
const statusDetails = getAuthorStatusDetails(status);
return (
<>
<VirtualTableSelectCell
@@ -53,8 +52,8 @@ class BookshelfRow extends Component {
<VirtualTableRowCell className={styles.status}>
<Icon
className={styles.statusIcon}
name={statusDetails.icon}
title={statusDetails.title}
name={status === 'ended' ? icons.AUTHOR_ENDED : icons.AUTHOR_CONTINUING}
title={status === 'ended' ? translate('StatusEndedEnded') : translate('StatusEndedContinuing')}
/>
</VirtualTableRowCell>

View File

@@ -20,8 +20,6 @@ import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue';
import TextInput from './TextInput';
import styles from './EnhancedSelectInput.css';
const MINIMUM_DISTANCE_FROM_EDGE = 10;
function isArrowKey(keyCode) {
return keyCode === keyCodes.UP_ARROW || keyCode === keyCodes.DOWN_ARROW;
}
@@ -139,9 +137,18 @@ class EnhancedSelectInput extends Component {
// Listeners
onComputeMaxHeight = (data) => {
const {
top,
bottom
} = data.offsets.reference;
const windowHeight = window.innerHeight;
data.styles.maxHeight = windowHeight - MINIMUM_DISTANCE_FROM_EDGE;
if ((/^botton/).test(data.placement)) {
data.styles.maxHeight = windowHeight - bottom;
} else {
data.styles.maxHeight = top;
}
return data;
};
@@ -450,10 +457,6 @@ class EnhancedSelectInput extends Component {
order: 851,
enabled: true,
fn: this.onComputeMaxHeight
},
preventOverflow: {
enabled: true,
boundariesElement: 'viewport'
}
}}
>

View File

@@ -28,7 +28,8 @@ function createMapStateToProps() {
if (includeNoChange) {
values.unshift({
key: 'noChange',
value: translate('NoChange'),
value: '',
name: translate('NoChange'),
isDisabled: includeNoChangeDisabled,
isMissing: false
});
@@ -38,6 +39,7 @@ function createMapStateToProps() {
values.push({
key: '',
value: '',
name: '',
isDisabled: true,
isHidden: true
});
@@ -54,7 +56,8 @@ function createMapStateToProps() {
values.push({
key: ADD_NEW_KEY,
value: 'Add a new path'
value: '',
name: 'Add a new path'
});
return {
@@ -102,27 +105,6 @@ class RootFolderSelectInputConnector extends Component {
}
}
componentDidUpdate(prevProps) {
const {
name,
value,
values,
onChange
} = this.props;
if (prevProps.values === values) {
return;
}
if (!value && values.length && values.some((v) => !!v.key && v.key !== ADD_NEW_KEY)) {
const defaultValue = values[0];
if (defaultValue.key !== ADD_NEW_KEY) {
onChange({ name, value: defaultValue.key });
}
}
}
//
// Render

View File

@@ -13,15 +13,6 @@
}
}
.value {
display: flex;
}
.authorFolder {
flex: 0 0 auto;
color: var(--disabledColor);
}
.freeSpace {
margin-left: 15px;
color: var(--darkGray);

View File

@@ -1,12 +1,10 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'authorFolder': string;
'freeSpace': string;
'isMissing': string;
'isMobile': string;
'optionText': string;
'value': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -7,24 +7,18 @@ import styles from './RootFolderSelectInputOption.css';
function RootFolderSelectInputOption(props) {
const {
id,
value,
name,
freeSpace,
authorFolder,
isMissing,
isMobile,
isWindows,
...otherProps
} = props;
const slashCharacter = isWindows ? '\\' : '/';
const text = name === '' ? value : `[${name}] ${value}`;
const text = value === '' ? name : `${name} [${value}]`;
return (
<EnhancedSelectInputOption
id={id}
isMobile={isMobile}
{...otherProps}
>
@@ -33,18 +27,7 @@ function RootFolderSelectInputOption(props) {
isMobile && styles.isMobile
)}
>
<div className={styles.value}>
{text}
{
authorFolder && id !== 'addNew' ?
<div className={styles.authorFolder}>
{slashCharacter}
{authorFolder}
</div> :
null
}
</div>
<div>{text}</div>
{
freeSpace == null ?
@@ -67,18 +50,11 @@ function RootFolderSelectInputOption(props) {
}
RootFolderSelectInputOption.propTypes = {
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
freeSpace: PropTypes.number,
authorFolder: PropTypes.string,
isMissing: PropTypes.bool,
isMobile: PropTypes.bool.isRequired,
isWindows: PropTypes.bool
};
RootFolderSelectInputOption.defaultProps = {
name: ''
isMobile: PropTypes.bool.isRequired
};
export default RootFolderSelectInputOption;

View File

@@ -7,20 +7,10 @@
overflow: hidden;
}
.pathContainer {
@add-mixin truncate;
display: flex;
flex: 1 0 0;
}
.path {
flex: 0 1 auto;
}
.authorFolder {
@add-mixin truncate;
flex: 0 1 auto;
color: var(--disabledColor);
flex: 1 0 0;
}
.freeSpace {

View File

@@ -1,10 +1,8 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'authorFolder': string;
'freeSpace': string;
'path': string;
'pathContainer': string;
'selectedValue': string;
}
export const cssExports: CssExports;

View File

@@ -9,34 +9,19 @@ function RootFolderSelectInputSelectedValue(props) {
name,
value,
freeSpace,
authorFolder,
includeFreeSpace,
isWindows,
...otherProps
} = props;
const slashCharacter = isWindows ? '\\' : '/';
const text = name === '' ? value : `[${name}] ${value}`;
const text = value === '' ? name : `${name} [${value}]`;
return (
<EnhancedSelectInputSelectedValue
className={styles.selectedValue}
{...otherProps}
>
<div className={styles.pathContainer}>
<div className={styles.path}>
{text}
</div>
{
authorFolder ?
<div className={styles.authorFolder}>
{slashCharacter}
{authorFolder}
</div> :
null
}
<div className={styles.path}>
{text}
</div>
{
@@ -53,13 +38,10 @@ RootFolderSelectInputSelectedValue.propTypes = {
name: PropTypes.string,
value: PropTypes.string,
freeSpace: PropTypes.number,
authorFolder: PropTypes.string,
isWindows: PropTypes.bool,
includeFreeSpace: PropTypes.bool.isRequired
};
RootFolderSelectInputSelectedValue.defaultProps = {
name: '',
includeFreeSpace: true
};

View File

@@ -52,7 +52,6 @@ class SelectInput extends Component {
const {
key,
value: optionValue,
isDisabled: optionIsDisabled = false,
...otherOptionProps
} = option;
@@ -60,7 +59,6 @@ class SelectInput extends Component {
<option
key={key}
value={key}
disabled={optionIsDisabled}
{...otherOptionProps}
>
{typeof optionValue === 'function' ? optionValue() : optionValue}

View File

@@ -83,6 +83,13 @@
}
@media only screen and (max-width: $breakpointMedium) {
.modal.small,
.modal.medium {
width: 90%;
}
}
@media only screen and (max-width: $breakpointSmall) {
.modalContainer {
position: fixed;
}

View File

@@ -4,7 +4,7 @@
line-height: 1.52857143;
}
@media only screen and (max-width: $breakpointMedium) {
@media only screen and (max-width: $breakpointSmall) {
.cell {
white-space: nowrap;
}

View File

@@ -7,7 +7,7 @@
white-space: nowrap;
}
@media only screen and (max-width: $breakpointMedium) {
@media only screen and (max-width: $breakpointSmall) {
.cell {
white-space: nowrap;
}

View File

@@ -10,7 +10,7 @@
border-collapse: collapse;
}
@media only screen and (max-width: $breakpointMedium) {
@media only screen and (max-width: $breakpointSmall) {
.tableContainer {
min-width: 100%;
width: fit-content;

View File

@@ -9,7 +9,7 @@
margin-left: 10px;
}
@media only screen and (max-width: $breakpointMedium) {
@media only screen and (max-width: $breakpointSmall) {
.headerCell {
white-space: nowrap;
}

View File

@@ -60,7 +60,7 @@
height: 25px;
}
@media only screen and (max-width: $breakpointMedium) {
@media only screen and (max-width: $breakpointSmall) {
.pager {
flex-wrap: wrap;
}

View File

@@ -9,7 +9,7 @@
margin-left: 10px;
}
@media only screen and (max-width: $breakpointMedium) {
@media only screen and (max-width: $breakpointSmall) {
.headerCell {
white-space: nowrap;
}

View File

@@ -35,12 +35,11 @@
.message {
margin-top: 30px;
text-align: center;
font-weight: 300;
font-size: $largeFontSize;
}
.helpText {
margin-bottom: 10px;
font-weight: 300;
font-size: 24px;
}

View File

@@ -82,8 +82,7 @@ class AddNewItem extends Component {
render() {
const {
error,
items,
hasExistingAuthors
items
} = this.props;
const term = this.state.term;
@@ -187,8 +186,7 @@ class AddNewItem extends Component {
}
{
term ?
null :
!term &&
<div className={styles.message}>
<div className={styles.helpText}>
{translate('ItsEasyToAddANewAuthorOrBookJustStartTypingTheNameOfTheItemYouWantToAdd')}
@@ -201,24 +199,6 @@ class AddNewItem extends Component {
</div>
}
{
!term && !hasExistingAuthors ?
<div className={styles.message}>
<div className={styles.noAuthorsText}>
You haven't added any authors yet, do you want to add an existing library location (Root Folder) and update?
</div>
<div>
<Button
to="/settings/mediamanagement"
kind={kinds.PRIMARY}
>
{translate('AddRootFolder')}
</Button>
</div>
</div> :
null
}
<div />
</PageContentBody>
</PageContent>
@@ -233,7 +213,6 @@ AddNewItem.propTypes = {
isAdding: PropTypes.bool.isRequired,
addError: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
hasExistingAuthors: PropTypes.bool.isRequired,
onSearchChange: PropTypes.func.isRequired,
onClearSearch: PropTypes.func.isRequired
};

View File

@@ -10,15 +10,13 @@ import AddNewItem from './AddNewItem';
function createMapStateToProps() {
return createSelector(
(state) => state.search,
(state) => state.authors.items.length,
(state) => state.router.location,
(search, existingAuthorsCount, location) => {
(search, location) => {
const { params } = parseUrl(location.search);
return {
...search,
term: params.term,
hasExistingAuthors: existingAuthorsCount > 0
...search
};
}
);

View File

@@ -9,7 +9,6 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import AddAuthorOptionsForm from '../Common/AddAuthorOptionsForm.js';
import styles from './AddNewAuthorModalContent.css';
@@ -55,7 +54,7 @@ class AddNewAuthorModalContent extends Component {
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('AddNewAuthor')}
Add new Author
</ModalHeader>
<ModalBody>
@@ -134,7 +133,7 @@ class AddNewAuthorModalContent extends Component {
AddNewAuthorModalContent.propTypes = {
authorName: PropTypes.string.isRequired,
disambiguation: PropTypes.string,
disambiguation: PropTypes.string.isRequired,
overview: PropTypes.string,
images: PropTypes.arrayOf(PropTypes.object).isRequired,
isAdding: PropTypes.bool.isRequired,

View File

@@ -4,7 +4,6 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { addAuthor, setAuthorAddDefault } from 'Store/Actions/searchActions';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
import selectSettings from 'Store/Selectors/selectSettings';
import AddNewAuthorModalContent from './AddNewAuthorModalContent';
@@ -13,8 +12,7 @@ function createMapStateToProps() {
(state) => state.search,
(state) => state.settings.metadataProfiles,
createDimensionsSelector(),
createSystemStatusSelector(),
(searchState, metadataProfiles, dimensions, systemStatus) => {
(searchState, metadataProfiles, dimensions) => {
const {
isAdding,
addError,
@@ -34,7 +32,6 @@ function createMapStateToProps() {
isSmallScreen: dimensions.isSmallScreen,
validationErrors,
validationWarnings,
isWindows: systemStatus.isWindows,
...settings
};
}

View File

@@ -78,7 +78,6 @@ class AddNewAuthorSearchResult extends Component {
status,
overview,
ratings,
folder,
images,
isExistingAuthor,
isSmallScreen
@@ -206,7 +205,6 @@ class AddNewAuthorSearchResult extends Component {
disambiguation={disambiguation}
year={year}
overview={overview}
folder={folder}
images={images}
onModalClose={this.onAddAuthorModalClose}
/>
@@ -224,7 +222,6 @@ AddNewAuthorSearchResult.propTypes = {
status: PropTypes.string.isRequired,
overview: PropTypes.string,
ratings: PropTypes.object.isRequired,
folder: PropTypes.string.isRequired,
images: PropTypes.arrayOf(PropTypes.object).isRequired,
isExistingAuthor: PropTypes.bool.isRequired,
isSmallScreen: PropTypes.bool.isRequired

View File

@@ -10,7 +10,6 @@ import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { kinds } from 'Helpers/Props';
import stripHtml from 'Utilities/String/stripHtml';
import translate from 'Utilities/String/translate';
import AddAuthorOptionsForm from '../Common/AddAuthorOptionsForm.js';
import styles from './AddNewBookModalContent.css';
@@ -59,7 +58,7 @@ class AddNewBookModalContent extends Component {
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('AddNewBook')}
Add new Book
</ModalHeader>
<ModalBody>

View File

@@ -4,7 +4,6 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { addBook, setBookAddDefault } from 'Store/Actions/searchActions';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
import selectSettings from 'Store/Selectors/selectSettings';
import AddNewBookModalContent from './AddNewBookModalContent';
@@ -14,8 +13,7 @@ function createMapStateToProps() {
(state) => state.search,
(state) => state.settings.metadataProfiles,
createDimensionsSelector(),
createSystemStatusSelector(),
(isExistingAuthor, searchState, metadataProfiles, dimensions, systemStatus) => {
(isExistingAuthor, searchState, metadataProfiles, dimensions) => {
const {
isAdding,
addError,
@@ -35,7 +33,6 @@ function createMapStateToProps() {
isSmallScreen: dimensions.isSmallScreen,
validationErrors,
validationWarnings,
isWindows: systemStatus.isWindows,
...settings
};
}

View File

@@ -203,7 +203,6 @@ class AddNewBookSearchResult extends Component {
disambiguation={disambiguation}
authorName={author.authorName}
overview={overview}
folder={author.folder}
images={images}
onModalClose={this.onAddBookModalClose}
/>

View File

@@ -39,9 +39,7 @@ class AddAuthorOptionsForm extends Component {
includeNoneMetadataProfile,
includeSpecificBookMonitor,
showMetadataProfile,
folder,
tags,
isWindows,
onInputChange,
...otherProps
} = this.props;
@@ -56,15 +54,6 @@ class AddAuthorOptionsForm extends Component {
<FormInputGroup
type={inputTypes.ROOT_FOLDER_SELECT}
name="rootFolderPath"
valueOptions={{
authorFolder: folder,
isWindows
}}
selectedValueOptions={{
authorFolder: folder,
isWindows
}}
helpText={translate('AddNewAuthorRootFolderHelpText', { folder })}
onChange={onInputChange}
{...rootFolderPath}
/>
@@ -190,14 +179,8 @@ AddAuthorOptionsForm.propTypes = {
showMetadataProfile: PropTypes.bool.isRequired,
includeNoneMetadataProfile: PropTypes.bool.isRequired,
includeSpecificBookMonitor: PropTypes.bool.isRequired,
folder: PropTypes.string.isRequired,
tags: PropTypes.object.isRequired,
isWindows: PropTypes.bool.isRequired,
onInputChange: PropTypes.func.isRequired
};
AddAuthorOptionsForm.defaultProps = {
includeSpecificBookMonitor: false
};
export default AddAuthorOptionsForm;

View File

@@ -10,11 +10,11 @@ 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 Column from 'Components/Table/Column';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import useSelectState from 'Helpers/Hooks/useSelectState';
import { kinds } from 'Helpers/Props';
import SortDirection from 'Helpers/Props/SortDirection';
import {
bulkDeleteDownloadClients,
bulkEditDownloadClients,
@@ -35,7 +35,7 @@ type OnSelectedChangeCallback = React.ComponentProps<
typeof ManageDownloadClientsModalRow
>['onSelectedChange'];
const COLUMNS: Column[] = [
const COLUMNS = [
{
name: 'name',
label: () => translate('Name'),
@@ -82,6 +82,8 @@ const COLUMNS: Column[] = [
interface ManageDownloadClientsModalContentProps {
onModalClose(): void;
sortKey?: string;
sortDirection?: SortDirection;
}
function ManageDownloadClientsModalContent(

View File

@@ -76,7 +76,7 @@ function EditImportListExclusionModalContent(props) {
<FormGroup>
<FormLabel>
{translate('ForeignId')}
{translate('MusicbrainzId')}
</FormLabel>
<FormInputGroup

View File

@@ -10,11 +10,11 @@ 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 Column from 'Components/Table/Column';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import useSelectState from 'Helpers/Hooks/useSelectState';
import { kinds } from 'Helpers/Props';
import SortDirection from 'Helpers/Props/SortDirection';
import {
bulkDeleteIndexers,
bulkEditIndexers,
@@ -35,7 +35,7 @@ type OnSelectedChangeCallback = React.ComponentProps<
typeof ManageIndexersModalRow
>['onSelectedChange'];
const COLUMNS: Column[] = [
const COLUMNS = [
{
name: 'name',
label: () => translate('Name'),
@@ -82,6 +82,8 @@ const COLUMNS: Column[] = [
interface ManageIndexersModalContentProps {
onModalClose(): void;
sortKey?: string;
sortDirection?: SortDirection;
}
function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {

View File

@@ -1,18 +0,0 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { createQualityProfileSelectorForHook } from 'Store/Selectors/createQualityProfileSelector';
import translate from 'Utilities/String/translate';
interface QualityProfileNameProps {
qualityProfileId: number;
}
function QualityProfileName({ qualityProfileId }: QualityProfileNameProps) {
const qualityProfile = useSelector(
createQualityProfileSelectorForHook(qualityProfileId)
);
return <span>{qualityProfile?.name ?? translate('Unknown')}</span>;
}
export default QualityProfileName;

View File

@@ -0,0 +1,31 @@
import PropTypes from 'prop-types';
import React from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createQualityProfileSelector from 'Store/Selectors/createQualityProfileSelector';
function createMapStateToProps() {
return createSelector(
createQualityProfileSelector(),
(qualityProfile) => {
return {
name: qualityProfile.name
};
}
);
}
function QualityProfileNameConnector({ name, ...otherProps }) {
return (
<span>
{name}
</span>
);
}
QualityProfileNameConnector.propTypes = {
qualityProfileId: PropTypes.number.isRequired,
name: PropTypes.string.isRequired
};
export default connect(createMapStateToProps)(QualityProfileNameConnector);

View File

@@ -45,12 +45,6 @@ export const defaultState = {
isSortable: true,
isVisible: true
},
{
name: 'books.lastSearchTime',
label: 'Last Searched',
isSortable: true,
isVisible: false
},
{
name: 'actions',
columnLabel: 'Actions',
@@ -114,12 +108,6 @@ export const defaultState = {
isSortable: true,
isVisible: true
},
{
name: 'books.lastSearchTime',
label: 'Last Searched',
isSortable: true,
isVisible: false
},
{
name: 'actions',
columnLabel: 'Actions',

View File

@@ -27,12 +27,6 @@ export default function translate(
key: string,
tokens: Record<string, string | number | boolean> = {}
) {
const { isProduction = true } = window.Readarr;
if (!isProduction && !(key in translations)) {
console.warn(`Missing translation for key: ${key}`);
}
const translation = translations[key] || key;
tokens.appName = 'Readarr';

View File

@@ -131,15 +131,13 @@ class CutoffUnmetConnector extends Component {
onSearchSelectedPress = (selected) => {
this.props.executeCommand({
name: commandNames.BOOK_SEARCH,
bookIds: selected,
commandFinished: this.repopulate
bookIds: selected
});
};
onSearchAllCutoffUnmetPress = () => {
this.props.executeCommand({
name: commandNames.CUTOFF_UNMET_BOOK_SEARCH,
commandFinished: this.repopulate
name: commandNames.CUTOFF_UNMET_BOOK_SEARCH
});
};

View File

@@ -16,7 +16,6 @@ function CutoffUnmetRow(props) {
releaseDate,
titleSlug,
title,
lastSearchTime,
disambiguation,
isSelected,
columns,
@@ -69,15 +68,6 @@ function CutoffUnmetRow(props) {
);
}
if (name === 'books.lastSearchTime') {
return (
<RelativeDateCellConnector
key={name}
date={lastSearchTime}
/>
);
}
if (name === 'releaseDate') {
return (
<RelativeDateCellConnector
@@ -115,7 +105,6 @@ CutoffUnmetRow.propTypes = {
releaseDate: PropTypes.string.isRequired,
titleSlug: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
lastSearchTime: PropTypes.string,
disambiguation: PropTypes.string,
isSelected: PropTypes.bool,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,

View File

@@ -121,15 +121,13 @@ class MissingConnector extends Component {
onSearchSelectedPress = (selected) => {
this.props.executeCommand({
name: commandNames.BOOK_SEARCH,
bookIds: selected,
commandFinished: this.repopulate
bookIds: selected
});
};
onSearchAllMissingPress = () => {
this.props.executeCommand({
name: commandNames.MISSING_BOOK_SEARCH,
commandFinished: this.repopulate
name: commandNames.MISSING_BOOK_SEARCH
});
};

View File

@@ -16,7 +16,6 @@ function MissingRow(props) {
releaseDate,
titleSlug,
title,
lastSearchTime,
disambiguation,
isSelected,
columns,
@@ -78,15 +77,6 @@ function MissingRow(props) {
);
}
if (name === 'books.lastSearchTime') {
return (
<RelativeDateCellConnector
key={name}
date={lastSearchTime}
/>
);
}
if (name === 'actions') {
return (
<BookSearchCellConnector
@@ -114,7 +104,6 @@ MissingRow.propTypes = {
releaseDate: PropTypes.string.isRequired,
titleSlug: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
lastSearchTime: PropTypes.string,
disambiguation: PropTypes.string,
isSelected: PropTypes.bool,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,

View File

@@ -7,6 +7,5 @@ interface Window {
theme: string;
urlBase: string;
version: string;
isProduction: boolean;
};
}

View File

@@ -25,10 +25,10 @@
"defaults"
],
"dependencies": {
"@fortawesome/fontawesome-free": "6.7.1",
"@fortawesome/fontawesome-svg-core": "6.7.1",
"@fortawesome/free-regular-svg-icons": "6.7.1",
"@fortawesome/free-solid-svg-icons": "6.7.1",
"@fortawesome/fontawesome-free": "6.6.0",
"@fortawesome/fontawesome-svg-core": "6.6.0",
"@fortawesome/free-regular-svg-icons": "6.6.0",
"@fortawesome/free-solid-svg-icons": "6.6.0",
"@fortawesome/react-fontawesome": "0.2.2",
"@microsoft/signalr": "6.0.25",
"@sentry/browser": "7.119.1",
@@ -86,13 +86,13 @@
"typescript": "5.1.6"
},
"devDependencies": {
"@babel/core": "7.26.0",
"@babel/eslint-parser": "7.25.9",
"@babel/plugin-proposal-export-default-from": "7.25.9",
"@babel/core": "7.25.8",
"@babel/eslint-parser": "7.25.8",
"@babel/plugin-proposal-export-default-from": "7.25.8",
"@babel/plugin-syntax-dynamic-import": "7.8.3",
"@babel/preset-env": "7.26.0",
"@babel/preset-react": "7.26.3",
"@babel/preset-typescript": "7.26.0",
"@babel/preset-env": "7.25.8",
"@babel/preset-react": "7.25.7",
"@babel/preset-typescript": "7.25.7",
"@types/lodash": "4.14.195",
"@types/react-lazyload": "3.2.3",
"@types/redux-actions": "2.6.5",
@@ -102,7 +102,7 @@
"babel-loader": "9.2.1",
"babel-plugin-inline-classnames": "2.0.1",
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
"core-js": "3.39.0",
"core-js": "3.38.1",
"css-loader": "6.8.1",
"css-modules-typescript-loader": "4.0.1",
"eslint": "8.57.1",
@@ -142,7 +142,7 @@
"worker-loader": "3.0.8"
},
"volta": {
"node": "20.11.1",
"node": "16.17.0",
"yarn": "1.22.19"
}
}

View File

@@ -99,35 +99,6 @@
<RootNamespace Condition="'$(ReadarrProject)'=='true'">$(MSBuildProjectName.Replace('Readarr','NzbDrone'))</RootNamespace>
</PropertyGroup>
<ItemGroup Condition="'$(TestProject)'!='true'">
<!-- Annotates .NET assemblies with repository information including SHA -->
<!-- Sentry uses this to link directly to GitHub at the exact version/file/line -->
<!-- This is built-in on .NET 8 and can be removed once the project is updated -->
<PackageReference Include="Microsoft.SourceLink.GitHub" />
</ItemGroup>
<!-- Sentry specific configuration: Only in Release mode -->
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<!-- https://docs.sentry.io/platforms/dotnet/configuration/msbuild/ -->
<!-- OrgSlug, ProjectSlug and AuthToken are required.
They can be set below, via argument to 'msbuild -p:' or environment variable -->
<SentryOrg></SentryOrg>
<SentryProject></SentryProject>
<SentryUrl></SentryUrl> <!-- If empty, assumed to be sentry.io -->
<SentryAuthToken></SentryAuthToken> <!-- Use env var instead: SENTRY_AUTH_TOKEN -->
<!-- Upload PDBs to Sentry, enabling stack traces with line numbers and file paths
without the need to deploy the application with PDBs -->
<SentryUploadSymbols>true</SentryUploadSymbols>
<!-- Source Link settings -->
<!-- https://github.com/dotnet/sourcelink/blob/main/docs/README.md#publishrepositoryurl -->
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<!-- Embeds all source code in the respective PDB. This can make it a bit bigger but since it'll be uploaded
to Sentry and not distributed to run on the server, it helps debug crashes while making releases smaller -->
<EmbedAllSources>true</EmbedAllSources>
</PropertyGroup>
<!-- Standard testing packages -->
<ItemGroup Condition="'$(TestProject)'=='true'">
<PackageReference Include="Microsoft.NET.Test.Sdk" />

View File

@@ -9,8 +9,7 @@
<PackageVersion Include="DryIoc.Microsoft.DependencyInjection" Version="6.2.0" />
<PackageVersion Include="Equ" Version="2.3.0" />
<PackageVersion Include="FluentAssertions" Version="5.10.3" />
<PackageVersion Include="IPAddressRange" Version="6.1.0" />
<PackageVersion Include="Polly" Version="8.5.2" />
<PackageVersion Include="Polly" Version="8.4.2" />
<PackageVersion Include="Servarr.FluentMigrator.Runner" Version="3.3.2.9" />
<PackageVersion Include="Servarr.FluentMigrator.Runner.SQLite" Version="3.3.2.9" />
<PackageVersion Include="Servarr.FluentMigrator.Runner.Postgres" Version="3.3.2.9" />
@@ -18,16 +17,14 @@
<PackageVersion Include="Ical.Net" Version="4.3.1" />
<PackageVersion Include="ImpromptuInterface" Version="7.0.1" />
<PackageVersion Include="LazyCache" Version="2.4.0" />
<PackageVersion Include="Mailkit" Version="4.8.0" />
<PackageVersion Include="Mailkit" Version="3.6.0" />
<PackageVersion Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.35" />
<PackageVersion Include="Microsoft.Data.SqlClient" Version="2.1.7" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="6.0.2" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.2" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="6.0.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
<PackageVersion Include="Microsoft.Win32.Registry" Version="5.0.0" />
<PackageVersion Include="Mono.Posix.NETStandard" Version="5.20.1.34-servarr22" />
<PackageVersion Include="Moq" Version="4.17.2" />
@@ -37,28 +34,28 @@
<PackageVersion Include="NLog.Extensions.Logging" Version="5.2.3" />
<PackageVersion Include="NLog" Version="5.1.4" />
<PackageVersion Include="NLog.Targets.Syslog" Version="7.0.0" />
<PackageVersion Include="Npgsql" Version="7.0.10" />
<PackageVersion Include="Npgsql" Version="7.0.8" />
<PackageVersion Include="NUnit3TestAdapter" Version="4.2.1" />
<PackageVersion Include="NUnit" Version="3.14.0" />
<PackageVersion Include="NunitXml.TestLogger" Version="3.0.117" />
<PackageVersion Include="PdfSharpCore" Version="1.3.65" />
<PackageVersion Include="PdfSharpCore" Version="1.3.32" />
<PackageVersion Include="RestSharp.Serializers.SystemTextJson" Version="106.15.0" />
<PackageVersion Include="RestSharp" Version="106.15.0" />
<PackageVersion Include="Selenium.Support" Version="3.141.0" />
<PackageVersion Include="Selenium.WebDriver.ChromeDriver" Version="134.0.6998.16500" />
<PackageVersion Include="Sentry" Version="4.0.2" />
<PackageVersion Include="Selenium.WebDriver.ChromeDriver" Version="91.0.4472.10100" />
<PackageVersion Include="Sentry" Version="3.31.0" />
<PackageVersion Include="SharpZipLib" Version="1.4.2" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.7" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.5" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.1.118" />
<PackageVersion Include="Swashbuckle.AspNetCore.Annotations" Version="6.6.2" />
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.6.2" />
<PackageVersion Include="System.Buffers" Version="4.6.0" />
<PackageVersion Include="System.Buffers" Version="4.5.1" />
<PackageVersion Include="System.Configuration.ConfigurationManager" Version="6.0.1" />
<PackageVersion Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
<PackageVersion Include="System.IO.Abstractions.TestingHelpers" Version="17.0.24" />
<PackageVersion Include="System.IO.Abstractions" Version="17.0.24" />
<PackageVersion Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
<PackageVersion Include="System.Memory" Version="4.6.2" />
<PackageVersion Include="System.Memory" Version="4.5.5" />
<PackageVersion Include="System.Reflection.TypeExtensions" Version="4.7.0" />
<PackageVersion Include="System.Resources.Extensions" Version="6.0.0" />
<PackageVersion Include="System.Runtime.Loader" Version="4.3.0" />
@@ -66,7 +63,7 @@
<PackageVersion Include="System.ServiceProcess.ServiceController" Version="6.0.1" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="6.0.0" />
<PackageVersion Include="System.Text.Json" Version="6.0.10" />
<PackageVersion Include="System.ValueTuple" Version="4.6.1" />
<PackageVersion Include="System.ValueTuple" Version="4.5.0" />
<PackageVersion Include="TagLibSharp-Lidarr" Version="2.2.0.19" />
</ItemGroup>
</Project>
</Project>

View File

@@ -40,16 +40,15 @@ namespace NzbDrone.Automation.Test
var service = ChromeDriverService.CreateDefaultService();
// Timeout as windows automation tests seem to take alot longer to get going
driver = new ChromeDriver(service, options, TimeSpan.FromMinutes(3));
driver = new ChromeDriver(service, options, new TimeSpan(0, 3, 0));
driver.Manage().Window.Size = new System.Drawing.Size(1920, 1080);
driver.Manage().Window.FullScreen();
_runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger(), null);
_runner.KillAll();
_runner.Start(true);
driver.Navigate().GoToUrl("http://localhost:8787");
driver.Url = "http://localhost:8787";
var page = new PageBase(driver);
page.WaitForNoSpinner();
@@ -69,7 +68,7 @@ namespace NzbDrone.Automation.Test
{
try
{
var image = (driver as ITakesScreenshot).GetScreenshot();
var image = ((ITakesScreenshot)driver).GetScreenshot();
image.SaveAsFile($"./{name}_test_screenshot.png", ScreenshotImageFormat.Png);
}
catch (Exception ex)

View File

@@ -1,17 +1,19 @@
using System;
using System.Threading;
using OpenQA.Selenium;
using OpenQA.Selenium.Remote;
using OpenQA.Selenium.Support.UI;
namespace NzbDrone.Automation.Test.PageModel
{
public class PageBase
{
private readonly IWebDriver _driver;
private readonly RemoteWebDriver _driver;
public PageBase(IWebDriver driver)
public PageBase(RemoteWebDriver driver)
{
_driver = driver;
driver.Manage().Window.Maximize();
}
public IWebElement FindByClass(string className, int timeout = 5)

View File

@@ -21,28 +21,9 @@ namespace NzbDrone.Common.Test.ExtensionTests
[TestCase("1.2.3.4")]
[TestCase("172.55.0.1")]
[TestCase("192.55.0.1")]
[TestCase("100.64.0.1")]
[TestCase("100.127.255.254")]
public void should_return_false_for_public_ip_address(string ipAddress)
{
IPAddress.Parse(ipAddress).IsLocalAddress().Should().BeFalse();
}
[TestCase("100.64.0.1")]
[TestCase("100.127.255.254")]
[TestCase("100.100.100.100")]
public void should_return_true_for_cgnat_ip_address(string ipAddress)
{
IPAddress.Parse(ipAddress).IsCgnatIpAddress().Should().BeTrue();
}
[TestCase("1.2.3.4")]
[TestCase("192.168.5.1")]
[TestCase("100.63.255.255")]
[TestCase("100.128.0.0")]
public void should_return_false_for_non_cgnat_ip_address(string ipAddress)
{
IPAddress.Parse(ipAddress).IsCgnatIpAddress().Should().BeFalse();
}
}
}

View File

@@ -4,7 +4,6 @@ using System.Linq;
using FluentAssertions;
using NLog;
using NUnit.Framework;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Instrumentation.Sentry;
using NzbDrone.Test.Common;
@@ -28,7 +27,7 @@ namespace NzbDrone.Common.Test.InstrumentationTests
[SetUp]
public void Setup()
{
_subject = new SentryTarget("https://aaaaaaaaaaaaaaaaaaaaaaaaaa@sentry.io/111111", Mocker.GetMock<IAppFolderInfo>().Object);
_subject = new SentryTarget("https://aaaaaaaaaaaaaaaaaaaaaaaaaa@sentry.io/111111");
}
private LogEventInfo GivenLogEvent(LogLevel level, Exception ex, string message)

View File

@@ -1,5 +1,6 @@
using System;
using System.IO;
using System.Text;
using FluentAssertions;
using Moq;
using NUnit.Framework;
@@ -316,5 +317,14 @@ namespace NzbDrone.Common.Test
result[2].Should().Be(@"Music");
result[3].Should().Be(@"Author Title");
}
[Test]
public void should_be_equal_with_different_unicode_representations()
{
var path1 = @"C:\Test\file.mkv".AsOsAgnostic().Normalize(NormalizationForm.FormC);
var path2 = @"C:\Test\file.mkv".AsOsAgnostic().Normalize(NormalizationForm.FormD);
path1.PathEquals(path2);
}
}
}

View File

@@ -42,18 +42,17 @@ namespace NzbDrone.Common
public void CreateZip(string path, IEnumerable<string> files)
{
_logger.Debug("Creating archive {0}", path);
using var zipFile = ZipFile.Create(path);
zipFile.BeginUpdate();
foreach (var file in files)
using (var zipFile = ZipFile.Create(path))
{
zipFile.Add(file, Path.GetFileName(file));
}
zipFile.BeginUpdate();
zipFile.CommitUpdate();
foreach (var file in files)
{
zipFile.Add(file, Path.GetFileName(file));
}
zipFile.CommitUpdate();
}
}
private void ExtractZip(string compressedFile, string destination)

View File

@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.IO;
using System.IO.Abstractions;
using System.Linq;
using System.Threading;
using NLog;
using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.EnvironmentInfo;
@@ -307,26 +306,9 @@ namespace NzbDrone.Common.Disk
{
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
var files = GetFiles(path, recursive).ToList();
var files = GetFiles(path, recursive);
files.ForEach(RemoveReadOnly);
var attempts = 0;
while (attempts < 3 && files.Any())
{
EmptyFolder(path);
if (GetFiles(path, recursive).Any())
{
// Wait for IO operations to complete after emptying the folder since they aren't always
// instantly removed and it can lead to false positives that files are still present.
Thread.Sleep(3000);
}
attempts++;
files = GetFiles(path, recursive).ToList();
}
files.ToList().ForEach(RemoveReadOnly);
_fileSystem.Directory.Delete(path, recursive);
}

View File

@@ -342,11 +342,10 @@ namespace NzbDrone.Common.Disk
var isCifs = targetDriveFormat == "cifs";
var isBtrfs = sourceDriveFormat == "btrfs" && targetDriveFormat == "btrfs";
var isZfs = sourceDriveFormat == "zfs" && targetDriveFormat == "zfs";
if (mode.HasFlag(TransferMode.Copy))
{
if (isBtrfs || isZfs)
if (isBtrfs)
{
if (_diskProvider.TryCreateRefLink(sourcePath, targetPath))
{
@@ -360,7 +359,7 @@ namespace NzbDrone.Common.Disk
if (mode.HasFlag(TransferMode.Move))
{
if (isBtrfs || isZfs)
if (isBtrfs)
{
if (isSameMount && _diskProvider.TryRenameFile(sourcePath, targetPath))
{

View File

@@ -39,24 +39,18 @@ namespace NzbDrone.Common.Extensions
private static bool IsLocalIPv4(byte[] ipv4Bytes)
{
// Link local (no IP assigned by DHCP): 169.254.0.0 to 169.254.255.255 (169.254.0.0/16)
var isLinkLocal = ipv4Bytes[0] == 169 && ipv4Bytes[1] == 254;
bool IsLinkLocal() => ipv4Bytes[0] == 169 && ipv4Bytes[1] == 254;
// Class A private range: 10.0.0.0 10.255.255.255 (10.0.0.0/8)
var isClassA = ipv4Bytes[0] == 10;
bool IsClassA() => ipv4Bytes[0] == 10;
// Class B private range: 172.16.0.0 172.31.255.255 (172.16.0.0/12)
var isClassB = ipv4Bytes[0] == 172 && ipv4Bytes[1] >= 16 && ipv4Bytes[1] <= 31;
bool IsClassB() => ipv4Bytes[0] == 172 && ipv4Bytes[1] >= 16 && ipv4Bytes[1] <= 31;
// Class C private range: 192.168.0.0 192.168.255.255 (192.168.0.0/16)
var isClassC = ipv4Bytes[0] == 192 && ipv4Bytes[1] == 168;
bool IsClassC() => ipv4Bytes[0] == 192 && ipv4Bytes[1] == 168;
return isLinkLocal || isClassA || isClassC || isClassB;
}
public static bool IsCgnatIpAddress(this IPAddress ipAddress)
{
var bytes = ipAddress.GetAddressBytes();
return bytes.Length == 4 && bytes[0] == 100 && bytes[1] >= 64 && bytes[1] <= 127;
return IsLinkLocal() || IsClassA() || IsClassC() || IsClassB();
}
}
}

View File

@@ -60,6 +60,10 @@ namespace NzbDrone.Common.Extensions
public static bool PathEquals(this string firstPath, string secondPath, StringComparison? comparison = null)
{
// Normalize paths to ensure unicode characters are represented the same way
firstPath = firstPath.Normalize();
secondPath = secondPath?.Normalize();
if (!comparison.HasValue)
{
comparison = DiskProviderBase.PathStringComparison;
@@ -108,15 +112,6 @@ namespace NzbDrone.Common.Extensions
return Directory.GetParent(cleanPath)?.FullName;
}
public static string GetCleanPath(this string path)
{
var cleanPath = OsInfo.IsWindows
? PARENT_PATH_END_SLASH_REGEX.Replace(path, "")
: path.TrimEnd(Path.DirectorySeparatorChar);
return cleanPath;
}
public static bool IsParentPath(this string parentPath, string childPath)
{
if (parentPath != "/" && !parentPath.EndsWith(":\\"))

View File

@@ -1,4 +1,3 @@
using System;
using NzbDrone.Common.Extensions;
namespace NzbDrone.Common.Http.Proxy
@@ -30,8 +29,7 @@ namespace NzbDrone.Common.Http.Proxy
{
if (!string.IsNullOrWhiteSpace(BypassFilter))
{
var hostlist = BypassFilter.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
var hostlist = BypassFilter.Split(',');
for (var i = 0; i < hostlist.Length; i++)
{
if (hostlist[i].StartsWith("*"))
@@ -43,7 +41,7 @@ namespace NzbDrone.Common.Http.Proxy
return hostlist;
}
return Array.Empty<string>();
return new string[] { };
}
}

View File

@@ -4,27 +4,27 @@ namespace NzbDrone.Common.Instrumentation.Extensions
{
public static class LoggerExtensions
{
[MessageTemplateFormatMethod("message")]
public static void ProgressInfo(this Logger logger, string message, params object[] args)
{
LogProgressMessage(logger, LogLevel.Info, message, args);
var formattedMessage = string.Format(message, args);
LogProgressMessage(logger, LogLevel.Info, formattedMessage);
}
[MessageTemplateFormatMethod("message")]
public static void ProgressDebug(this Logger logger, string message, params object[] args)
{
LogProgressMessage(logger, LogLevel.Debug, message, args);
var formattedMessage = string.Format(message, args);
LogProgressMessage(logger, LogLevel.Debug, formattedMessage);
}
[MessageTemplateFormatMethod("message")]
public static void ProgressTrace(this Logger logger, string message, params object[] args)
{
LogProgressMessage(logger, LogLevel.Trace, message, args);
var formattedMessage = string.Format(message, args);
LogProgressMessage(logger, LogLevel.Trace, formattedMessage);
}
private static void LogProgressMessage(Logger logger, LogLevel level, string message, object[] parameters)
private static void LogProgressMessage(Logger logger, LogLevel level, string message)
{
var logEvent = new LogEventInfo(level, logger.Name, null, message, parameters);
var logEvent = new LogEventInfo(level, logger.Name, message);
logEvent.Properties.Add("Status", "");
logger.Log(logEvent);

View File

@@ -41,7 +41,7 @@ namespace NzbDrone.Common.Instrumentation
RegisterDebugger();
}
RegisterSentry(updateApp, appFolderInfo);
RegisterSentry(updateApp);
if (updateApp)
{
@@ -62,7 +62,7 @@ namespace NzbDrone.Common.Instrumentation
LogManager.ReconfigExistingLoggers();
}
private static void RegisterSentry(bool updateClient, IAppFolderInfo appFolderInfo)
private static void RegisterSentry(bool updateClient)
{
string dsn;
@@ -77,7 +77,7 @@ namespace NzbDrone.Common.Instrumentation
: "https://31e00a6c63ea42c8b5fe70358526a30d@sentry.servarr.com/4";
}
var target = new SentryTarget(dsn, appFolderInfo)
var target = new SentryTarget(dsn)
{
Name = "sentryTarget",
Layout = "${message}"

View File

@@ -9,7 +9,6 @@ using NLog;
using NLog.Common;
using NLog.Targets;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using Sentry;
namespace NzbDrone.Common.Instrumentation.Sentry
@@ -100,7 +99,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
public bool FilterEvents { get; set; }
public bool SentryEnabled { get; set; }
public SentryTarget(string dsn, IAppFolderInfo appFolderInfo)
public SentryTarget(string dsn)
{
_sdk = SentrySdk.Init(o =>
{
@@ -108,33 +107,9 @@ namespace NzbDrone.Common.Instrumentation.Sentry
o.AttachStacktrace = true;
o.MaxBreadcrumbs = 200;
o.Release = $"{BuildInfo.AppName}@{BuildInfo.Release}";
o.SetBeforeSend(x => SentryCleanser.CleanseEvent(x));
o.SetBeforeBreadcrumb(x => SentryCleanser.CleanseBreadcrumb(x));
o.BeforeSend = x => SentryCleanser.CleanseEvent(x);
o.BeforeBreadcrumb = x => SentryCleanser.CleanseBreadcrumb(x);
o.Environment = BuildInfo.Branch;
// Crash free run statistics (sends a ping for healthy and for crashes sessions)
o.AutoSessionTracking = false;
// Caches files in the event device is offline
// Sentry creates a 'sentry' sub directory, no need to concat here
o.CacheDirectoryPath = appFolderInfo.GetAppDataPath();
// default environment is production
if (!RuntimeInfo.IsProduction)
{
if (RuntimeInfo.IsDevelopment)
{
o.Environment = "development";
}
else if (RuntimeInfo.IsTesting)
{
o.Environment = "testing";
}
else
{
o.Environment = "other";
}
}
});
InitializeScope();
@@ -152,7 +127,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
{
SentrySdk.ConfigureScope(scope =>
{
scope.User = new SentryUser
scope.User = new User
{
Id = HashUtil.AnonymousToken()
};
@@ -194,7 +169,9 @@ namespace NzbDrone.Common.Instrumentation.Sentry
private void OnError(Exception ex)
{
if (ex is WebException webException)
var webException = ex as WebException;
if (webException != null)
{
var response = webException.Response as HttpWebResponse;
var statusCode = response?.StatusCode;
@@ -313,21 +290,13 @@ namespace NzbDrone.Common.Instrumentation.Sentry
}
}
var level = LoggingLevelMap[logEvent.Level];
var sentryEvent = new SentryEvent(logEvent.Exception)
{
Level = level,
Level = LoggingLevelMap[logEvent.Level],
Logger = logEvent.LoggerName,
Message = logEvent.FormattedMessage
};
if (level is SentryLevel.Fatal && logEvent.Exception is not null)
{
// Usages of 'fatal' here indicates the process will crash. In Sentry this is represented with
// the 'unhandled' exception flag
logEvent.Exception.SetSentryMechanism("Logger.Fatal", "Logger.Fatal was called", false);
}
sentryEvent.SetExtras(extras);
sentryEvent.SetFingerprint(fingerPrint);

View File

@@ -6,5 +6,4 @@ public class AuthOptions
public bool? Enabled { get; set; }
public string Method { get; set; }
public string Required { get; set; }
public bool? TrustCgnatIpAddresses { get; set; }
}

View File

@@ -21,10 +21,10 @@ namespace NzbDrone.Common
{
if (OsInfo.IsWindows)
{
return obj.CleanFilePath().ToLower().GetHashCode();
return obj.CleanFilePath().Normalize().ToLower().GetHashCode();
}
return obj.CleanFilePath().GetHashCode();
return obj.CleanFilePath().Normalize().GetHashCode();
}
}
}

View File

@@ -6,7 +6,6 @@ using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using NLog;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Model;
@@ -118,9 +117,7 @@ namespace NzbDrone.Common.Processes
UseShellExecute = false,
RedirectStandardError = true,
RedirectStandardOutput = true,
RedirectStandardInput = true,
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8
RedirectStandardInput = true
};
if (environmentVariables != null)
@@ -316,7 +313,7 @@ namespace NzbDrone.Common.Processes
processInfo = new ProcessInfo();
processInfo.Id = process.Id;
processInfo.Name = process.ProcessName;
processInfo.StartPath = process.MainModule?.FileName;
processInfo.StartPath = process.MainModule.FileName;
if (process.Id != GetCurrentProcessId() && process.HasExited)
{

View File

@@ -6,7 +6,6 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DryIoc.dll" />
<PackageReference Include="IPAddressRange" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" />
<PackageReference Include="NLog.Extensions.Logging" />

View File

@@ -17,7 +17,7 @@ namespace NzbDrone.Common.Reflection
public static List<Type> ImplementationsOf<T>(this Assembly assembly)
{
return assembly.GetExportedTypes().Where(c => typeof(T).IsAssignableFrom(c)).ToList();
return assembly.GetTypes().Where(c => typeof(T).IsAssignableFrom(c)).ToList();
}
public static bool IsSimpleType(this Type type)
@@ -68,7 +68,7 @@ namespace NzbDrone.Common.Reflection
public static Type FindTypeByName(this Assembly assembly, string name)
{
return assembly.GetExportedTypes().SingleOrDefault(c => c.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase));
return assembly.GetTypes().SingleOrDefault(c => c.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase));
}
public static bool HasAttribute<TAttribute>(this Type type)

View File

@@ -178,9 +178,8 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
VerifyWarning(item);
}
[TestCase("pausedDL")]
[TestCase("stoppedDL")]
public void paused_item_should_have_required_properties(string state)
[Test]
public void paused_item_should_have_required_properties()
{
var torrent = new QBittorrentTorrent
{
@@ -189,7 +188,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
Size = 1000,
Progress = 0.7,
Eta = 8640000,
State = state,
State = "pausedDL",
Label = "",
SavePath = ""
};
@@ -201,7 +200,6 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
}
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
[TestCase("queuedUP")]
[TestCase("uploading")]
[TestCase("stalledUP")]
@@ -399,9 +397,8 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
result.OutputPath.FullPath.Should().Be(Path.Combine(torrent.SavePath, "Droned.S01.12"));
}
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void api_261_should_use_content_path(string state)
[Test]
public void api_261_should_use_content_path()
{
var torrent = new QBittorrentTorrent
{
@@ -410,7 +407,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
Size = 1000,
Progress = 0.7,
Eta = 8640000,
State = state,
State = "pausedUP",
Label = "",
SavePath = @"C:\Torrents".AsOsAgnostic(),
ContentPath = @"C:\Torrents\Droned.S01.12".AsOsAgnostic()
@@ -687,96 +684,44 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
item.CanMoveFiles.Should().BeFalse();
}
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_not_be_removable_and_should_not_allow_move_files_if_max_ratio_is_not_set(string state)
[Test]
public void should_not_be_removable_and_should_not_allow_move_files_if_max_ratio_is_not_set()
{
GivenGlobalSeedLimits(-1);
GivenCompletedTorrent(state, ratio: 1.0f);
GivenCompletedTorrent("pausedUP", ratio: 1.0f);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeFalse();
item.CanMoveFiles.Should().BeFalse();
}
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_be_removable_and_should_allow_move_files_if_max_ratio_reached_and_paused(string state)
[Test]
public void should_be_removable_and_should_allow_move_files_if_max_ratio_reached_and_paused()
{
GivenGlobalSeedLimits(1.0f);
GivenCompletedTorrent(state, ratio: 1.0f);
GivenCompletedTorrent("pausedUP", ratio: 1.0f);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_be_removable_and_should_allow_move_files_if_max_ratio_reached_after_rounding_and_paused(string state)
{
GivenGlobalSeedLimits(1.0f);
GivenCompletedTorrent(state, ratio: 1.1006066990976857f);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_be_removable_and_should_allow_move_files_if_just_under_max_ratio_reached_after_rounding_and_paused(string state)
{
GivenGlobalSeedLimits(1.0f);
GivenCompletedTorrent(state, ratio: 0.9999f);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_be_removable_and_should_allow_move_files_if_overridden_max_ratio_reached_and_paused(string state)
[Test]
public void should_be_removable_and_should_allow_move_files_if_overridden_max_ratio_reached_and_paused()
{
GivenGlobalSeedLimits(2.0f);
GivenCompletedTorrent(state, ratio: 1.0f, ratioLimit: 0.8f);
GivenCompletedTorrent("pausedUP", ratio: 1.0f, ratioLimit: 0.8f);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_be_removable_and_should_allow_move_files_if_overridden_max_ratio_reached_after_rounding_and_paused(string state)
{
GivenGlobalSeedLimits(2.0f);
GivenCompletedTorrent(state, ratio: 1.1006066990976857f, ratioLimit: 1.1f);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_be_removable_and_should_allow_move_files_if_just_under_overridden_max_ratio_reached_after_rounding_and_paused(string state)
{
GivenGlobalSeedLimits(2.0f);
GivenCompletedTorrent(state, ratio: 0.9999f, ratioLimit: 1.0f);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_not_be_removable_if_overridden_max_ratio_not_reached_and_paused(string state)
[Test]
public void should_not_be_removable_if_overridden_max_ratio_not_reached_and_paused()
{
GivenGlobalSeedLimits(0.2f);
GivenCompletedTorrent(state, ratio: 0.5f, ratioLimit: 0.8f);
GivenCompletedTorrent("pausedUP", ratio: 0.5f, ratioLimit: 0.8f);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeFalse();
@@ -794,36 +739,33 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
item.CanMoveFiles.Should().BeFalse();
}
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_be_removable_and_should_allow_move_files_if_max_seedingtime_reached_and_paused(string state)
[Test]
public void should_be_removable_and_should_allow_move_files_if_max_seedingtime_reached_and_paused()
{
GivenGlobalSeedLimits(-1, 20);
GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 20);
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 20);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_be_removable_and_should_allow_move_files_if_overridden_max_seedingtime_reached_and_paused(string state)
[Test]
public void should_be_removable_and_should_allow_move_files_if_overridden_max_seedingtime_reached_and_paused()
{
GivenGlobalSeedLimits(-1, 40);
GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 20, seedingTimeLimit: 10);
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 20, seedingTimeLimit: 10);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_not_be_removable_if_overridden_max_seedingtime_not_reached_and_paused(string state)
[Test]
public void should_not_be_removable_if_overridden_max_seedingtime_not_reached_and_paused()
{
GivenGlobalSeedLimits(-1, 20);
GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 30, seedingTimeLimit: 40);
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 30, seedingTimeLimit: 40);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeFalse();
@@ -841,72 +783,66 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
item.CanMoveFiles.Should().BeFalse();
}
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_be_removable_and_should_allow_move_files_if_max_inactive_seedingtime_reached_and_paused(string state)
[Test]
public void should_be_removable_and_should_allow_move_files_if_max_inactive_seedingtime_reached_and_paused()
{
GivenGlobalSeedLimits(-1, maxInactiveSeedingTime: 20);
GivenCompletedTorrent(state, ratio: 2.0f, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(25)).ToUnixTimeSeconds());
GivenCompletedTorrent("pausedUP", ratio: 2.0f, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(25)).ToUnixTimeSeconds());
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_be_removable_and_should_allow_move_files_if_overridden_max_inactive_seedingtime_reached_and_paused(string state)
[Test]
public void should_be_removable_and_should_allow_move_files_if_overridden_max_inactive_seedingtime_reached_and_paused()
{
GivenGlobalSeedLimits(-1, maxInactiveSeedingTime: 40);
GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 20, inactiveSeedingTimeLimit: 10, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(15)).ToUnixTimeSeconds());
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 20, inactiveSeedingTimeLimit: 10, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(15)).ToUnixTimeSeconds());
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_not_be_removable_if_overridden_max_inactive_seedingtime_not_reached_and_paused(string state)
[Test]
public void should_not_be_removable_if_overridden_max_inactive_seedingtime_not_reached_and_paused()
{
GivenGlobalSeedLimits(-1, maxInactiveSeedingTime: 20);
GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 30, inactiveSeedingTimeLimit: 40, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(30)).ToUnixTimeSeconds());
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 30, inactiveSeedingTimeLimit: 40, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(30)).ToUnixTimeSeconds());
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeFalse();
item.CanMoveFiles.Should().BeFalse();
}
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_be_removable_and_should_allow_move_files_if_max_seedingtime_reached_but_ratio_not_and_paused(string state)
[Test]
public void should_be_removable_and_should_allow_move_files_if_max_seedingtime_reached_but_ratio_not_and_paused()
{
GivenGlobalSeedLimits(2.0f, 20);
GivenCompletedTorrent(state, ratio: 1.0f, seedingTime: 30);
GivenCompletedTorrent("pausedUP", ratio: 1.0f, seedingTime: 30);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_be_removable_and_should_allow_move_files_if_max_inactive_seedingtime_reached_but_ratio_not_and_paused(string state)
[Test]
public void should_be_removable_and_should_allow_move_files_if_max_inactive_seedingtime_reached_but_ratio_not_and_paused()
{
GivenGlobalSeedLimits(2.0f, maxInactiveSeedingTime: 20);
GivenCompletedTorrent(state, ratio: 1.0f, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(25)).ToUnixTimeSeconds());
GivenCompletedTorrent("pausedUP", ratio: 1.0f, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(25)).ToUnixTimeSeconds());
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_not_fetch_details_twice(string state)
[Test]
public void should_not_fetch_details_twice()
{
GivenGlobalSeedLimits(-1, 30);
GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 20);
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 20);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeFalse();
@@ -918,9 +854,8 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
.Verify(p => p.GetTorrentProperties(It.IsAny<string>(), It.IsAny<QBittorrentSettings>()), Times.Once());
}
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_get_category_from_the_category_if_set(string state)
[Test]
public void should_get_category_from_the_category_if_set()
{
const string category = "music-readarr";
GivenGlobalSeedLimits(1.0f);
@@ -932,7 +867,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
Size = 1000,
Progress = 1.0,
Eta = 8640000,
State = state,
State = "pausedUP",
Category = category,
SavePath = "",
Ratio = 1.0f
@@ -944,9 +879,8 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
item.Category.Should().Be(category);
}
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_get_category_from_the_label_if_the_category_is_not_available(string state)
[Test]
public void should_get_category_from_the_label_if_the_category_is_not_available()
{
const string category = "music-readarr";
GivenGlobalSeedLimits(1.0f);
@@ -958,7 +892,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
Size = 1000,
Progress = 1.0,
Eta = 8640000,
State = state,
State = "pausedUP",
Label = category,
SavePath = "",
Ratio = 1.0f

View File

@@ -478,37 +478,6 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests
downloadClientInfo.RemovesCompletedDownloads.Should().BeTrue();
}
[TestCase("all", 0)]
[TestCase("days-archive", 15)]
[TestCase("days-delete", 15)]
public void should_set_history_removes_completed_downloads_false_for_separate_properties(string option, int number)
{
_config.Misc.history_retention_option = option;
_config.Misc.history_retention_number = number;
var downloadClientInfo = Subject.GetStatus();
downloadClientInfo.RemovesCompletedDownloads.Should().BeFalse();
}
[TestCase("number-archive", 10)]
[TestCase("number-delete", 10)]
[TestCase("number-archive", 0)]
[TestCase("number-delete", 0)]
[TestCase("days-archive", 3)]
[TestCase("days-delete", 3)]
[TestCase("all-archive", 0)]
[TestCase("all-delete", 0)]
public void should_set_history_removes_completed_downloads_true_for_separate_properties(string option, int number)
{
_config.Misc.history_retention_option = option;
_config.Misc.history_retention_number = number;
var downloadClientInfo = Subject.GetStatus();
downloadClientInfo.RemovesCompletedDownloads.Should().BeTrue();
}
[TestCase(@"Y:\nzbget\root", @"completed\downloads", @"vv", @"Y:\nzbget\root\completed\downloads", @"Y:\nzbget\root\completed\downloads\vv")]
[TestCase(@"Y:\nzbget\root", @"completed", @"vv", @"Y:\nzbget\root\completed", @"Y:\nzbget\root\completed\vv")]
[TestCase(@"/nzbget/root", @"completed/downloads", @"vv", @"/nzbget/root/completed/downloads", @"/nzbget/root/completed/downloads/vv")]

View File

@@ -12,7 +12,7 @@ namespace NzbDrone.Core.Test.Http
{
private HttpProxySettings GetProxySettings()
{
return new HttpProxySettings(ProxyType.Socks5, "localhost", 8080, "*.httpbin.org,google.com,172.16.0.0/12", true, null, null);
return new HttpProxySettings(ProxyType.Socks5, "localhost", 8080, "*.httpbin.org,google.com", true, null, null);
}
[Test]
@@ -23,7 +23,6 @@ namespace NzbDrone.Core.Test.Http
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://eu.httpbin.org/get")).Should().BeTrue();
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://google.com/get")).Should().BeTrue();
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://localhost:8654/get")).Should().BeTrue();
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://172.21.0.1:8989/api/v3/indexer/schema")).Should().BeTrue();
}
[Test]
@@ -32,7 +31,6 @@ namespace NzbDrone.Core.Test.Http
var settings = GetProxySettings();
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://bing.com/get")).Should().BeFalse();
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://172.3.0.1:8989/api/v3/indexer/schema")).Should().BeFalse();
}
}
}

View File

@@ -13,7 +13,7 @@ using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.MetadataSource.Goodreads
{
[TestFixture]
[Ignore("Waiting for metadata to be back again", Until = "2026-01-15 00:00:00Z")]
[Ignore("Waiting for metadata to be back again", Until = "2024-12-15 00:00:00Z")]
public class BookInfoProxyFixture : CoreTest<BookInfoProxy>
{
private MetadataProfile _metadataProfile;

View File

@@ -15,7 +15,7 @@ using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.MetadataSource.Goodreads
{
[TestFixture]
[Ignore("Waiting for metadata to be back again", Until = "2026-01-15 00:00:00Z")]
[Ignore("Waiting for metadata to be back again", Until = "2024-12-15 00:00:00Z")]
public class BookInfoProxySearchFixture : CoreTest<BookInfoProxy>
{
[SetUp]

View File

@@ -40,7 +40,7 @@ namespace NzbDrone.Core.Test.MetadataSource.Goodreads
[TestCase("Harry Potter and the sorcerer's stone a detailed summary", 72245296)]
[TestCase("B0192CTMYG", 61209488)]
[TestCase("9780439554930", 3)]
[TestCase("9780439554930", 48517161)]
public void successful_book_search(string title, int expected)
{
var result = Subject.Search(title);

View File

@@ -21,14 +21,14 @@ namespace NzbDrone.Core.Test.UpdateTests
public void no_update_when_version_higher()
{
UseRealHttp();
Subject.GetLatestUpdate("develop", new Version(10, 0)).Should().BeNull();
Subject.GetLatestUpdate("nightly", new Version(10, 0)).Should().BeNull();
}
[Test]
public void finds_update_when_version_lower()
{
UseRealHttp();
Subject.GetLatestUpdate("develop", new Version(0, 1)).Should().NotBeNull();
Subject.GetLatestUpdate("nightly", new Version(0, 1)).Should().NotBeNull();
}
[Test]
@@ -42,9 +42,10 @@ namespace NzbDrone.Core.Test.UpdateTests
[Test]
public void should_get_recent_updates()
{
const string branch = "develop";
const string branch = "nightly";
UseRealHttp();
var recent = Subject.GetRecentUpdates(branch, new Version(0, 1), null);
var recentWithChanges = recent.Where(c => c.Changes != null);
recent.Should().NotBeEmpty();
recent.Should().OnlyContain(c => c.Hash.IsNotNullOrWhiteSpace());

View File

@@ -66,19 +66,12 @@ namespace NzbDrone.Core.Backup
{
_logger.ProgressInfo("Starting Backup");
var backupFolder = GetBackupFolder(backupType);
_diskProvider.EnsureFolder(_backupTempFolder);
_diskProvider.EnsureFolder(backupFolder);
if (!_diskProvider.FolderWritable(backupFolder))
{
throw new UnauthorizedAccessException($"Backup folder {backupFolder} is not writable");
}
_diskProvider.EnsureFolder(GetBackupFolder(backupType));
var dateNow = DateTime.Now;
var backupFilename = $"readarr_backup_v{BuildInfo.Version}_{dateNow:yyyy.MM.dd_HH.mm.ss}.zip";
var backupPath = Path.Combine(backupFolder, backupFilename);
var backupPath = Path.Combine(GetBackupFolder(backupType), backupFilename);
Cleanup();

View File

@@ -102,9 +102,9 @@ namespace NzbDrone.Core.Books
_logger.Error("ReadarrId {0} was not found, it may have been removed from Goodreads.", newAuthor.Metadata.Value.ForeignAuthorId);
throw new ValidationException(new List<ValidationFailure>
{
new ("ForeignAuthorId", "An author with this ID was not found", newAuthor.Metadata.Value.ForeignAuthorId)
});
{
new ValidationFailure("MusicbrainzId", "An author with this ID was not found", newAuthor.Metadata.Value.ForeignAuthorId)
});
}
author.ApplyChanges(newAuthor);

View File

@@ -222,7 +222,7 @@ namespace NzbDrone.Core.Books
protected override void DeleteEntity(Author local, bool deleteFiles)
{
_authorService.DeleteAuthor(local.Id, deleteFiles);
_authorService.DeleteAuthor(local.Id, true);
}
protected override List<Book> GetRemoteChildren(Author local, Author remote)

View File

@@ -239,7 +239,7 @@ namespace NzbDrone.Core.Books
protected override void DeleteEntity(Book local, bool deleteFiles)
{
_bookService.DeleteBook(local.Id, deleteFiles);
_bookService.DeleteBook(local.Id, true);
}
protected override List<Edition> GetRemoteChildren(Book local, Book remote)

View File

@@ -126,7 +126,7 @@ namespace NzbDrone.Core.Books
if (ShouldDelete(local))
{
_logger.Warn($"{typeof(TEntity).Name} {local} not found in metadata and is being deleted");
DeleteEntity(local, false);
DeleteEntity(local, true);
return false;
}
else

View File

@@ -53,7 +53,6 @@ namespace NzbDrone.Core.Configuration
string SyslogServer { get; }
int SyslogPort { get; }
string SyslogLevel { get; }
string Theme { get; }
string PostgresHost { get; }
int PostgresPort { get; }
string PostgresUser { get; }
@@ -61,7 +60,7 @@ namespace NzbDrone.Core.Configuration
string PostgresMainDb { get; }
string PostgresLogDb { get; }
string PostgresCacheDb { get; }
bool TrustCgnatIpAddresses { get; }
string Theme { get; }
}
public class ConfigFileProvider : IConfigFileProvider
@@ -254,21 +253,7 @@ namespace NzbDrone.Core.Configuration
}
public string UiFolder => BuildInfo.IsDebug ? Path.Combine("..", "UI") : "UI";
public string InstanceName
{
get
{
var instanceName = _appOptions.InstanceName ?? GetValue("InstanceName", BuildInfo.AppName);
if (instanceName.Contains(BuildInfo.AppName, StringComparison.OrdinalIgnoreCase))
{
return instanceName;
}
return BuildInfo.AppName;
}
}
public string InstanceName => _appOptions.InstanceName ?? GetValue("InstanceName", BuildInfo.AppName);
public bool UpdateAutomatically => _updateOptions.Automatically ?? GetValueBoolean("UpdateAutomatically", OsInfo.IsWindows, false);
@@ -477,7 +462,5 @@ namespace NzbDrone.Core.Configuration
{
SetValue("ApiKey", GenerateApiKey());
}
public bool TrustCgnatIpAddresses => _authOptions.TrustCgnatIpAddresses ?? GetValueBoolean("TrustCgnatIpAddresses", false, persist: false);
}
}

View File

@@ -404,12 +404,6 @@ namespace NzbDrone.Core.Configuration
public string ApplicationUrl => GetValue("ApplicationUrl", string.Empty);
public bool TrustCgnatIpAddresses
{
get { return GetValueBoolean("TrustCgnatIpAddresses", false); }
set { SetValue("TrustCgnatIpAddresses", value); }
}
private string GetValue(string key)
{
return GetValue(key, string.Empty);

View File

@@ -264,7 +264,7 @@ namespace NzbDrone.Core.Datastore
protected void Delete(SqlBuilder builder)
{
var sql = builder.AddDeleteTemplate(typeof(TModel));
var sql = builder.AddDeleteTemplate(typeof(TModel)).LogQuery();
using (var conn = _database.OpenConnection())
{

View File

@@ -40,7 +40,6 @@ namespace NzbDrone.Core.Datastore
Environment.SetEnvironmentVariable("No_Expand", "true");
Environment.SetEnvironmentVariable("No_SQLiteXmlConfigFile", "true");
Environment.SetEnvironmentVariable("No_PreLoadSQLite", "true");
Environment.SetEnvironmentVariable("No_SQLiteFunctions", "true");
}
public DbFactory(IMigrationController migrationController,

View File

@@ -239,7 +239,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
// Avoid removing torrents that haven't reached the global max ratio.
// Removal also requires the torrent to be paused, in case a higher max ratio was set on the torrent itself (which is not exposed by the api).
item.CanMoveFiles = item.CanBeRemoved = torrent.State is "pausedUP" or "stoppedUP" && HasReachedSeedLimit(torrent, config);
item.CanMoveFiles = item.CanBeRemoved = torrent.State == "pausedUP" && HasReachedSeedLimit(torrent, config);
switch (torrent.State)
{
@@ -248,8 +248,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
item.Message = "qBittorrent is reporting an error";
break;
case "stoppedDL": // torrent is stopped and has NOT finished downloading
case "pausedDL": // torrent is paused and has NOT finished downloading (qBittorrent < 5)
case "pausedDL": // torrent is paused and has NOT finished downloading
item.Status = DownloadItemStatus.Paused;
break;
@@ -260,8 +259,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
item.Status = DownloadItemStatus.Queued;
break;
case "pausedUP": // torrent is paused and has finished downloading (qBittorent < 5)
case "stoppedUP": // torrent is stopped and has finished downloading
case "pausedUP": // torrent is paused and has finished downloading
case "uploading": // torrent is being seeded and data is being transferred
case "stalledUP": // torrent is being seeded, but no connection were made
case "queuedUP": // queuing is enabled and torrent is queued for upload
@@ -620,14 +618,14 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
{
if (torrent.RatioLimit >= 0)
{
if (torrent.RatioLimit - torrent.Ratio <= 0.001f)
if (torrent.Ratio >= torrent.RatioLimit)
{
return true;
}
}
else if (torrent.RatioLimit == -2 && config.MaxRatioEnabled)
{
if (config.MaxRatio - torrent.Ratio <= 0.001f)
if (Math.Round(torrent.Ratio, 2) >= config.MaxRatio)
{
return true;
}

View File

@@ -26,6 +26,8 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
Dictionary<string, QBittorrentLabel> GetLabels(QBittorrentSettings settings);
void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings);
void MoveTorrentToTopInQueue(string hash, QBittorrentSettings settings);
void PauseTorrent(string hash, QBittorrentSettings settings);
void ResumeTorrent(string hash, QBittorrentSettings settings);
void SetForceStart(string hash, bool enabled, QBittorrentSettings settings);
}

View File

@@ -148,7 +148,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
{
request.AddFormParameter("paused", false);
}
else if ((QBittorrentState)settings.InitialState == QBittorrentState.Stop)
else if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause)
{
request.AddFormParameter("paused", true);
}
@@ -178,7 +178,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
{
request.AddFormParameter("paused", false);
}
else if ((QBittorrentState)settings.InitialState == QBittorrentState.Stop)
else if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause)
{
request.AddFormParameter("paused", true);
}
@@ -214,7 +214,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
catch (DownloadClientException ex)
{
// if setCategory fails due to method not being found, then try older setLabel command for qBittorrent < v.3.3.5
if (ex.InnerException is HttpException httpException && httpException.Response.StatusCode == HttpStatusCode.NotFound)
if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.NotFound)
{
var setLabelRequest = BuildRequest(settings).Resource("/command/setLabel")
.Post()
@@ -257,7 +257,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
catch (DownloadClientException ex)
{
// qBittorrent rejects all Prio commands with 403: Forbidden if Options -> BitTorrent -> Torrent Queueing is not enabled
if (ex.InnerException is HttpException httpException && httpException.Response.StatusCode == HttpStatusCode.Forbidden)
if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.Forbidden)
{
return;
}
@@ -266,6 +266,22 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
}
}
public void PauseTorrent(string hash, QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource("/command/pause")
.Post()
.AddFormParameter("hash", hash);
ProcessRequest(request, settings);
}
public void ResumeTorrent(string hash, QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource("/command/resume")
.Post()
.AddFormParameter("hash", hash);
ProcessRequest(request, settings);
}
public void SetForceStart(string hash, bool enabled, QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource("/command/setForceStart")

View File

@@ -246,20 +246,14 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
request.AddFormParameter("category", settings.MusicCategory);
}
// Avoid extraneous API version check if initial state is ForceStart
if ((QBittorrentState)settings.InitialState is QBittorrentState.Start or QBittorrentState.Stop)
// Note: ForceStart is handled by separate api call
if ((QBittorrentState)settings.InitialState == QBittorrentState.Start)
{
var stoppedParameterName = GetApiVersion(settings) >= new Version(2, 11, 0) ? "stopped" : "paused";
// Note: ForceStart is handled by separate api call
if ((QBittorrentState)settings.InitialState == QBittorrentState.Start)
{
request.AddFormParameter(stoppedParameterName, false);
}
else if ((QBittorrentState)settings.InitialState == QBittorrentState.Stop)
{
request.AddFormParameter(stoppedParameterName, true);
}
request.AddFormParameter("paused", false);
}
else if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause)
{
request.AddFormParameter("paused", true);
}
if (settings.SequentialOrder)
@@ -297,7 +291,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
catch (DownloadClientException ex)
{
// setShareLimits was added in api v2.0.1 so catch it case of the unlikely event that someone has api v2.0
if (ex.InnerException is HttpException httpException && httpException.Response.StatusCode == HttpStatusCode.NotFound)
if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.NotFound)
{
return;
}
@@ -319,7 +313,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
catch (DownloadClientException ex)
{
// qBittorrent rejects all Prio commands with 409: Conflict if Options -> BitTorrent -> Torrent Queueing is not enabled
if (ex.InnerException is HttpException httpException && httpException.Response.StatusCode == HttpStatusCode.Conflict)
if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.Conflict)
{
return;
}
@@ -328,6 +322,22 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
}
}
public void PauseTorrent(string hash, QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource("/api/v2/torrents/pause")
.Post()
.AddFormParameter("hashes", hash);
ProcessRequest(request, settings);
}
public void ResumeTorrent(string hash, QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource("/api/v2/torrents/resume")
.Post()
.AddFormParameter("hashes", hash);
ProcessRequest(request, settings);
}
public void SetForceStart(string hash, bool enabled, QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource("/api/v2/torrents/setForceStart")

View File

@@ -1,16 +1,9 @@
using NzbDrone.Core.Annotations;
namespace NzbDrone.Core.Download.Clients.QBittorrent
{
public enum QBittorrentState
{
[FieldOption(Label = "Started")]
Start = 0,
[FieldOption(Label = "Force Started")]
ForceStart = 1,
[FieldOption(Label = "Stopped")]
Stop = 2
Pause = 2
}
}

View File

@@ -263,7 +263,20 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
status.OutputRootFolders = new List<OsPath> { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, category.FullPath) };
}
status.RemovesCompletedDownloads = RemovesCompletedDownloads(config);
if (config.Misc.history_retention.IsNullOrWhiteSpace())
{
status.RemovesCompletedDownloads = false;
}
else if (config.Misc.history_retention.EndsWith("d"))
{
int.TryParse(config.Misc.history_retention.AsSpan(0, config.Misc.history_retention.Length - 1),
out var daysRetention);
status.RemovesCompletedDownloads = daysRetention < 14;
}
else
{
status.RemovesCompletedDownloads = config.Misc.history_retention != "0";
}
return status;
}
@@ -505,43 +518,6 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
return categories.Contains(category);
}
private bool RemovesCompletedDownloads(SabnzbdConfig config)
{
var retention = config.Misc.history_retention;
var option = config.Misc.history_retention_option;
var number = config.Misc.history_retention_number;
switch (option)
{
case "all":
return false;
case "number-archive":
case "number-delete":
return true;
case "days-archive":
case "days-delete":
return number < 14;
case "all-archive":
case "all-delete":
return true;
}
// TODO: Remove these checks once support for SABnzbd < 4.3 is removed
if (retention.IsNullOrWhiteSpace())
{
return false;
}
if (retention.EndsWith("d"))
{
int.TryParse(config.Misc.history_retention.AsSpan(0, config.Misc.history_retention.Length - 1),
out var daysRetention);
return daysRetention < 14;
}
return retention != "0";
}
private bool ValidatePath(DownloadClientItem downloadClientItem)
{
var downloadItemOutputPath = downloadClientItem.OutputPath;

View File

@@ -30,8 +30,6 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
public bool enable_date_sorting { get; set; }
public bool pre_check { get; set; }
public string history_retention { get; set; }
public string history_retention_option { get; set; }
public int history_retention_number { get; set; }
}
public class SabnzbdCategory

View File

@@ -4,7 +4,6 @@ using System.Net;
using Newtonsoft.Json.Linq;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Common.Serializer;
@@ -209,7 +208,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission
private void AuthenticateClient(HttpRequestBuilder requestBuilder, TransmissionSettings settings, bool reauthenticate = false)
{
var authKey = $"{requestBuilder.BaseUrl}:{settings.Password}";
var authKey = string.Format("{0}:{1}", requestBuilder.BaseUrl, settings.Password);
var sessionId = _authSessionIDCache.Find(authKey);
@@ -221,26 +220,24 @@ namespace NzbDrone.Core.Download.Clients.Transmission
authLoginRequest.SuppressHttpError = true;
var response = _httpClient.Execute(authLoginRequest);
switch (response.StatusCode)
if (response.StatusCode == HttpStatusCode.MovedPermanently)
{
case HttpStatusCode.MovedPermanently:
var url = response.Headers.GetSingleValue("Location");
var url = response.Headers.GetSingleValue("Location");
throw new DownloadClientException("Remote site redirected to " + url);
case HttpStatusCode.Forbidden:
throw new DownloadClientException($"Failed to authenticate with Transmission. It may be necessary to add {BuildInfo.AppName}'s IP address to RPC whitelist.");
case HttpStatusCode.Conflict:
sessionId = response.Headers.GetSingleValue("X-Transmission-Session-Id");
throw new DownloadClientException("Remote site redirected to " + url);
}
else if (response.StatusCode == HttpStatusCode.Conflict)
{
sessionId = response.Headers.GetSingleValue("X-Transmission-Session-Id");
if (sessionId == null)
{
throw new DownloadClientException("Remote host did not return a Session Id.");
}
break;
default:
throw new DownloadClientAuthenticationException("Failed to authenticate with Transmission.");
if (sessionId == null)
{
throw new DownloadClientException("Remote host did not return a Session Id.");
}
}
else
{
throw new DownloadClientAuthenticationException("Failed to authenticate with Transmission.");
}
_logger.Debug("Transmission authentication succeeded.");

View File

@@ -4,24 +4,24 @@ namespace NzbDrone.Core.Exceptions
{
public class AuthorNotFoundException : NzbDroneException
{
public string ForeignAuthorId { get; set; }
public string MusicBrainzId { get; set; }
public AuthorNotFoundException(string foreignAuthorId)
: base($"Author with id {foreignAuthorId} was not found, it may have been removed from the metadata server.")
public AuthorNotFoundException(string musicbrainzId)
: base(string.Format("Author with id {0} was not found, it may have been removed from the metadata server.", musicbrainzId))
{
ForeignAuthorId = foreignAuthorId;
MusicBrainzId = musicbrainzId;
}
public AuthorNotFoundException(string foreignAuthorId, string message, params object[] args)
public AuthorNotFoundException(string musicbrainzId, string message, params object[] args)
: base(message, args)
{
ForeignAuthorId = foreignAuthorId;
MusicBrainzId = musicbrainzId;
}
public AuthorNotFoundException(string foreignAuthorId, string message)
public AuthorNotFoundException(string musicbrainzId, string message)
: base(message)
{
ForeignAuthorId = foreignAuthorId;
MusicBrainzId = musicbrainzId;
}
}
}

View File

@@ -4,24 +4,24 @@ namespace NzbDrone.Core.Exceptions
{
public class BookNotFoundException : NzbDroneException
{
public string ForeignBookId { get; set; }
public string MusicBrainzId { get; set; }
public BookNotFoundException(string foreignBookId)
: base($"Book with id {foreignBookId} was not found, it may have been removed from metadata server.")
public BookNotFoundException(string musicbrainzId)
: base(string.Format("Book with id {0} was not found, it may have been removed from metadata server.", musicbrainzId))
{
ForeignBookId = foreignBookId;
MusicBrainzId = musicbrainzId;
}
public BookNotFoundException(string foreignBookId, string message, params object[] args)
public BookNotFoundException(string musicbrainzId, string message, params object[] args)
: base(message, args)
{
ForeignBookId = foreignBookId;
MusicBrainzId = musicbrainzId;
}
public BookNotFoundException(string foreignBookId, string message)
public BookNotFoundException(string musicbrainzId, string message)
: base(message)
{
ForeignBookId = foreignBookId;
MusicBrainzId = musicbrainzId;
}
}
}

View File

@@ -4,24 +4,24 @@ namespace NzbDrone.Core.Exceptions
{
public class EditionNotFoundException : NzbDroneException
{
public string ForeignEditionId { get; set; }
public string MusicBrainzId { get; set; }
public EditionNotFoundException(string foreignEditionId)
: base($"Edition with id {foreignEditionId} was not found, it may have been removed from metadata server.")
public EditionNotFoundException(string musicbrainzId)
: base(string.Format("Edition with id {0} was not found, it may have been removed from metadata server.", musicbrainzId))
{
ForeignEditionId = foreignEditionId;
MusicBrainzId = musicbrainzId;
}
public EditionNotFoundException(string foreignEditionId, string message, params object[] args)
public EditionNotFoundException(string musicbrainzId, string message, params object[] args)
: base(message, args)
{
ForeignEditionId = foreignEditionId;
MusicBrainzId = musicbrainzId;
}
public EditionNotFoundException(string foreignEditionId, string message)
public EditionNotFoundException(string musicbrainzId, string message)
: base(message)
{
ForeignEditionId = foreignEditionId;
MusicBrainzId = musicbrainzId;
}
}
}

View File

@@ -10,9 +10,6 @@ namespace NzbDrone.Core.History
{
public const string DOWNLOAD_CLIENT = "downloadClient";
public const string RELEASE_SOURCE = "releaseSource";
public const string RELEASE_GROUP = "releaseGroup";
public const string SIZE = "size";
public const string INDEXER = "indexer";
public EntityHistory()
{

View File

@@ -116,7 +116,6 @@ namespace NzbDrone.Core.History
{
var builder = Builder()
.Join<EntityHistory, Author>((h, a) => h.AuthorId == a.Id)
.LeftJoin<EntityHistory, Book>((h, b) => h.BookId == b.Id)
.Where<EntityHistory>(x => x.Date >= date);
if (eventType.HasValue)
@@ -124,10 +123,9 @@ namespace NzbDrone.Core.History
builder.Where<EntityHistory>(h => h.EventType == eventType);
}
return _database.QueryJoined<EntityHistory, Author, Book>(builder, (history, author, book) =>
return _database.QueryJoined<EntityHistory, Author>(builder, (history, author) =>
{
history.Author = author;
history.Book = book;
return history;
}).OrderBy(h => h.Date).ToList();
}

Some files were not shown because too many files have changed in this diff Show More