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

View File

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

View File

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

View File

@@ -1,7 +1,5 @@
import ModelBase from 'App/ModelBase'; import ModelBase from 'App/ModelBase';
export type AuthorStatus = 'continuing' | 'ended';
interface Author extends ModelBase { interface Author extends ModelBase {
added: string; added: string;
genres: string[]; genres: string[];
@@ -12,7 +10,6 @@ interface Author extends ModelBase {
metadataProfileId: number; metadataProfileId: number;
rootFolderPath: string; rootFolderPath: string;
sortName: string; sortName: string;
status: AuthorStatus;
tags: number[]; tags: number[];
authorName: string; authorName: string;
isSaving?: boolean; 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 MonitoringOptionsModal from 'Author/MonitoringOptions/MonitoringOptionsModal';
import BookEditorFooter from 'Book/Editor/BookEditorFooter'; import BookEditorFooter from 'Book/Editor/BookEditorFooter';
import BookFileEditorTable from 'BookFile/Editor/BookFileEditorTable'; import BookFileEditorTable from 'BookFile/Editor/BookFileEditorTable';
import Alert from 'Components/Alert';
import IconButton from 'Components/Link/IconButton'; import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; 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 PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import SwipeHeaderConnector from 'Components/Swipe/SwipeHeaderConnector'; 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 InteractiveSearchFilterMenuConnector from 'InteractiveSearch/InteractiveSearchFilterMenuConnector';
import InteractiveSearchTable from 'InteractiveSearch/InteractiveSearchTable'; import InteractiveSearchTable from 'InteractiveSearch/InteractiveSearchTable';
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector'; import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
@@ -413,25 +412,22 @@ class AuthorDetails extends Component {
<div className={styles.contentContainer}> <div className={styles.contentContainer}>
{ {
!isPopulated && !booksError && !bookFilesError ? !isPopulated && !booksError && !bookFilesError &&
<LoadingIndicator /> : <LoadingIndicator />
null
} }
{ {
!isFetching && booksError ? !isFetching && booksError &&
<Alert kind={kinds.DANGER}> <div>
{translate('LoadingBooksFailed')} {translate('LoadingBooksFailed')}
</Alert> : </div>
null
} }
{ {
!isFetching && bookFilesError ? !isFetching && bookFilesError &&
<Alert kind={kinds.DANGER}> <div>
{translate('LoadingBookFilesFailed')} {translate('LoadingBookFilesFailed')}
</Alert> : </div>
null
} }
{ {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -28,7 +28,8 @@ function createMapStateToProps() {
if (includeNoChange) { if (includeNoChange) {
values.unshift({ values.unshift({
key: 'noChange', key: 'noChange',
value: translate('NoChange'), value: '',
name: translate('NoChange'),
isDisabled: includeNoChangeDisabled, isDisabled: includeNoChangeDisabled,
isMissing: false isMissing: false
}); });
@@ -38,6 +39,7 @@ function createMapStateToProps() {
values.push({ values.push({
key: '', key: '',
value: '', value: '',
name: '',
isDisabled: true, isDisabled: true,
isHidden: true isHidden: true
}); });
@@ -54,7 +56,8 @@ function createMapStateToProps() {
values.push({ values.push({
key: ADD_NEW_KEY, key: ADD_NEW_KEY,
value: 'Add a new path' value: '',
name: 'Add a new path'
}); });
return { 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 // Render

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -82,8 +82,7 @@ class AddNewItem extends Component {
render() { render() {
const { const {
error, error,
items, items
hasExistingAuthors
} = this.props; } = this.props;
const term = this.state.term; const term = this.state.term;
@@ -187,8 +186,7 @@ class AddNewItem extends Component {
} }
{ {
term ? !term &&
null :
<div className={styles.message}> <div className={styles.message}>
<div className={styles.helpText}> <div className={styles.helpText}>
{translate('ItsEasyToAddANewAuthorOrBookJustStartTypingTheNameOfTheItemYouWantToAdd')} {translate('ItsEasyToAddANewAuthorOrBookJustStartTypingTheNameOfTheItemYouWantToAdd')}
@@ -201,24 +199,6 @@ class AddNewItem extends Component {
</div> </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 /> <div />
</PageContentBody> </PageContentBody>
</PageContent> </PageContent>
@@ -233,7 +213,6 @@ AddNewItem.propTypes = {
isAdding: PropTypes.bool.isRequired, isAdding: PropTypes.bool.isRequired,
addError: PropTypes.object, addError: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired,
hasExistingAuthors: PropTypes.bool.isRequired,
onSearchChange: PropTypes.func.isRequired, onSearchChange: PropTypes.func.isRequired,
onClearSearch: PropTypes.func.isRequired onClearSearch: PropTypes.func.isRequired
}; };

View File

@@ -10,15 +10,13 @@ import AddNewItem from './AddNewItem';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state) => state.search, (state) => state.search,
(state) => state.authors.items.length,
(state) => state.router.location, (state) => state.router.location,
(search, existingAuthorsCount, location) => { (search, location) => {
const { params } = parseUrl(location.search); const { params } = parseUrl(location.search);
return { return {
...search,
term: params.term, 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 ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import { kinds } from 'Helpers/Props'; import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import AddAuthorOptionsForm from '../Common/AddAuthorOptionsForm.js'; import AddAuthorOptionsForm from '../Common/AddAuthorOptionsForm.js';
import styles from './AddNewAuthorModalContent.css'; import styles from './AddNewAuthorModalContent.css';
@@ -55,7 +54,7 @@ class AddNewAuthorModalContent extends Component {
return ( return (
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
<ModalHeader> <ModalHeader>
{translate('AddNewAuthor')} Add new Author
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody>
@@ -134,7 +133,7 @@ class AddNewAuthorModalContent extends Component {
AddNewAuthorModalContent.propTypes = { AddNewAuthorModalContent.propTypes = {
authorName: PropTypes.string.isRequired, authorName: PropTypes.string.isRequired,
disambiguation: PropTypes.string, disambiguation: PropTypes.string.isRequired,
overview: PropTypes.string, overview: PropTypes.string,
images: PropTypes.arrayOf(PropTypes.object).isRequired, images: PropTypes.arrayOf(PropTypes.object).isRequired,
isAdding: PropTypes.bool.isRequired, isAdding: PropTypes.bool.isRequired,

View File

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

View File

@@ -78,7 +78,6 @@ class AddNewAuthorSearchResult extends Component {
status, status,
overview, overview,
ratings, ratings,
folder,
images, images,
isExistingAuthor, isExistingAuthor,
isSmallScreen isSmallScreen
@@ -206,7 +205,6 @@ class AddNewAuthorSearchResult extends Component {
disambiguation={disambiguation} disambiguation={disambiguation}
year={year} year={year}
overview={overview} overview={overview}
folder={folder}
images={images} images={images}
onModalClose={this.onAddAuthorModalClose} onModalClose={this.onAddAuthorModalClose}
/> />
@@ -224,7 +222,6 @@ AddNewAuthorSearchResult.propTypes = {
status: PropTypes.string.isRequired, status: PropTypes.string.isRequired,
overview: PropTypes.string, overview: PropTypes.string,
ratings: PropTypes.object.isRequired, ratings: PropTypes.object.isRequired,
folder: PropTypes.string.isRequired,
images: PropTypes.arrayOf(PropTypes.object).isRequired, images: PropTypes.arrayOf(PropTypes.object).isRequired,
isExistingAuthor: PropTypes.bool.isRequired, isExistingAuthor: PropTypes.bool.isRequired,
isSmallScreen: 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 ModalHeader from 'Components/Modal/ModalHeader';
import { kinds } from 'Helpers/Props'; import { kinds } from 'Helpers/Props';
import stripHtml from 'Utilities/String/stripHtml'; import stripHtml from 'Utilities/String/stripHtml';
import translate from 'Utilities/String/translate';
import AddAuthorOptionsForm from '../Common/AddAuthorOptionsForm.js'; import AddAuthorOptionsForm from '../Common/AddAuthorOptionsForm.js';
import styles from './AddNewBookModalContent.css'; import styles from './AddNewBookModalContent.css';
@@ -59,7 +58,7 @@ class AddNewBookModalContent extends Component {
return ( return (
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
<ModalHeader> <ModalHeader>
{translate('AddNewBook')} Add new Book
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,11 +10,11 @@ import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent'; import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter'; import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import Column from 'Components/Table/Column';
import Table from 'Components/Table/Table'; import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody'; import TableBody from 'Components/Table/TableBody';
import useSelectState from 'Helpers/Hooks/useSelectState'; import useSelectState from 'Helpers/Hooks/useSelectState';
import { kinds } from 'Helpers/Props'; import { kinds } from 'Helpers/Props';
import SortDirection from 'Helpers/Props/SortDirection';
import { import {
bulkDeleteIndexers, bulkDeleteIndexers,
bulkEditIndexers, bulkEditIndexers,
@@ -35,7 +35,7 @@ type OnSelectedChangeCallback = React.ComponentProps<
typeof ManageIndexersModalRow typeof ManageIndexersModalRow
>['onSelectedChange']; >['onSelectedChange'];
const COLUMNS: Column[] = [ const COLUMNS = [
{ {
name: 'name', name: 'name',
label: () => translate('Name'), label: () => translate('Name'),
@@ -82,6 +82,8 @@ const COLUMNS: Column[] = [
interface ManageIndexersModalContentProps { interface ManageIndexersModalContentProps {
onModalClose(): void; onModalClose(): void;
sortKey?: string;
sortDirection?: SortDirection;
} }
function ManageIndexersModalContent(props: ManageIndexersModalContentProps) { 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, isSortable: true,
isVisible: true isVisible: true
}, },
{
name: 'books.lastSearchTime',
label: 'Last Searched',
isSortable: true,
isVisible: false
},
{ {
name: 'actions', name: 'actions',
columnLabel: 'Actions', columnLabel: 'Actions',
@@ -114,12 +108,6 @@ export const defaultState = {
isSortable: true, isSortable: true,
isVisible: true isVisible: true
}, },
{
name: 'books.lastSearchTime',
label: 'Last Searched',
isSortable: true,
isVisible: false
},
{ {
name: 'actions', name: 'actions',
columnLabel: 'Actions', columnLabel: 'Actions',

View File

@@ -27,12 +27,6 @@ export default function translate(
key: string, key: string,
tokens: Record<string, string | number | boolean> = {} 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; const translation = translations[key] || key;
tokens.appName = 'Readarr'; tokens.appName = 'Readarr';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -99,35 +99,6 @@
<RootNamespace Condition="'$(ReadarrProject)'=='true'">$(MSBuildProjectName.Replace('Readarr','NzbDrone'))</RootNamespace> <RootNamespace Condition="'$(ReadarrProject)'=='true'">$(MSBuildProjectName.Replace('Readarr','NzbDrone'))</RootNamespace>
</PropertyGroup> </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 --> <!-- Standard testing packages -->
<ItemGroup Condition="'$(TestProject)'=='true'"> <ItemGroup Condition="'$(TestProject)'=='true'">
<PackageReference Include="Microsoft.NET.Test.Sdk" /> <PackageReference Include="Microsoft.NET.Test.Sdk" />

View File

@@ -9,8 +9,7 @@
<PackageVersion Include="DryIoc.Microsoft.DependencyInjection" Version="6.2.0" /> <PackageVersion Include="DryIoc.Microsoft.DependencyInjection" Version="6.2.0" />
<PackageVersion Include="Equ" Version="2.3.0" /> <PackageVersion Include="Equ" Version="2.3.0" />
<PackageVersion Include="FluentAssertions" Version="5.10.3" /> <PackageVersion Include="FluentAssertions" Version="5.10.3" />
<PackageVersion Include="IPAddressRange" Version="6.1.0" /> <PackageVersion Include="Polly" Version="8.4.2" />
<PackageVersion Include="Polly" Version="8.5.2" />
<PackageVersion Include="Servarr.FluentMigrator.Runner" Version="3.3.2.9" /> <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.SQLite" Version="3.3.2.9" />
<PackageVersion Include="Servarr.FluentMigrator.Runner.Postgres" 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="Ical.Net" Version="4.3.1" />
<PackageVersion Include="ImpromptuInterface" Version="7.0.1" /> <PackageVersion Include="ImpromptuInterface" Version="7.0.1" />
<PackageVersion Include="LazyCache" Version="2.4.0" /> <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.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.Caching.Memory" Version="6.0.2" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="6.0.1" /> <PackageVersion Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" 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.Hosting.WindowsServices" Version="6.0.2" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="6.0.0" /> <PackageVersion Include="Microsoft.Extensions.Logging" Version="6.0.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.10.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="Microsoft.Win32.Registry" Version="5.0.0" />
<PackageVersion Include="Mono.Posix.NETStandard" Version="5.20.1.34-servarr22" /> <PackageVersion Include="Mono.Posix.NETStandard" Version="5.20.1.34-servarr22" />
<PackageVersion Include="Moq" Version="4.17.2" /> <PackageVersion Include="Moq" Version="4.17.2" />
@@ -37,28 +34,28 @@
<PackageVersion Include="NLog.Extensions.Logging" Version="5.2.3" /> <PackageVersion Include="NLog.Extensions.Logging" Version="5.2.3" />
<PackageVersion Include="NLog" Version="5.1.4" /> <PackageVersion Include="NLog" Version="5.1.4" />
<PackageVersion Include="NLog.Targets.Syslog" Version="7.0.0" /> <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="NUnit3TestAdapter" Version="4.2.1" />
<PackageVersion Include="NUnit" Version="3.14.0" /> <PackageVersion Include="NUnit" Version="3.14.0" />
<PackageVersion Include="NunitXml.TestLogger" Version="3.0.117" /> <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.Serializers.SystemTextJson" Version="106.15.0" />
<PackageVersion Include="RestSharp" Version="106.15.0" /> <PackageVersion Include="RestSharp" Version="106.15.0" />
<PackageVersion Include="Selenium.Support" Version="3.141.0" /> <PackageVersion Include="Selenium.Support" Version="3.141.0" />
<PackageVersion Include="Selenium.WebDriver.ChromeDriver" Version="134.0.6998.16500" /> <PackageVersion Include="Selenium.WebDriver.ChromeDriver" Version="91.0.4472.10100" />
<PackageVersion Include="Sentry" Version="4.0.2" /> <PackageVersion Include="Sentry" Version="3.31.0" />
<PackageVersion Include="SharpZipLib" Version="1.4.2" /> <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="StyleCop.Analyzers" Version="1.1.118" />
<PackageVersion Include="Swashbuckle.AspNetCore.Annotations" Version="6.6.2" /> <PackageVersion Include="Swashbuckle.AspNetCore.Annotations" Version="6.6.2" />
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerGen" 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.Configuration.ConfigurationManager" Version="6.0.1" />
<PackageVersion Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" /> <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.TestingHelpers" Version="17.0.24" />
<PackageVersion Include="System.IO.Abstractions" 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.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.Reflection.TypeExtensions" Version="4.7.0" />
<PackageVersion Include="System.Resources.Extensions" Version="6.0.0" /> <PackageVersion Include="System.Resources.Extensions" Version="6.0.0" />
<PackageVersion Include="System.Runtime.Loader" Version="4.3.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.ServiceProcess.ServiceController" Version="6.0.1" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="6.0.0" /> <PackageVersion Include="System.Text.Encoding.CodePages" Version="6.0.0" />
<PackageVersion Include="System.Text.Json" Version="6.0.10" /> <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" /> <PackageVersion Include="TagLibSharp-Lidarr" Version="2.2.0.19" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

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

View File

@@ -1,17 +1,19 @@
using System; using System;
using System.Threading; using System.Threading;
using OpenQA.Selenium; using OpenQA.Selenium;
using OpenQA.Selenium.Remote;
using OpenQA.Selenium.Support.UI; using OpenQA.Selenium.Support.UI;
namespace NzbDrone.Automation.Test.PageModel namespace NzbDrone.Automation.Test.PageModel
{ {
public class PageBase public class PageBase
{ {
private readonly IWebDriver _driver; private readonly RemoteWebDriver _driver;
public PageBase(IWebDriver driver) public PageBase(RemoteWebDriver driver)
{ {
_driver = driver; _driver = driver;
driver.Manage().Window.Maximize();
} }
public IWebElement FindByClass(string className, int timeout = 5) 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("1.2.3.4")]
[TestCase("172.55.0.1")] [TestCase("172.55.0.1")]
[TestCase("192.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) public void should_return_false_for_public_ip_address(string ipAddress)
{ {
IPAddress.Parse(ipAddress).IsLocalAddress().Should().BeFalse(); 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 FluentAssertions;
using NLog; using NLog;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Instrumentation.Sentry; using NzbDrone.Common.Instrumentation.Sentry;
using NzbDrone.Test.Common; using NzbDrone.Test.Common;
@@ -28,7 +27,7 @@ namespace NzbDrone.Common.Test.InstrumentationTests
[SetUp] [SetUp]
public void 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) private LogEventInfo GivenLogEvent(LogLevel level, Exception ex, string message)

View File

@@ -1,5 +1,6 @@
using System; using System;
using System.IO; using System.IO;
using System.Text;
using FluentAssertions; using FluentAssertions;
using Moq; using Moq;
using NUnit.Framework; using NUnit.Framework;
@@ -316,5 +317,14 @@ namespace NzbDrone.Common.Test
result[2].Should().Be(@"Music"); result[2].Should().Be(@"Music");
result[3].Should().Be(@"Author Title"); 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) public void CreateZip(string path, IEnumerable<string> files)
{ {
_logger.Debug("Creating archive {0}", path); using (var zipFile = ZipFile.Create(path))
using var zipFile = ZipFile.Create(path);
zipFile.BeginUpdate();
foreach (var file in files)
{ {
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) private void ExtractZip(string compressedFile, string destination)

View File

@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.IO.Abstractions; using System.IO.Abstractions;
using System.Linq; using System.Linq;
using System.Threading;
using NLog; using NLog;
using NzbDrone.Common.EnsureThat; using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
@@ -307,26 +306,9 @@ namespace NzbDrone.Common.Disk
{ {
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs); Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
var files = GetFiles(path, recursive).ToList(); var files = GetFiles(path, recursive);
files.ForEach(RemoveReadOnly); files.ToList().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();
}
_fileSystem.Directory.Delete(path, recursive); _fileSystem.Directory.Delete(path, recursive);
} }

View File

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

View File

@@ -39,24 +39,18 @@ namespace NzbDrone.Common.Extensions
private static bool IsLocalIPv4(byte[] ipv4Bytes) 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) // 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) // 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) // 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) // 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; 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;
} }
} }
} }

View File

@@ -60,6 +60,10 @@ namespace NzbDrone.Common.Extensions
public static bool PathEquals(this string firstPath, string secondPath, StringComparison? comparison = null) 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) if (!comparison.HasValue)
{ {
comparison = DiskProviderBase.PathStringComparison; comparison = DiskProviderBase.PathStringComparison;
@@ -108,15 +112,6 @@ namespace NzbDrone.Common.Extensions
return Directory.GetParent(cleanPath)?.FullName; 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) public static bool IsParentPath(this string parentPath, string childPath)
{ {
if (parentPath != "/" && !parentPath.EndsWith(":\\")) if (parentPath != "/" && !parentPath.EndsWith(":\\"))

View File

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

View File

@@ -4,27 +4,27 @@ namespace NzbDrone.Common.Instrumentation.Extensions
{ {
public static class LoggerExtensions public static class LoggerExtensions
{ {
[MessageTemplateFormatMethod("message")]
public static void ProgressInfo(this Logger logger, string message, params object[] args) 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) 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) 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", ""); logEvent.Properties.Add("Status", "");
logger.Log(logEvent); logger.Log(logEvent);

View File

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

View File

@@ -9,7 +9,6 @@ using NLog;
using NLog.Common; using NLog.Common;
using NLog.Targets; using NLog.Targets;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using Sentry; using Sentry;
namespace NzbDrone.Common.Instrumentation.Sentry namespace NzbDrone.Common.Instrumentation.Sentry
@@ -100,7 +99,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
public bool FilterEvents { get; set; } public bool FilterEvents { get; set; }
public bool SentryEnabled { get; set; } public bool SentryEnabled { get; set; }
public SentryTarget(string dsn, IAppFolderInfo appFolderInfo) public SentryTarget(string dsn)
{ {
_sdk = SentrySdk.Init(o => _sdk = SentrySdk.Init(o =>
{ {
@@ -108,33 +107,9 @@ namespace NzbDrone.Common.Instrumentation.Sentry
o.AttachStacktrace = true; o.AttachStacktrace = true;
o.MaxBreadcrumbs = 200; o.MaxBreadcrumbs = 200;
o.Release = $"{BuildInfo.AppName}@{BuildInfo.Release}"; o.Release = $"{BuildInfo.AppName}@{BuildInfo.Release}";
o.SetBeforeSend(x => SentryCleanser.CleanseEvent(x)); o.BeforeSend = x => SentryCleanser.CleanseEvent(x);
o.SetBeforeBreadcrumb(x => SentryCleanser.CleanseBreadcrumb(x)); o.BeforeBreadcrumb = x => SentryCleanser.CleanseBreadcrumb(x);
o.Environment = BuildInfo.Branch; 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(); InitializeScope();
@@ -152,7 +127,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
{ {
SentrySdk.ConfigureScope(scope => SentrySdk.ConfigureScope(scope =>
{ {
scope.User = new SentryUser scope.User = new User
{ {
Id = HashUtil.AnonymousToken() Id = HashUtil.AnonymousToken()
}; };
@@ -194,7 +169,9 @@ namespace NzbDrone.Common.Instrumentation.Sentry
private void OnError(Exception ex) 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 response = webException.Response as HttpWebResponse;
var statusCode = response?.StatusCode; var statusCode = response?.StatusCode;
@@ -313,21 +290,13 @@ namespace NzbDrone.Common.Instrumentation.Sentry
} }
} }
var level = LoggingLevelMap[logEvent.Level];
var sentryEvent = new SentryEvent(logEvent.Exception) var sentryEvent = new SentryEvent(logEvent.Exception)
{ {
Level = level, Level = LoggingLevelMap[logEvent.Level],
Logger = logEvent.LoggerName, Logger = logEvent.LoggerName,
Message = logEvent.FormattedMessage 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.SetExtras(extras);
sentryEvent.SetFingerprint(fingerPrint); sentryEvent.SetFingerprint(fingerPrint);

View File

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

View File

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

View File

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

View File

@@ -17,7 +17,7 @@ namespace NzbDrone.Common.Reflection
public static List<Type> ImplementationsOf<T>(this Assembly assembly) 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) public static bool IsSimpleType(this Type type)
@@ -68,7 +68,7 @@ namespace NzbDrone.Common.Reflection
public static Type FindTypeByName(this Assembly assembly, string name) 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) public static bool HasAttribute<TAttribute>(this Type type)

View File

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

View File

@@ -478,37 +478,6 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests
downloadClientInfo.RemovesCompletedDownloads.Should().BeTrue(); 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\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(@"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")] [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() 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] [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://eu.httpbin.org/get")).Should().BeTrue();
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://google.com/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://localhost:8654/get")).Should().BeTrue();
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://172.21.0.1:8989/api/v3/indexer/schema")).Should().BeTrue();
} }
[Test] [Test]
@@ -32,7 +31,6 @@ namespace NzbDrone.Core.Test.Http
var settings = GetProxySettings(); var settings = GetProxySettings();
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://bing.com/get")).Should().BeFalse(); 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 namespace NzbDrone.Core.Test.MetadataSource.Goodreads
{ {
[TestFixture] [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> public class BookInfoProxyFixture : CoreTest<BookInfoProxy>
{ {
private MetadataProfile _metadataProfile; private MetadataProfile _metadataProfile;

View File

@@ -15,7 +15,7 @@ using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.MetadataSource.Goodreads namespace NzbDrone.Core.Test.MetadataSource.Goodreads
{ {
[TestFixture] [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> public class BookInfoProxySearchFixture : CoreTest<BookInfoProxy>
{ {
[SetUp] [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("Harry Potter and the sorcerer's stone a detailed summary", 72245296)]
[TestCase("B0192CTMYG", 61209488)] [TestCase("B0192CTMYG", 61209488)]
[TestCase("9780439554930", 3)] [TestCase("9780439554930", 48517161)]
public void successful_book_search(string title, int expected) public void successful_book_search(string title, int expected)
{ {
var result = Subject.Search(title); var result = Subject.Search(title);

View File

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

View File

@@ -66,19 +66,12 @@ namespace NzbDrone.Core.Backup
{ {
_logger.ProgressInfo("Starting Backup"); _logger.ProgressInfo("Starting Backup");
var backupFolder = GetBackupFolder(backupType);
_diskProvider.EnsureFolder(_backupTempFolder); _diskProvider.EnsureFolder(_backupTempFolder);
_diskProvider.EnsureFolder(backupFolder); _diskProvider.EnsureFolder(GetBackupFolder(backupType));
if (!_diskProvider.FolderWritable(backupFolder))
{
throw new UnauthorizedAccessException($"Backup folder {backupFolder} is not writable");
}
var dateNow = DateTime.Now; var dateNow = DateTime.Now;
var backupFilename = $"readarr_backup_v{BuildInfo.Version}_{dateNow:yyyy.MM.dd_HH.mm.ss}.zip"; 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(); 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); _logger.Error("ReadarrId {0} was not found, it may have been removed from Goodreads.", newAuthor.Metadata.Value.ForeignAuthorId);
throw new ValidationException(new List<ValidationFailure> 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); author.ApplyChanges(newAuthor);

View File

@@ -222,7 +222,7 @@ namespace NzbDrone.Core.Books
protected override void DeleteEntity(Author local, bool deleteFiles) 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) 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) 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) protected override List<Edition> GetRemoteChildren(Book local, Book remote)

View File

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

View File

@@ -53,7 +53,6 @@ namespace NzbDrone.Core.Configuration
string SyslogServer { get; } string SyslogServer { get; }
int SyslogPort { get; } int SyslogPort { get; }
string SyslogLevel { get; } string SyslogLevel { get; }
string Theme { get; }
string PostgresHost { get; } string PostgresHost { get; }
int PostgresPort { get; } int PostgresPort { get; }
string PostgresUser { get; } string PostgresUser { get; }
@@ -61,7 +60,7 @@ namespace NzbDrone.Core.Configuration
string PostgresMainDb { get; } string PostgresMainDb { get; }
string PostgresLogDb { get; } string PostgresLogDb { get; }
string PostgresCacheDb { get; } string PostgresCacheDb { get; }
bool TrustCgnatIpAddresses { get; } string Theme { get; }
} }
public class ConfigFileProvider : IConfigFileProvider public class ConfigFileProvider : IConfigFileProvider
@@ -254,21 +253,7 @@ namespace NzbDrone.Core.Configuration
} }
public string UiFolder => BuildInfo.IsDebug ? Path.Combine("..", "UI") : "UI"; public string UiFolder => BuildInfo.IsDebug ? Path.Combine("..", "UI") : "UI";
public string InstanceName => _appOptions.InstanceName ?? GetValue("InstanceName", BuildInfo.AppName);
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 bool UpdateAutomatically => _updateOptions.Automatically ?? GetValueBoolean("UpdateAutomatically", OsInfo.IsWindows, false); public bool UpdateAutomatically => _updateOptions.Automatically ?? GetValueBoolean("UpdateAutomatically", OsInfo.IsWindows, false);
@@ -477,7 +462,5 @@ namespace NzbDrone.Core.Configuration
{ {
SetValue("ApiKey", GenerateApiKey()); 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 string ApplicationUrl => GetValue("ApplicationUrl", string.Empty);
public bool TrustCgnatIpAddresses
{
get { return GetValueBoolean("TrustCgnatIpAddresses", false); }
set { SetValue("TrustCgnatIpAddresses", value); }
}
private string GetValue(string key) private string GetValue(string key)
{ {
return GetValue(key, string.Empty); return GetValue(key, string.Empty);

View File

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

View File

@@ -40,7 +40,6 @@ namespace NzbDrone.Core.Datastore
Environment.SetEnvironmentVariable("No_Expand", "true"); Environment.SetEnvironmentVariable("No_Expand", "true");
Environment.SetEnvironmentVariable("No_SQLiteXmlConfigFile", "true"); Environment.SetEnvironmentVariable("No_SQLiteXmlConfigFile", "true");
Environment.SetEnvironmentVariable("No_PreLoadSQLite", "true"); Environment.SetEnvironmentVariable("No_PreLoadSQLite", "true");
Environment.SetEnvironmentVariable("No_SQLiteFunctions", "true");
} }
public DbFactory(IMigrationController migrationController, 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. // 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). // 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) switch (torrent.State)
{ {
@@ -248,8 +248,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
item.Message = "qBittorrent is reporting an error"; item.Message = "qBittorrent is reporting an error";
break; break;
case "stoppedDL": // torrent is stopped and has NOT finished downloading case "pausedDL": // torrent is paused and has NOT finished downloading
case "pausedDL": // torrent is paused and has NOT finished downloading (qBittorrent < 5)
item.Status = DownloadItemStatus.Paused; item.Status = DownloadItemStatus.Paused;
break; break;
@@ -260,8 +259,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
item.Status = DownloadItemStatus.Queued; item.Status = DownloadItemStatus.Queued;
break; break;
case "pausedUP": // torrent is paused and has finished downloading (qBittorent < 5) case "pausedUP": // torrent is paused and has finished downloading
case "stoppedUP": // torrent is stopped and has finished downloading
case "uploading": // torrent is being seeded and data is being transferred case "uploading": // torrent is being seeded and data is being transferred
case "stalledUP": // torrent is being seeded, but no connection were made case "stalledUP": // torrent is being seeded, but no connection were made
case "queuedUP": // queuing is enabled and torrent is queued for upload 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 >= 0)
{ {
if (torrent.RatioLimit - torrent.Ratio <= 0.001f) if (torrent.Ratio >= torrent.RatioLimit)
{ {
return true; return true;
} }
} }
else if (torrent.RatioLimit == -2 && config.MaxRatioEnabled) else if (torrent.RatioLimit == -2 && config.MaxRatioEnabled)
{ {
if (config.MaxRatio - torrent.Ratio <= 0.001f) if (Math.Round(torrent.Ratio, 2) >= config.MaxRatio)
{ {
return true; return true;
} }

View File

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

View File

@@ -148,7 +148,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
{ {
request.AddFormParameter("paused", false); request.AddFormParameter("paused", false);
} }
else if ((QBittorrentState)settings.InitialState == QBittorrentState.Stop) else if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause)
{ {
request.AddFormParameter("paused", true); request.AddFormParameter("paused", true);
} }
@@ -178,7 +178,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
{ {
request.AddFormParameter("paused", false); request.AddFormParameter("paused", false);
} }
else if ((QBittorrentState)settings.InitialState == QBittorrentState.Stop) else if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause)
{ {
request.AddFormParameter("paused", true); request.AddFormParameter("paused", true);
} }
@@ -214,7 +214,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
catch (DownloadClientException ex) catch (DownloadClientException ex)
{ {
// if setCategory fails due to method not being found, then try older setLabel command for qBittorrent < v.3.3.5 // 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") var setLabelRequest = BuildRequest(settings).Resource("/command/setLabel")
.Post() .Post()
@@ -257,7 +257,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
catch (DownloadClientException ex) catch (DownloadClientException ex)
{ {
// qBittorrent rejects all Prio commands with 403: Forbidden if Options -> BitTorrent -> Torrent Queueing is not enabled // 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; 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) public void SetForceStart(string hash, bool enabled, QBittorrentSettings settings)
{ {
var request = BuildRequest(settings).Resource("/command/setForceStart") var request = BuildRequest(settings).Resource("/command/setForceStart")

View File

@@ -246,20 +246,14 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
request.AddFormParameter("category", settings.MusicCategory); request.AddFormParameter("category", settings.MusicCategory);
} }
// Avoid extraneous API version check if initial state is ForceStart // Note: ForceStart is handled by separate api call
if ((QBittorrentState)settings.InitialState is QBittorrentState.Start or QBittorrentState.Stop) if ((QBittorrentState)settings.InitialState == QBittorrentState.Start)
{ {
var stoppedParameterName = GetApiVersion(settings) >= new Version(2, 11, 0) ? "stopped" : "paused"; request.AddFormParameter("paused", false);
}
// Note: ForceStart is handled by separate api call else if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause)
if ((QBittorrentState)settings.InitialState == QBittorrentState.Start) {
{ request.AddFormParameter("paused", true);
request.AddFormParameter(stoppedParameterName, false);
}
else if ((QBittorrentState)settings.InitialState == QBittorrentState.Stop)
{
request.AddFormParameter(stoppedParameterName, true);
}
} }
if (settings.SequentialOrder) if (settings.SequentialOrder)
@@ -297,7 +291,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
catch (DownloadClientException ex) 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 // 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; return;
} }
@@ -319,7 +313,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
catch (DownloadClientException ex) catch (DownloadClientException ex)
{ {
// qBittorrent rejects all Prio commands with 409: Conflict if Options -> BitTorrent -> Torrent Queueing is not enabled // 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; 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) public void SetForceStart(string hash, bool enabled, QBittorrentSettings settings)
{ {
var request = BuildRequest(settings).Resource("/api/v2/torrents/setForceStart") 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 namespace NzbDrone.Core.Download.Clients.QBittorrent
{ {
public enum QBittorrentState public enum QBittorrentState
{ {
[FieldOption(Label = "Started")]
Start = 0, Start = 0,
[FieldOption(Label = "Force Started")]
ForceStart = 1, ForceStart = 1,
Pause = 2
[FieldOption(Label = "Stopped")]
Stop = 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.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; return status;
} }
@@ -505,43 +518,6 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
return categories.Contains(category); 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) private bool ValidatePath(DownloadClientItem downloadClientItem)
{ {
var downloadItemOutputPath = downloadClientItem.OutputPath; 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 enable_date_sorting { get; set; }
public bool pre_check { get; set; } public bool pre_check { get; set; }
public string history_retention { 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 public class SabnzbdCategory

View File

@@ -4,7 +4,6 @@ using System.Net;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using NLog; using NLog;
using NzbDrone.Common.Cache; using NzbDrone.Common.Cache;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Common.Serializer; using NzbDrone.Common.Serializer;
@@ -209,7 +208,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission
private void AuthenticateClient(HttpRequestBuilder requestBuilder, TransmissionSettings settings, bool reauthenticate = false) 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); var sessionId = _authSessionIDCache.Find(authKey);
@@ -221,26 +220,24 @@ namespace NzbDrone.Core.Download.Clients.Transmission
authLoginRequest.SuppressHttpError = true; authLoginRequest.SuppressHttpError = true;
var response = _httpClient.Execute(authLoginRequest); var response = _httpClient.Execute(authLoginRequest);
if (response.StatusCode == HttpStatusCode.MovedPermanently)
switch (response.StatusCode)
{ {
case HttpStatusCode.MovedPermanently: var url = response.Headers.GetSingleValue("Location");
var url = response.Headers.GetSingleValue("Location");
throw new DownloadClientException("Remote site redirected to " + url); 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."); else if (response.StatusCode == HttpStatusCode.Conflict)
case HttpStatusCode.Conflict: {
sessionId = response.Headers.GetSingleValue("X-Transmission-Session-Id"); sessionId = response.Headers.GetSingleValue("X-Transmission-Session-Id");
if (sessionId == null) if (sessionId == null)
{ {
throw new DownloadClientException("Remote host did not return a Session Id."); throw new DownloadClientException("Remote host did not return a Session Id.");
} }
}
break; else
default: {
throw new DownloadClientAuthenticationException("Failed to authenticate with Transmission."); throw new DownloadClientAuthenticationException("Failed to authenticate with Transmission.");
} }
_logger.Debug("Transmission authentication succeeded."); _logger.Debug("Transmission authentication succeeded.");

View File

@@ -4,24 +4,24 @@ namespace NzbDrone.Core.Exceptions
{ {
public class AuthorNotFoundException : NzbDroneException public class AuthorNotFoundException : NzbDroneException
{ {
public string ForeignAuthorId { get; set; } public string MusicBrainzId { get; set; }
public AuthorNotFoundException(string foreignAuthorId) public AuthorNotFoundException(string musicbrainzId)
: base($"Author with id {foreignAuthorId} was not found, it may have been removed from the metadata server.") : 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) : base(message, args)
{ {
ForeignAuthorId = foreignAuthorId; MusicBrainzId = musicbrainzId;
} }
public AuthorNotFoundException(string foreignAuthorId, string message) public AuthorNotFoundException(string musicbrainzId, string message)
: base(message) : base(message)
{ {
ForeignAuthorId = foreignAuthorId; MusicBrainzId = musicbrainzId;
} }
} }
} }

View File

@@ -4,24 +4,24 @@ namespace NzbDrone.Core.Exceptions
{ {
public class BookNotFoundException : NzbDroneException public class BookNotFoundException : NzbDroneException
{ {
public string ForeignBookId { get; set; } public string MusicBrainzId { get; set; }
public BookNotFoundException(string foreignBookId) public BookNotFoundException(string musicbrainzId)
: base($"Book with id {foreignBookId} was not found, it may have been removed from metadata server.") : 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) : base(message, args)
{ {
ForeignBookId = foreignBookId; MusicBrainzId = musicbrainzId;
} }
public BookNotFoundException(string foreignBookId, string message) public BookNotFoundException(string musicbrainzId, string message)
: base(message) : base(message)
{ {
ForeignBookId = foreignBookId; MusicBrainzId = musicbrainzId;
} }
} }
} }

View File

@@ -4,24 +4,24 @@ namespace NzbDrone.Core.Exceptions
{ {
public class EditionNotFoundException : NzbDroneException public class EditionNotFoundException : NzbDroneException
{ {
public string ForeignEditionId { get; set; } public string MusicBrainzId { get; set; }
public EditionNotFoundException(string foreignEditionId) public EditionNotFoundException(string musicbrainzId)
: base($"Edition with id {foreignEditionId} was not found, it may have been removed from metadata server.") : 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) : base(message, args)
{ {
ForeignEditionId = foreignEditionId; MusicBrainzId = musicbrainzId;
} }
public EditionNotFoundException(string foreignEditionId, string message) public EditionNotFoundException(string musicbrainzId, string message)
: base(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 DOWNLOAD_CLIENT = "downloadClient";
public const string RELEASE_SOURCE = "releaseSource"; public const string RELEASE_SOURCE = "releaseSource";
public const string RELEASE_GROUP = "releaseGroup";
public const string SIZE = "size";
public const string INDEXER = "indexer";
public EntityHistory() public EntityHistory()
{ {

View File

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

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