mirror of
https://github.com/Readarr/Readarr.git
synced 2026-03-14 15:44:20 -04:00
Compare commits
1 Commits
v0.4.13.27
...
sonarr-pul
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d48235c125 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -120,7 +120,6 @@ _artifacts
|
||||
_rawPackage/
|
||||
_dotTrace*
|
||||
_tests/
|
||||
_temp*
|
||||
*.Result.xml
|
||||
coverage*.xml
|
||||
coverage*.json
|
||||
|
||||
@@ -9,18 +9,18 @@ variables:
|
||||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '0.4.13'
|
||||
majorVersion: '0.4.0'
|
||||
minorVersion: $[counter('minorVersion', 1)]
|
||||
readarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(readarrVersion)'
|
||||
sentryOrg: 'servarr'
|
||||
sentryUrl: 'https://sentry.servarr.com'
|
||||
dotnetVersion: '6.0.427'
|
||||
dotnetVersion: '6.0.424'
|
||||
nodeVersion: '20.X'
|
||||
innoVersion: '6.2.0'
|
||||
windowsImage: 'windows-2022'
|
||||
linuxImage: 'ubuntu-20.04'
|
||||
macImage: 'macOS-13'
|
||||
macImage: 'macOS-12'
|
||||
|
||||
trigger:
|
||||
branches:
|
||||
@@ -1102,19 +1102,19 @@ stages:
|
||||
vmImage: ${{ variables.windowsImage }}
|
||||
steps:
|
||||
- checkout: self # Need history for Sonar analysis
|
||||
- task: SonarCloudPrepare@3
|
||||
- task: SonarCloudPrepare@2
|
||||
env:
|
||||
SONAR_SCANNER_OPTS: ''
|
||||
inputs:
|
||||
SonarCloud: 'SonarCloud'
|
||||
organization: 'readarr'
|
||||
scannerMode: 'cli'
|
||||
scannerMode: 'CLI'
|
||||
configMode: 'manual'
|
||||
cliProjectKey: 'readarrui'
|
||||
cliProjectName: 'ReadarrUI'
|
||||
cliProjectVersion: '$(readarrVersion)'
|
||||
cliSources: './frontend'
|
||||
- task: SonarCloudAnalyze@3
|
||||
- task: SonarCloudAnalyze@2
|
||||
|
||||
- job: Api_Docs
|
||||
displayName: API Docs
|
||||
@@ -1190,12 +1190,12 @@ stages:
|
||||
submodules: true
|
||||
- powershell: Set-Service SCardSvr -StartupType Manual
|
||||
displayName: Enable Windows Test Service
|
||||
- task: SonarCloudPrepare@3
|
||||
- task: SonarCloudPrepare@2
|
||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||
inputs:
|
||||
SonarCloud: 'SonarCloud'
|
||||
organization: 'readarr'
|
||||
scannerMode: 'dotnet'
|
||||
scannerMode: 'MSBuild'
|
||||
projectKey: 'Readarr_Readarr'
|
||||
projectName: 'Readarr'
|
||||
projectVersion: '$(readarrVersion)'
|
||||
@@ -1208,10 +1208,10 @@ stages:
|
||||
./build.sh --backend -f net6.0 -r win-x64
|
||||
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
|
||||
displayName: Coverage Unit Tests
|
||||
- task: SonarCloudAnalyze@3
|
||||
- task: SonarCloudAnalyze@2
|
||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||
displayName: Publish SonarCloud Results
|
||||
- task: reportgenerator@5.3.11
|
||||
- task: reportgenerator@5
|
||||
displayName: Generate Coverage Report
|
||||
inputs:
|
||||
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'
|
||||
|
||||
2
frontend/.vscode/settings.json
vendored
2
frontend/.vscode/settings.json
vendored
@@ -9,7 +9,7 @@
|
||||
|
||||
"editor.formatOnSave": false,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": "explicit"
|
||||
"source.fixAll": true
|
||||
},
|
||||
|
||||
"typescript.preferences.quoteStyle": "single",
|
||||
|
||||
@@ -26,7 +26,6 @@ module.exports = (env) => {
|
||||
const config = {
|
||||
mode: isProduction ? 'production' : 'development',
|
||||
devtool: isProduction ? 'source-map' : 'eval-source-map',
|
||||
target: 'web',
|
||||
|
||||
stats: {
|
||||
children: false
|
||||
@@ -68,7 +67,7 @@ module.exports = (env) => {
|
||||
output: {
|
||||
path: distFolder,
|
||||
publicPath: '/',
|
||||
filename: isProduction ? '[name]-[contenthash].js' : '[name].js',
|
||||
filename: '[name]-[contenthash].js',
|
||||
sourceMapFilename: '[file].map'
|
||||
},
|
||||
|
||||
@@ -93,7 +92,7 @@ module.exports = (env) => {
|
||||
|
||||
new MiniCssExtractPlugin({
|
||||
filename: 'Content/styles.css',
|
||||
chunkFilename: isProduction ? 'Content/[id]-[chunkhash].css' : 'Content/[id].css'
|
||||
chunkFilename: 'Content/[id]-[chunkhash].css'
|
||||
}),
|
||||
|
||||
new HtmlWebpackPlugin({
|
||||
@@ -182,7 +181,7 @@ module.exports = (env) => {
|
||||
loose: true,
|
||||
debug: false,
|
||||
useBuiltIns: 'entry',
|
||||
corejs: '3.39'
|
||||
corejs: 3
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -203,7 +202,7 @@ module.exports = (env) => {
|
||||
options: {
|
||||
importLoaders: 1,
|
||||
modules: {
|
||||
localIdentName: isProduction ? '[name]/[local]/[hash:base64:5]' : '[name]/[local]'
|
||||
localIdentName: '[name]/[local]/[hash:base64:5]'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -165,8 +165,7 @@ function HistoryDetails(props) {
|
||||
|
||||
if (eventType === 'downloadFailed') {
|
||||
const {
|
||||
message,
|
||||
indexer
|
||||
message
|
||||
} = data;
|
||||
|
||||
return (
|
||||
@@ -178,21 +177,11 @@ function HistoryDetails(props) {
|
||||
/>
|
||||
|
||||
{
|
||||
indexer ?
|
||||
<DescriptionListItem
|
||||
title={translate('Indexer')}
|
||||
data={indexer}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
message ?
|
||||
!!message &&
|
||||
<DescriptionListItem
|
||||
title={translate('Message')}
|
||||
data={message}
|
||||
/> :
|
||||
null
|
||||
/>
|
||||
}
|
||||
</DescriptionList>
|
||||
);
|
||||
|
||||
@@ -32,7 +32,7 @@ import LogsTableConnector from 'System/Events/LogsTableConnector';
|
||||
import Logs from 'System/Logs/Logs';
|
||||
import Status from 'System/Status/Status';
|
||||
import Tasks from 'System/Tasks/Tasks';
|
||||
import Updates from 'System/Updates/Updates';
|
||||
import UpdatesConnector from 'System/Updates/UpdatesConnector';
|
||||
import UnmappedFilesTableConnector from 'UnmappedFiles/UnmappedFilesTableConnector';
|
||||
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
||||
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
|
||||
@@ -247,7 +247,7 @@ function AppRoutes(props) {
|
||||
|
||||
<Route
|
||||
path="/system/updates"
|
||||
component={Updates}
|
||||
component={UpdatesConnector}
|
||||
/>
|
||||
|
||||
<Route
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import AuthorsAppState from './AuthorsAppState';
|
||||
import CommandAppState from './CommandAppState';
|
||||
import SettingsAppState from './SettingsAppState';
|
||||
import SystemAppState from './SystemAppState';
|
||||
import TagsAppState from './TagsAppState';
|
||||
|
||||
interface FilterBuilderPropOption {
|
||||
@@ -36,24 +35,10 @@ export interface CustomFilter {
|
||||
filers: PropertyFilter[];
|
||||
}
|
||||
|
||||
export interface AppSectionState {
|
||||
isConnected: boolean;
|
||||
isReconnecting: boolean;
|
||||
version: string;
|
||||
prevVersion?: string;
|
||||
dimensions: {
|
||||
isSmallScreen: boolean;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface AppState {
|
||||
app: AppSectionState;
|
||||
authors: AuthorsAppState;
|
||||
commands: CommandAppState;
|
||||
settings: SettingsAppState;
|
||||
system: SystemAppState;
|
||||
tags: TagsAppState;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import AppSectionState, {
|
||||
AppSectionDeleteState,
|
||||
AppSectionItemState,
|
||||
AppSectionSaveState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import DownloadClient from 'typings/DownloadClient';
|
||||
@@ -8,16 +7,13 @@ import ImportList from 'typings/ImportList';
|
||||
import Indexer from 'typings/Indexer';
|
||||
import IndexerFlag from 'typings/IndexerFlag';
|
||||
import Notification from 'typings/Notification';
|
||||
import General from 'typings/Settings/General';
|
||||
import UiSettings from 'typings/Settings/UiSettings';
|
||||
import { UiSettings } from 'typings/UiSettings';
|
||||
|
||||
export interface DownloadClientAppState
|
||||
extends AppSectionState<DownloadClient>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {}
|
||||
|
||||
export type GeneralAppState = AppSectionItemState<General>;
|
||||
|
||||
export interface ImportListAppState
|
||||
extends AppSectionState<ImportList>,
|
||||
AppSectionDeleteState,
|
||||
@@ -37,12 +33,11 @@ export type UiSettingsAppState = AppSectionState<UiSettings>;
|
||||
|
||||
interface SettingsAppState {
|
||||
downloadClients: DownloadClientAppState;
|
||||
general: GeneralAppState;
|
||||
importLists: ImportListAppState;
|
||||
indexerFlags: IndexerFlagSettingsAppState;
|
||||
indexers: IndexerAppState;
|
||||
notifications: NotificationAppState;
|
||||
ui: UiSettingsAppState;
|
||||
uiSettings: UiSettingsAppState;
|
||||
}
|
||||
|
||||
export default SettingsAppState;
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import SystemStatus from 'typings/SystemStatus';
|
||||
import Update from 'typings/Update';
|
||||
import AppSectionState, { AppSectionItemState } from './AppSectionState';
|
||||
|
||||
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
|
||||
export type UpdateAppState = AppSectionState<Update>;
|
||||
|
||||
interface SystemAppState {
|
||||
updates: UpdateAppState;
|
||||
status: SystemStatusAppState;
|
||||
}
|
||||
|
||||
export default SystemAppState;
|
||||
@@ -1,7 +1,5 @@
|
||||
import ModelBase from 'App/ModelBase';
|
||||
|
||||
export type AuthorStatus = 'continuing' | 'ended';
|
||||
|
||||
interface Author extends ModelBase {
|
||||
added: string;
|
||||
genres: string[];
|
||||
@@ -12,7 +10,6 @@ interface Author extends ModelBase {
|
||||
metadataProfileId: number;
|
||||
rootFolderPath: string;
|
||||
sortName: string;
|
||||
status: AuthorStatus;
|
||||
tags: number[];
|
||||
authorName: string;
|
||||
isSaving?: boolean;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import AuthorHistoryTable from 'Author/History/AuthorHistoryTable';
|
||||
import MonitoringOptionsModal from 'Author/MonitoringOptions/MonitoringOptionsModal';
|
||||
import BookEditorFooter from 'Book/Editor/BookEditorFooter';
|
||||
import BookFileEditorTable from 'BookFile/Editor/BookFileEditorTable';
|
||||
import Alert from 'Components/Alert';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Link from 'Components/Link/Link';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
@@ -18,7 +17,7 @@ import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import SwipeHeaderConnector from 'Components/Swipe/SwipeHeaderConnector';
|
||||
import { align, icons, kinds } from 'Helpers/Props';
|
||||
import { align, icons } from 'Helpers/Props';
|
||||
import InteractiveSearchFilterMenuConnector from 'InteractiveSearch/InteractiveSearchFilterMenuConnector';
|
||||
import InteractiveSearchTable from 'InteractiveSearch/InteractiveSearchTable';
|
||||
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
|
||||
@@ -413,25 +412,22 @@ class AuthorDetails extends Component {
|
||||
|
||||
<div className={styles.contentContainer}>
|
||||
{
|
||||
!isPopulated && !booksError && !bookFilesError ?
|
||||
<LoadingIndicator /> :
|
||||
null
|
||||
!isPopulated && !booksError && !bookFilesError &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && booksError ?
|
||||
<Alert kind={kinds.DANGER}>
|
||||
!isFetching && booksError &&
|
||||
<div>
|
||||
{translate('LoadingBooksFailed')}
|
||||
</Alert> :
|
||||
null
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && bookFilesError ?
|
||||
<Alert kind={kinds.DANGER}>
|
||||
!isFetching && bookFilesError &&
|
||||
<div>
|
||||
{translate('LoadingBookFilesFailed')}
|
||||
</Alert> :
|
||||
null
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import TextTruncate from 'react-text-truncate';
|
||||
import AuthorPoster from 'Author/AuthorPoster';
|
||||
import { getAuthorStatusDetails } from 'Author/AuthorStatus';
|
||||
import HeartRating from 'Components/HeartRating';
|
||||
import Icon from 'Components/Icon';
|
||||
import Label from 'Components/Label';
|
||||
@@ -12,7 +11,7 @@ import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
|
||||
import QualityProfileName from 'Settings/Profiles/Quality/QualityProfileName';
|
||||
import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector';
|
||||
import fonts from 'Styles/Variables/fonts';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import stripHtml from 'Utilities/String/stripHtml';
|
||||
@@ -88,11 +87,11 @@ class AuthorDetailsHeader extends Component {
|
||||
titleWidth
|
||||
} = this.state;
|
||||
|
||||
const statusDetails = getAuthorStatusDetails(status);
|
||||
|
||||
const fanartUrl = getFanartUrl(images);
|
||||
const marqueeWidth = titleWidth - (isSmallScreen ? 85 : 160);
|
||||
|
||||
const continuing = status === 'continuing';
|
||||
|
||||
let bookFilesCountMessage = translate('BookFilesCountMessage');
|
||||
|
||||
if (bookFileCount === 1) {
|
||||
@@ -214,7 +213,7 @@ class AuthorDetailsHeader extends Component {
|
||||
|
||||
<span className={styles.qualityProfileName}>
|
||||
{
|
||||
<QualityProfileName
|
||||
<QualityProfileNameConnector
|
||||
qualityProfileId={qualityProfileId}
|
||||
/>
|
||||
}
|
||||
@@ -237,16 +236,16 @@ class AuthorDetailsHeader extends Component {
|
||||
|
||||
<Label
|
||||
className={styles.detailsLabel}
|
||||
title={statusDetails.message}
|
||||
title={continuing ? translate('ContinuingMoreBooksAreExpected') : translate('ContinuingNoAdditionalBooksAreExpected')}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<Icon
|
||||
name={statusDetails.icon}
|
||||
name={continuing ? icons.AUTHOR_CONTINUING : icons.AUTHOR_ENDED}
|
||||
size={17}
|
||||
/>
|
||||
|
||||
<span className={styles.qualityProfileName}>
|
||||
{statusDetails.title}
|
||||
{continuing ? 'Continuing' : 'Deceased'}
|
||||
</span>
|
||||
</Label>
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import dimensions from 'Styles/Variables/dimensions';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AuthorIndexOverviewInfoRow from './AuthorIndexOverviewInfoRow';
|
||||
import styles from './AuthorIndexOverviewInfo.css';
|
||||
|
||||
@@ -77,9 +76,9 @@ function getInfoRowProps(row, props) {
|
||||
};
|
||||
}
|
||||
|
||||
if (name === 'qualityProfileId' && !!props.qualityProfile?.name) {
|
||||
if (name === 'qualityProfileId') {
|
||||
return {
|
||||
title: translate('QualityProfile'),
|
||||
title: 'Quality Profile',
|
||||
iconName: icons.PROFILE,
|
||||
label: props.qualityProfile.name
|
||||
};
|
||||
|
||||
@@ -235,12 +235,12 @@ class AuthorIndexPoster extends Component {
|
||||
</div>
|
||||
}
|
||||
|
||||
{showQualityProfile && !!qualityProfile?.name ? (
|
||||
<div className={styles.title} title={translate('QualityProfile')}>
|
||||
{qualityProfile.name}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{
|
||||
showQualityProfile &&
|
||||
<div className={styles.title}>
|
||||
{qualityProfile.name}
|
||||
</div>
|
||||
}
|
||||
{
|
||||
nextAiring &&
|
||||
<div className={styles.nextAiring}>
|
||||
|
||||
@@ -209,7 +209,7 @@ class AuthorIndexRow extends Component {
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
{qualityProfile?.name ?? ''}
|
||||
{qualityProfile.name}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
@@ -220,7 +220,7 @@ class AuthorIndexRow extends Component {
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
{metadataProfile?.name ?? ''}
|
||||
{metadataProfile.name}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { getAuthorStatusDetails } from 'Author/AuthorStatus';
|
||||
import Icon from 'Components/Icon';
|
||||
import VirtualTableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import { icons } from 'Helpers/Props';
|
||||
@@ -16,8 +15,6 @@ function AuthorStatusCell(props) {
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const statusDetails = getAuthorStatusDetails(status);
|
||||
|
||||
return (
|
||||
<Component
|
||||
className={className}
|
||||
@@ -31,8 +28,8 @@ function AuthorStatusCell(props) {
|
||||
|
||||
<Icon
|
||||
className={styles.statusIcon}
|
||||
name={statusDetails.icon}
|
||||
title={`${statusDetails.title}: ${statusDetails.message}`}
|
||||
name={status === 'ended' ? icons.AUTHOR_ENDED : icons.AUTHOR_CONTINUING}
|
||||
title={status === 'ended' ? translate('StatusEndedDeceased') : translate('StatusEndedContinuing')}
|
||||
/>
|
||||
</Component>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,6 @@ import dimensions from 'Styles/Variables/dimensions';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import BookIndexOverviewInfoRow from './BookIndexOverviewInfoRow';
|
||||
import styles from './BookIndexOverviewInfo.css';
|
||||
|
||||
@@ -72,9 +71,9 @@ function getInfoRowProps(row, props) {
|
||||
};
|
||||
}
|
||||
|
||||
if (name === 'qualityProfileId' && !!props.qualityProfile?.name) {
|
||||
if (name === 'qualityProfileId') {
|
||||
return {
|
||||
title: translate('QualityProfile'),
|
||||
title: 'Quality Profile',
|
||||
iconName: icons.PROFILE,
|
||||
label: props.qualityProfile.name
|
||||
};
|
||||
|
||||
@@ -250,12 +250,12 @@ class BookIndexPoster extends Component {
|
||||
</div>
|
||||
}
|
||||
|
||||
{showQualityProfile && !!qualityProfile?.name ? (
|
||||
<div className={styles.title} title={translate('QualityProfile')}>
|
||||
{qualityProfile.name}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{
|
||||
showQualityProfile &&
|
||||
<div className={styles.title}>
|
||||
{qualityProfile.name}
|
||||
</div>
|
||||
}
|
||||
{
|
||||
nextAiring &&
|
||||
<div className={styles.nextAiring}>
|
||||
|
||||
@@ -195,7 +195,7 @@ class BookIndexRow extends Component {
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
{qualityProfile?.name ?? ''}
|
||||
{qualityProfile.name}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import AuthorNameLink from 'Author/AuthorNameLink';
|
||||
import { getAuthorStatusDetails } from 'Author/AuthorStatus';
|
||||
import Icon from 'Components/Icon';
|
||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import BookshelfBook from './BookshelfBook';
|
||||
import styles from './BookshelfRow.css';
|
||||
|
||||
@@ -29,8 +30,6 @@ class BookshelfRow extends Component {
|
||||
onBookMonitoredPress
|
||||
} = this.props;
|
||||
|
||||
const statusDetails = getAuthorStatusDetails(status);
|
||||
|
||||
return (
|
||||
<>
|
||||
<VirtualTableSelectCell
|
||||
@@ -53,8 +52,8 @@ class BookshelfRow extends Component {
|
||||
<VirtualTableRowCell className={styles.status}>
|
||||
<Icon
|
||||
className={styles.statusIcon}
|
||||
name={statusDetails.icon}
|
||||
title={statusDetails.title}
|
||||
name={status === 'ended' ? icons.AUTHOR_ENDED : icons.AUTHOR_CONTINUING}
|
||||
title={status === 'ended' ? translate('StatusEndedEnded') : translate('StatusEndedContinuing')}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
|
||||
|
||||
@@ -20,8 +20,6 @@ import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue';
|
||||
import TextInput from './TextInput';
|
||||
import styles from './EnhancedSelectInput.css';
|
||||
|
||||
const MINIMUM_DISTANCE_FROM_EDGE = 10;
|
||||
|
||||
function isArrowKey(keyCode) {
|
||||
return keyCode === keyCodes.UP_ARROW || keyCode === keyCodes.DOWN_ARROW;
|
||||
}
|
||||
@@ -139,9 +137,18 @@ class EnhancedSelectInput extends Component {
|
||||
// Listeners
|
||||
|
||||
onComputeMaxHeight = (data) => {
|
||||
const {
|
||||
top,
|
||||
bottom
|
||||
} = data.offsets.reference;
|
||||
|
||||
const windowHeight = window.innerHeight;
|
||||
|
||||
data.styles.maxHeight = windowHeight - MINIMUM_DISTANCE_FROM_EDGE;
|
||||
if ((/^botton/).test(data.placement)) {
|
||||
data.styles.maxHeight = windowHeight - bottom;
|
||||
} else {
|
||||
data.styles.maxHeight = top;
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
@@ -450,10 +457,6 @@ class EnhancedSelectInput extends Component {
|
||||
order: 851,
|
||||
enabled: true,
|
||||
fn: this.onComputeMaxHeight
|
||||
},
|
||||
preventOverflow: {
|
||||
enabled: true,
|
||||
boundariesElement: 'viewport'
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -83,6 +83,13 @@
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointMedium) {
|
||||
.modal.small,
|
||||
.modal.medium {
|
||||
width: 90%;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.modalContainer {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
|
||||
padding: 0;
|
||||
font-size: inherit;
|
||||
|
||||
&.isDisabled {
|
||||
color: var(--disabledColor);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.isDisabled {
|
||||
color: var(--disabledColor);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
line-height: 1.52857143;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointMedium) {
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.cell {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import styles from './TableRowCell.css';
|
||||
|
||||
class TableRowCell extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
children,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<td
|
||||
className={className}
|
||||
{...otherProps}
|
||||
>
|
||||
{children}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TableRowCell.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
children: PropTypes.oneOfType([PropTypes.string, PropTypes.node])
|
||||
};
|
||||
|
||||
TableRowCell.defaultProps = {
|
||||
className: styles.cell
|
||||
};
|
||||
|
||||
export default TableRowCell;
|
||||
11
frontend/src/Components/Table/Cells/TableRowCell.tsx
Normal file
11
frontend/src/Components/Table/Cells/TableRowCell.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React, { ComponentPropsWithoutRef } from 'react';
|
||||
import styles from './TableRowCell.css';
|
||||
|
||||
export interface TableRowCellprops extends ComponentPropsWithoutRef<'td'> {}
|
||||
|
||||
export default function TableRowCell({
|
||||
className = styles.cell,
|
||||
...tdProps
|
||||
}: TableRowCellprops) {
|
||||
return <td className={className} {...tdProps} />;
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointMedium) {
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.cell {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointMedium) {
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.tableContainer {
|
||||
min-width: 100%;
|
||||
width: fit-content;
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointMedium) {
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.headerCell {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointMedium) {
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.pager {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointMedium) {
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.headerCell {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -10,11 +10,11 @@ import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import Column from 'Components/Table/Column';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import useSelectState from 'Helpers/Hooks/useSelectState';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import SortDirection from 'Helpers/Props/SortDirection';
|
||||
import {
|
||||
bulkDeleteDownloadClients,
|
||||
bulkEditDownloadClients,
|
||||
@@ -35,7 +35,7 @@ type OnSelectedChangeCallback = React.ComponentProps<
|
||||
typeof ManageDownloadClientsModalRow
|
||||
>['onSelectedChange'];
|
||||
|
||||
const COLUMNS: Column[] = [
|
||||
const COLUMNS = [
|
||||
{
|
||||
name: 'name',
|
||||
label: () => translate('Name'),
|
||||
@@ -82,6 +82,8 @@ const COLUMNS: Column[] = [
|
||||
|
||||
interface ManageDownloadClientsModalContentProps {
|
||||
onModalClose(): void;
|
||||
sortKey?: string;
|
||||
sortDirection?: SortDirection;
|
||||
}
|
||||
|
||||
function ManageDownloadClientsModalContent(
|
||||
|
||||
@@ -18,6 +18,7 @@ function UpdateSettings(props) {
|
||||
const {
|
||||
advancedSettings,
|
||||
settings,
|
||||
isWindows,
|
||||
packageUpdateMechanism,
|
||||
onInputChange
|
||||
} = props;
|
||||
@@ -43,10 +44,10 @@ function UpdateSettings(props) {
|
||||
value: titleCase(packageUpdateMechanism)
|
||||
});
|
||||
} else {
|
||||
updateOptions.push({ key: 'builtIn', value: translate('BuiltIn') });
|
||||
updateOptions.push({ key: 'builtIn', value: 'Built-In' });
|
||||
}
|
||||
|
||||
updateOptions.push({ key: 'script', value: translate('Script') });
|
||||
updateOptions.push({ key: 'script', value: 'Script' });
|
||||
|
||||
return (
|
||||
<FieldSet legend={translate('Updates')}>
|
||||
@@ -59,8 +60,8 @@ function UpdateSettings(props) {
|
||||
<FormInputGroup
|
||||
type={inputTypes.AUTO_COMPLETE}
|
||||
name="branch"
|
||||
helpText={usingExternalUpdateMechanism ? translate('BranchUpdateMechanism') : translate('BranchUpdate')}
|
||||
helpLink="https://wiki.servarr.com/readarr/settings#updates"
|
||||
helpText={usingExternalUpdateMechanism ? translate('UsingExternalUpdateMechanismBranchUsedByExternalUpdateMechanism') : translate('UsingExternalUpdateMechanismBranchToUseToUpdateReadarr')}
|
||||
helpLink="https://wiki.servarr.com/readarr/faq#how-do-I-update-my-readarr"
|
||||
{...branch}
|
||||
values={branchValues}
|
||||
onChange={onInputChange}
|
||||
@@ -68,59 +69,62 @@ function UpdateSettings(props) {
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<div>
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>{translate('Automatic')}</FormLabel>
|
||||
{
|
||||
!isWindows &&
|
||||
<div>
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>{translate('Automatic')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="updateAutomatically"
|
||||
helpText={translate('UpdateAutomaticallyHelpText')}
|
||||
helpTextWarning={updateMechanism.value === 'docker' ? translate('AutomaticUpdatesDisabledDocker') : undefined}
|
||||
onChange={onInputChange}
|
||||
{...updateAutomatically}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="updateAutomatically"
|
||||
helpText={translate('UpdateAutomaticallyHelpText')}
|
||||
helpTextWarning={updateMechanism.value === 'docker' ? translate('AutomaticUpdatesDisabledDocker', { appName: 'Readarr' }) : undefined}
|
||||
onChange={onInputChange}
|
||||
{...updateAutomatically}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>{translate('Mechanism')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="updateMechanism"
|
||||
values={updateOptions}
|
||||
helpText={translate('UpdateMechanismHelpText')}
|
||||
helpLink="https://wiki.servarr.com/readarr/settings#updates"
|
||||
onChange={onInputChange}
|
||||
{...updateMechanism}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
updateMechanism.value === 'script' &&
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>{translate('ScriptPath')}</FormLabel>
|
||||
<FormLabel>{translate('Mechanism')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="updateScriptPath"
|
||||
helpText={translate('UpdateScriptPathHelpText')}
|
||||
type={inputTypes.SELECT}
|
||||
name="updateMechanism"
|
||||
values={updateOptions}
|
||||
helpText={translate('UpdateMechanismHelpText')}
|
||||
helpLink="https://wiki.servarr.com/readarr/faq#how-do-i-update-my-readarr"
|
||||
onChange={onInputChange}
|
||||
{...updateScriptPath}
|
||||
{...updateMechanism}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
</div>
|
||||
|
||||
{
|
||||
updateMechanism.value === 'script' &&
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>{translate('ScriptPath')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="updateScriptPath"
|
||||
helpText={translate('UpdateScriptPathHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...updateScriptPath}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ function EditImportListExclusionModalContent(props) {
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('ForeignId')}
|
||||
{translate('MusicbrainzId')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
|
||||
@@ -10,11 +10,11 @@ import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import Column from 'Components/Table/Column';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import useSelectState from 'Helpers/Hooks/useSelectState';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import SortDirection from 'Helpers/Props/SortDirection';
|
||||
import {
|
||||
bulkDeleteIndexers,
|
||||
bulkEditIndexers,
|
||||
@@ -35,7 +35,7 @@ type OnSelectedChangeCallback = React.ComponentProps<
|
||||
typeof ManageIndexersModalRow
|
||||
>['onSelectedChange'];
|
||||
|
||||
const COLUMNS: Column[] = [
|
||||
const COLUMNS = [
|
||||
{
|
||||
name: 'name',
|
||||
label: () => translate('Name'),
|
||||
@@ -82,6 +82,8 @@ const COLUMNS: Column[] = [
|
||||
|
||||
interface ManageIndexersModalContentProps {
|
||||
onModalClose(): void;
|
||||
sortKey?: string;
|
||||
sortDirection?: SortDirection;
|
||||
}
|
||||
|
||||
function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
@@ -14,11 +15,11 @@ function createMapStateToProps() {
|
||||
(state) => state.settings.advancedSettings,
|
||||
(state) => state.settings.namingExamples,
|
||||
createSettingsSectionSelector(SECTION),
|
||||
(advancedSettings, namingExamples, sectionSettings) => {
|
||||
(advancedSettings, examples, sectionSettings) => {
|
||||
return {
|
||||
advancedSettings,
|
||||
examples: namingExamples.item,
|
||||
examplesPopulated: namingExamples.isPopulated,
|
||||
examples: examples.item,
|
||||
examplesPopulated: !_.isEmpty(examples.item),
|
||||
...sectionSettings
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -45,12 +45,6 @@ export const defaultState = {
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'books.lastSearchTime',
|
||||
label: 'Last Searched',
|
||||
isSortable: true,
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
columnLabel: 'Actions',
|
||||
@@ -114,12 +108,6 @@ export const defaultState = {
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'books.lastSearchTime',
|
||||
label: 'Last Searched',
|
||||
isSortable: true,
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
columnLabel: 'Actions',
|
||||
|
||||
50
frontend/src/System/Updates/UpdateChanges.js
Normal file
50
frontend/src/System/Updates/UpdateChanges.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||
import styles from './UpdateChanges.css';
|
||||
|
||||
class UpdateChanges extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
title,
|
||||
changes
|
||||
} = this.props;
|
||||
|
||||
if (changes.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.title}>{title}</div>
|
||||
<ul>
|
||||
{
|
||||
changes.map((change, index) => {
|
||||
const checkChange = change.replace(/#\d{4,5}\b/g, (match, contents) => {
|
||||
return `[${match}](https://github.com/Readarr/Readarr/issues/${match.substring(1)})`;
|
||||
});
|
||||
|
||||
return (
|
||||
<li key={index}>
|
||||
<InlineMarkdown data={checkChange} />
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
UpdateChanges.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
changes: PropTypes.arrayOf(PropTypes.string)
|
||||
};
|
||||
|
||||
export default UpdateChanges;
|
||||
@@ -1,43 +0,0 @@
|
||||
import React from 'react';
|
||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||
import styles from './UpdateChanges.css';
|
||||
|
||||
interface UpdateChangesProps {
|
||||
title: string;
|
||||
changes: string[];
|
||||
}
|
||||
|
||||
function UpdateChanges(props: UpdateChangesProps) {
|
||||
const { title, changes } = props;
|
||||
|
||||
if (changes.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const uniqueChanges = [...new Set(changes)];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.title}>{title}</div>
|
||||
<ul>
|
||||
{uniqueChanges.map((change, index) => {
|
||||
const checkChange = change.replace(
|
||||
/#\d{4,5}\b/g,
|
||||
(match) =>
|
||||
`[${match}](https://github.com/Readarr/Readarr/issues/${match.substring(
|
||||
1
|
||||
)})`
|
||||
);
|
||||
|
||||
return (
|
||||
<li key={index}>
|
||||
<InlineMarkdown data={checkChange} />
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default UpdateChanges;
|
||||
252
frontend/src/System/Updates/Updates.js
Normal file
252
frontend/src/System/Updates/Updates.js
Normal file
@@ -0,0 +1,252 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import Icon from 'Components/Icon';
|
||||
import Label from 'Components/Label';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import formatDate from 'Utilities/Date/formatDate';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import UpdateChanges from './UpdateChanges';
|
||||
import styles from './Updates.css';
|
||||
|
||||
class Updates extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
currentVersion,
|
||||
isFetching,
|
||||
isPopulated,
|
||||
updatesError,
|
||||
generalSettingsError,
|
||||
items,
|
||||
isInstallingUpdate,
|
||||
updateMechanism,
|
||||
isDocker,
|
||||
updateMechanismMessage,
|
||||
shortDateFormat,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
onInstallLatestPress
|
||||
} = this.props;
|
||||
|
||||
const hasError = !!(updatesError || generalSettingsError);
|
||||
const hasUpdates = isPopulated && !hasError && items.length > 0;
|
||||
const noUpdates = isPopulated && !hasError && !items.length;
|
||||
const hasUpdateToInstall = hasUpdates && _.some(items, { installable: true, latest: true });
|
||||
const noUpdateToInstall = hasUpdates && !hasUpdateToInstall;
|
||||
|
||||
const externalUpdaterPrefix = 'Unable to update Readarr directly,';
|
||||
const externalUpdaterMessages = {
|
||||
external: 'Readarr is configured to use an external update mechanism',
|
||||
apt: 'use apt to install the update',
|
||||
docker: 'update the docker container to receive the update'
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContent title={translate('Updates')}>
|
||||
<PageContentBody>
|
||||
{
|
||||
!isPopulated && !hasError &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
noUpdates &&
|
||||
<Alert kind={kinds.INFO}>
|
||||
{translate('NoUpdatesAreAvailable')}
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
hasUpdateToInstall &&
|
||||
<div className={styles.messageContainer}>
|
||||
{
|
||||
(updateMechanism === 'builtIn' || updateMechanism === 'script') && !isDocker ?
|
||||
<SpinnerButton
|
||||
className={styles.updateAvailable}
|
||||
kind={kinds.PRIMARY}
|
||||
isSpinning={isInstallingUpdate}
|
||||
onPress={onInstallLatestPress}
|
||||
>
|
||||
Install Latest
|
||||
</SpinnerButton> :
|
||||
|
||||
<Fragment>
|
||||
<Icon
|
||||
name={icons.WARNING}
|
||||
kind={kinds.WARNING}
|
||||
size={30}
|
||||
/>
|
||||
|
||||
<div className={styles.message}>
|
||||
{externalUpdaterPrefix} <InlineMarkdown data={updateMechanismMessage || externalUpdaterMessages[updateMechanism] || externalUpdaterMessages.external} />
|
||||
</div>
|
||||
</Fragment>
|
||||
}
|
||||
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator
|
||||
className={styles.loading}
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
noUpdateToInstall &&
|
||||
<div className={styles.messageContainer}>
|
||||
<Icon
|
||||
className={styles.upToDateIcon}
|
||||
name={icons.CHECK_CIRCLE}
|
||||
size={30}
|
||||
/>
|
||||
|
||||
<div className={styles.message}>
|
||||
The latest version of Readarr is already installed
|
||||
</div>
|
||||
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator
|
||||
className={styles.loading}
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
hasUpdates &&
|
||||
<div>
|
||||
{
|
||||
items.map((update) => {
|
||||
const hasChanges = !!update.changes;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={update.version}
|
||||
className={styles.update}
|
||||
>
|
||||
<div className={styles.info}>
|
||||
<div className={styles.version}>{update.version}</div>
|
||||
<div className={styles.space}>—</div>
|
||||
<div
|
||||
className={styles.date}
|
||||
title={formatDateTime(update.releaseDate, longDateFormat, timeFormat)}
|
||||
>
|
||||
{formatDate(update.releaseDate, shortDateFormat)}
|
||||
</div>
|
||||
|
||||
{
|
||||
update.branch === 'master' ?
|
||||
null :
|
||||
<Label
|
||||
className={styles.label}
|
||||
>
|
||||
{update.branch}
|
||||
</Label>
|
||||
}
|
||||
|
||||
{
|
||||
update.version === currentVersion ?
|
||||
<Label
|
||||
className={styles.label}
|
||||
kind={kinds.SUCCESS}
|
||||
title={formatDateTime(update.installedOn, longDateFormat, timeFormat)}
|
||||
>
|
||||
Currently Installed
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
update.version !== currentVersion && update.installedOn ?
|
||||
<Label
|
||||
className={styles.label}
|
||||
kind={kinds.INVERSE}
|
||||
title={formatDateTime(update.installedOn, longDateFormat, timeFormat)}
|
||||
>
|
||||
Previously Installed
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
|
||||
{
|
||||
!hasChanges &&
|
||||
<div>
|
||||
{translate('MaintenanceRelease')}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
hasChanges &&
|
||||
<div className={styles.changes}>
|
||||
<UpdateChanges
|
||||
title={translate('New')}
|
||||
changes={update.changes.new}
|
||||
/>
|
||||
|
||||
<UpdateChanges
|
||||
title={translate('Fixed')}
|
||||
changes={update.changes.fixed}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
!!updatesError &&
|
||||
<div>
|
||||
Failed to fetch updates
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
!!generalSettingsError &&
|
||||
<div>
|
||||
Failed to update settings
|
||||
</div>
|
||||
}
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Updates.propTypes = {
|
||||
currentVersion: PropTypes.string.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
updatesError: PropTypes.object,
|
||||
generalSettingsError: PropTypes.object,
|
||||
items: PropTypes.array.isRequired,
|
||||
isInstallingUpdate: PropTypes.bool.isRequired,
|
||||
isDocker: PropTypes.bool.isRequired,
|
||||
updateMechanism: PropTypes.string,
|
||||
updateMechanismMessage: PropTypes.string,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
longDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
onInstallLatestPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default Updates;
|
||||
@@ -1,303 +0,0 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import Alert from 'Components/Alert';
|
||||
import Icon from 'Components/Icon';
|
||||
import Label from 'Components/Label';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { fetchGeneralSettings } from 'Store/Actions/settingsActions';
|
||||
import { fetchUpdates } from 'Store/Actions/systemActions';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import { UpdateMechanism } from 'typings/Settings/General';
|
||||
import formatDate from 'Utilities/Date/formatDate';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import UpdateChanges from './UpdateChanges';
|
||||
import styles from './Updates.css';
|
||||
|
||||
const VERSION_REGEX = /\d+\.\d+\.\d+\.\d+/i;
|
||||
|
||||
function createUpdatesSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.system.updates,
|
||||
(state: AppState) => state.settings.general,
|
||||
(updates, generalSettings) => {
|
||||
const { error: updatesError, items } = updates;
|
||||
|
||||
const isFetching = updates.isFetching || generalSettings.isFetching;
|
||||
const isPopulated = updates.isPopulated && generalSettings.isPopulated;
|
||||
|
||||
return {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
updatesError,
|
||||
generalSettingsError: generalSettings.error,
|
||||
items,
|
||||
updateMechanism: generalSettings.item.updateMechanism,
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function Updates() {
|
||||
const currentVersion = useSelector((state: AppState) => state.app.version);
|
||||
const { packageUpdateMechanismMessage } = useSelector(
|
||||
createSystemStatusSelector()
|
||||
);
|
||||
const { shortDateFormat, longDateFormat, timeFormat } = useSelector(
|
||||
createUISettingsSelector()
|
||||
);
|
||||
const isInstallingUpdate = useSelector(
|
||||
createCommandExecutingSelector(commandNames.APPLICATION_UPDATE)
|
||||
);
|
||||
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
updatesError,
|
||||
generalSettingsError,
|
||||
items,
|
||||
updateMechanism,
|
||||
} = useSelector(createUpdatesSelector());
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [isMajorUpdateModalOpen, setIsMajorUpdateModalOpen] = useState(false);
|
||||
const hasError = !!(updatesError || generalSettingsError);
|
||||
const hasUpdates = isPopulated && !hasError && items.length > 0;
|
||||
const noUpdates = isPopulated && !hasError && !items.length;
|
||||
|
||||
const externalUpdaterPrefix = translate('UpdateAppDirectlyLoadError');
|
||||
const externalUpdaterMessages: Partial<Record<UpdateMechanism, string>> = {
|
||||
external: translate('ExternalUpdater'),
|
||||
apt: translate('AptUpdater'),
|
||||
docker: translate('DockerUpdater'),
|
||||
};
|
||||
|
||||
const { isMajorUpdate, hasUpdateToInstall } = useMemo(() => {
|
||||
const majorVersion = parseInt(
|
||||
currentVersion.match(VERSION_REGEX)?.[0] ?? '0'
|
||||
);
|
||||
|
||||
const latestVersion = items[0]?.version;
|
||||
const latestMajorVersion = parseInt(
|
||||
latestVersion?.match(VERSION_REGEX)?.[0] ?? '0'
|
||||
);
|
||||
|
||||
return {
|
||||
isMajorUpdate: latestMajorVersion > majorVersion,
|
||||
hasUpdateToInstall: items.some(
|
||||
(update) => update.installable && update.latest
|
||||
),
|
||||
};
|
||||
}, [currentVersion, items]);
|
||||
|
||||
const noUpdateToInstall = hasUpdates && !hasUpdateToInstall;
|
||||
|
||||
const handleInstallLatestPress = useCallback(() => {
|
||||
if (isMajorUpdate) {
|
||||
setIsMajorUpdateModalOpen(true);
|
||||
} else {
|
||||
dispatch(executeCommand({ name: commandNames.APPLICATION_UPDATE }));
|
||||
}
|
||||
}, [isMajorUpdate, setIsMajorUpdateModalOpen, dispatch]);
|
||||
|
||||
const handleInstallLatestMajorVersionPress = useCallback(() => {
|
||||
setIsMajorUpdateModalOpen(false);
|
||||
|
||||
dispatch(
|
||||
executeCommand({
|
||||
name: commandNames.APPLICATION_UPDATE,
|
||||
installMajorUpdate: true,
|
||||
})
|
||||
);
|
||||
}, [setIsMajorUpdateModalOpen, dispatch]);
|
||||
|
||||
const handleCancelMajorVersionPress = useCallback(() => {
|
||||
setIsMajorUpdateModalOpen(false);
|
||||
}, [setIsMajorUpdateModalOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchUpdates());
|
||||
dispatch(fetchGeneralSettings());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<PageContent title={translate('Updates')}>
|
||||
<PageContentBody>
|
||||
{isPopulated || hasError ? null : <LoadingIndicator />}
|
||||
|
||||
{noUpdates ? (
|
||||
<Alert kind={kinds.INFO}>{translate('NoUpdatesAreAvailable')}</Alert>
|
||||
) : null}
|
||||
|
||||
{hasUpdateToInstall ? (
|
||||
<div className={styles.messageContainer}>
|
||||
{updateMechanism === 'builtIn' || updateMechanism === 'script' ? (
|
||||
<SpinnerButton
|
||||
kind={kinds.PRIMARY}
|
||||
isSpinning={isInstallingUpdate}
|
||||
onPress={handleInstallLatestPress}
|
||||
>
|
||||
{translate('InstallLatest')}
|
||||
</SpinnerButton>
|
||||
) : (
|
||||
<>
|
||||
<Icon name={icons.WARNING} kind={kinds.WARNING} size={30} />
|
||||
|
||||
<div className={styles.message}>
|
||||
{externalUpdaterPrefix}{' '}
|
||||
<InlineMarkdown
|
||||
data={
|
||||
packageUpdateMechanismMessage ||
|
||||
externalUpdaterMessages[updateMechanism] ||
|
||||
externalUpdaterMessages.external
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{isFetching ? (
|
||||
<LoadingIndicator className={styles.loading} size={20} />
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{noUpdateToInstall && (
|
||||
<div className={styles.messageContainer}>
|
||||
<Icon
|
||||
className={styles.upToDateIcon}
|
||||
name={icons.CHECK_CIRCLE}
|
||||
size={30}
|
||||
/>
|
||||
<div className={styles.message}>{translate('OnLatestVersion')}</div>
|
||||
|
||||
{isFetching && (
|
||||
<LoadingIndicator className={styles.loading} size={20} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasUpdates && (
|
||||
<div>
|
||||
{items.map((update) => {
|
||||
return (
|
||||
<div key={update.version} className={styles.update}>
|
||||
<div className={styles.info}>
|
||||
<div className={styles.version}>{update.version}</div>
|
||||
<div className={styles.space}>—</div>
|
||||
<div
|
||||
className={styles.date}
|
||||
title={formatDateTime(
|
||||
update.releaseDate,
|
||||
longDateFormat,
|
||||
timeFormat
|
||||
)}
|
||||
>
|
||||
{formatDate(update.releaseDate, shortDateFormat)}
|
||||
</div>
|
||||
|
||||
{update.branch === 'master' ? null : (
|
||||
<Label className={styles.label}>{update.branch}</Label>
|
||||
)}
|
||||
|
||||
{update.version === currentVersion ? (
|
||||
<Label
|
||||
className={styles.label}
|
||||
kind={kinds.SUCCESS}
|
||||
title={formatDateTime(
|
||||
update.installedOn,
|
||||
longDateFormat,
|
||||
timeFormat
|
||||
)}
|
||||
>
|
||||
{translate('CurrentlyInstalled')}
|
||||
</Label>
|
||||
) : null}
|
||||
|
||||
{update.version !== currentVersion && update.installedOn ? (
|
||||
<Label
|
||||
className={styles.label}
|
||||
kind={kinds.INVERSE}
|
||||
title={formatDateTime(
|
||||
update.installedOn,
|
||||
longDateFormat,
|
||||
timeFormat
|
||||
)}
|
||||
>
|
||||
{translate('PreviouslyInstalled')}
|
||||
</Label>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{update.changes ? (
|
||||
<div>
|
||||
<UpdateChanges
|
||||
title={translate('New')}
|
||||
changes={update.changes.new}
|
||||
/>
|
||||
|
||||
<UpdateChanges
|
||||
title={translate('Fixed')}
|
||||
changes={update.changes.fixed}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div>{translate('MaintenanceRelease')}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{updatesError ? (
|
||||
<Alert kind={kinds.WARNING}>
|
||||
{translate('FailedToFetchUpdates')}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{generalSettingsError ? (
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('FailedToFetchSettings')}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isMajorUpdateModalOpen}
|
||||
kind={kinds.WARNING}
|
||||
title={translate('InstallMajorVersionUpdate')}
|
||||
message={
|
||||
<div>
|
||||
<div>{translate('InstallMajorVersionUpdateMessage')}</div>
|
||||
<div>
|
||||
<InlineMarkdown
|
||||
data={translate('InstallMajorVersionUpdateMessageLink', {
|
||||
domain: 'readarr.com',
|
||||
url: 'https://readarr.com/#downloads',
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
confirmLabel={translate('Install')}
|
||||
onConfirm={handleInstallLatestMajorVersionPress}
|
||||
onCancel={handleCancelMajorVersionPress}
|
||||
/>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default Updates;
|
||||
@@ -131,15 +131,13 @@ class CutoffUnmetConnector extends Component {
|
||||
onSearchSelectedPress = (selected) => {
|
||||
this.props.executeCommand({
|
||||
name: commandNames.BOOK_SEARCH,
|
||||
bookIds: selected,
|
||||
commandFinished: this.repopulate
|
||||
bookIds: selected
|
||||
});
|
||||
};
|
||||
|
||||
onSearchAllCutoffUnmetPress = () => {
|
||||
this.props.executeCommand({
|
||||
name: commandNames.CUTOFF_UNMET_BOOK_SEARCH,
|
||||
commandFinished: this.repopulate
|
||||
name: commandNames.CUTOFF_UNMET_BOOK_SEARCH
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ function CutoffUnmetRow(props) {
|
||||
releaseDate,
|
||||
titleSlug,
|
||||
title,
|
||||
lastSearchTime,
|
||||
disambiguation,
|
||||
isSelected,
|
||||
columns,
|
||||
@@ -69,15 +68,6 @@ function CutoffUnmetRow(props) {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'books.lastSearchTime') {
|
||||
return (
|
||||
<RelativeDateCellConnector
|
||||
key={name}
|
||||
date={lastSearchTime}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'releaseDate') {
|
||||
return (
|
||||
<RelativeDateCellConnector
|
||||
@@ -115,7 +105,6 @@ CutoffUnmetRow.propTypes = {
|
||||
releaseDate: PropTypes.string.isRequired,
|
||||
titleSlug: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
lastSearchTime: PropTypes.string,
|
||||
disambiguation: PropTypes.string,
|
||||
isSelected: PropTypes.bool,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
|
||||
@@ -121,15 +121,13 @@ class MissingConnector extends Component {
|
||||
onSearchSelectedPress = (selected) => {
|
||||
this.props.executeCommand({
|
||||
name: commandNames.BOOK_SEARCH,
|
||||
bookIds: selected,
|
||||
commandFinished: this.repopulate
|
||||
bookIds: selected
|
||||
});
|
||||
};
|
||||
|
||||
onSearchAllMissingPress = () => {
|
||||
this.props.executeCommand({
|
||||
name: commandNames.MISSING_BOOK_SEARCH,
|
||||
commandFinished: this.repopulate
|
||||
name: commandNames.MISSING_BOOK_SEARCH
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ function MissingRow(props) {
|
||||
releaseDate,
|
||||
titleSlug,
|
||||
title,
|
||||
lastSearchTime,
|
||||
disambiguation,
|
||||
isSelected,
|
||||
columns,
|
||||
@@ -78,15 +77,6 @@ function MissingRow(props) {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'books.lastSearchTime') {
|
||||
return (
|
||||
<RelativeDateCellConnector
|
||||
key={name}
|
||||
date={lastSearchTime}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'actions') {
|
||||
return (
|
||||
<BookSearchCellConnector
|
||||
@@ -114,7 +104,6 @@ MissingRow.propTypes = {
|
||||
releaseDate: PropTypes.string.isRequired,
|
||||
titleSlug: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
lastSearchTime: PropTypes.string,
|
||||
disambiguation: PropTypes.string,
|
||||
isSelected: PropTypes.bool,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
export type UpdateMechanism =
|
||||
| 'builtIn'
|
||||
| 'script'
|
||||
| 'external'
|
||||
| 'apt'
|
||||
| 'docker';
|
||||
|
||||
export default interface General {
|
||||
bindAddress: string;
|
||||
port: number;
|
||||
sslPort: number;
|
||||
enableSsl: boolean;
|
||||
launchBrowser: boolean;
|
||||
authenticationMethod: string;
|
||||
authenticationRequired: string;
|
||||
analyticsEnabled: boolean;
|
||||
username: string;
|
||||
password: string;
|
||||
passwordConfirmation: string;
|
||||
logLevel: string;
|
||||
consoleLogLevel: string;
|
||||
branch: string;
|
||||
apiKey: string;
|
||||
sslCertPath: string;
|
||||
sslCertPassword: string;
|
||||
urlBase: string;
|
||||
instanceName: string;
|
||||
applicationUrl: string;
|
||||
updateAutomatically: boolean;
|
||||
updateMechanism: UpdateMechanism;
|
||||
updateScriptPath: string;
|
||||
proxyEnabled: boolean;
|
||||
proxyType: string;
|
||||
proxyHostname: string;
|
||||
proxyPort: number;
|
||||
proxyUsername: string;
|
||||
proxyPassword: string;
|
||||
proxyBypassFilter: string;
|
||||
proxyBypassLocalAddresses: boolean;
|
||||
certificateValidation: string;
|
||||
backupFolder: string;
|
||||
backupInterval: number;
|
||||
backupRetention: number;
|
||||
id: number;
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
interface SystemStatus {
|
||||
appData: string;
|
||||
appName: string;
|
||||
authentication: string;
|
||||
branch: string;
|
||||
buildTime: string;
|
||||
instanceName: string;
|
||||
isAdmin: boolean;
|
||||
isDebug: boolean;
|
||||
isDocker: boolean;
|
||||
isLinux: boolean;
|
||||
isNetCore: boolean;
|
||||
isOsx: boolean;
|
||||
isProduction: boolean;
|
||||
isUserInteractive: boolean;
|
||||
isWindows: boolean;
|
||||
migrationVersion: number;
|
||||
mode: string;
|
||||
osName: string;
|
||||
osVersion: string;
|
||||
packageUpdateMechanism: string;
|
||||
packageUpdateMechanismMessage: string;
|
||||
runtimeName: string;
|
||||
runtimeVersion: string;
|
||||
sqliteVersion: string;
|
||||
startTime: string;
|
||||
startupPath: string;
|
||||
urlBase: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export default SystemStatus;
|
||||
@@ -1,5 +1,4 @@
|
||||
export default interface UiSettings {
|
||||
theme: 'auto' | 'dark' | 'light';
|
||||
export interface UiSettings {
|
||||
showRelativeDates: boolean;
|
||||
shortDateFormat: string;
|
||||
longDateFormat: string;
|
||||
@@ -1,20 +0,0 @@
|
||||
export interface Changes {
|
||||
new: string[];
|
||||
fixed: string[];
|
||||
}
|
||||
|
||||
interface Update {
|
||||
version: string;
|
||||
branch: string;
|
||||
releaseDate: string;
|
||||
fileName: string;
|
||||
url: string;
|
||||
installed: boolean;
|
||||
installedOn: string;
|
||||
installable: boolean;
|
||||
latest: boolean;
|
||||
changes: Changes | null;
|
||||
hash: string;
|
||||
}
|
||||
|
||||
export default Update;
|
||||
96
package.json
96
package.json
@@ -25,33 +25,34 @@
|
||||
"defaults"
|
||||
],
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "6.7.1",
|
||||
"@fortawesome/fontawesome-svg-core": "6.7.1",
|
||||
"@fortawesome/free-regular-svg-icons": "6.7.1",
|
||||
"@fortawesome/free-solid-svg-icons": "6.7.1",
|
||||
"@fortawesome/react-fontawesome": "0.2.2",
|
||||
"@fortawesome/fontawesome-free": "6.4.0",
|
||||
"@fortawesome/fontawesome-svg-core": "6.4.0",
|
||||
"@fortawesome/free-regular-svg-icons": "6.4.0",
|
||||
"@fortawesome/free-solid-svg-icons": "6.4.0",
|
||||
"@fortawesome/react-fontawesome": "0.2.0",
|
||||
"@microsoft/signalr": "6.0.25",
|
||||
"@sentry/browser": "7.119.1",
|
||||
"@sentry/integrations": "7.119.1",
|
||||
"@types/node": "20.16.11",
|
||||
"@sentry/browser": "7.51.2",
|
||||
"@sentry/integrations": "7.51.2",
|
||||
"@types/node": "18.19.31",
|
||||
"@types/react": "18.2.79",
|
||||
"@types/react-dom": "18.2.25",
|
||||
"classnames": "2.5.1",
|
||||
"ansi-colors": "4.1.3",
|
||||
"classnames": "2.3.2",
|
||||
"clipboard": "2.0.11",
|
||||
"connected-react-router": "6.9.3",
|
||||
"element-class": "0.2.2",
|
||||
"filesize": "10.1.6",
|
||||
"filesize": "10.0.7",
|
||||
"fuse.js": "6.6.2",
|
||||
"history": "4.10.1",
|
||||
"jdu": "1.0.0",
|
||||
"jquery": "3.7.1",
|
||||
"jquery": "3.7.0",
|
||||
"lodash": "4.17.21",
|
||||
"mobile-detect": "1.4.5",
|
||||
"moment": "2.30.1",
|
||||
"moment": "2.29.4",
|
||||
"mousetrap": "1.6.5",
|
||||
"normalize.css": "8.0.1",
|
||||
"prop-types": "15.8.1",
|
||||
"qs": "6.13.0",
|
||||
"qs": "6.11.1",
|
||||
"react": "17.0.2",
|
||||
"react-addons-shallow-compare": "15.6.3",
|
||||
"react-async-script": "1.2.0",
|
||||
@@ -63,7 +64,7 @@
|
||||
"react-dnd-touch-backend": "14.1.1",
|
||||
"react-document-title": "2.0.3",
|
||||
"react-dom": "17.0.2",
|
||||
"react-focus-lock": "2.9.4",
|
||||
"react-focus-lock": "2.5.2",
|
||||
"react-google-recaptcha": "2.1.0",
|
||||
"react-lazyload": "3.2.0",
|
||||
"react-measure": "2.5.2",
|
||||
@@ -72,77 +73,80 @@
|
||||
"react-redux": "7.2.4",
|
||||
"react-router": "5.2.0",
|
||||
"react-router-dom": "5.2.0",
|
||||
"react-slider": "1.3.3",
|
||||
"react-tabs": "4.3.0",
|
||||
"react-text-truncate": "0.19.0",
|
||||
"react-slider": "1.3.1",
|
||||
"react-tabs": "3.2.2",
|
||||
"react-text-truncate": "0.18.0",
|
||||
"react-virtualized": "9.21.1",
|
||||
"redux": "4.2.1",
|
||||
"redux": "4.1.0",
|
||||
"redux-actions": "2.6.5",
|
||||
"redux-batched-actions": "0.5.0",
|
||||
"redux-localstorage": "0.4.1",
|
||||
"redux-thunk": "2.4.2",
|
||||
"redux-thunk": "2.3.0",
|
||||
"reselect": "4.1.8",
|
||||
"stacktrace-js": "2.0.2",
|
||||
"typescript": "5.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.26.0",
|
||||
"@babel/eslint-parser": "7.25.9",
|
||||
"@babel/plugin-proposal-export-default-from": "7.25.9",
|
||||
"@babel/core": "7.24.4",
|
||||
"@babel/eslint-parser": "7.24.1",
|
||||
"@babel/plugin-proposal-export-default-from": "7.24.1",
|
||||
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
||||
"@babel/preset-env": "7.26.0",
|
||||
"@babel/preset-react": "7.26.3",
|
||||
"@babel/preset-typescript": "7.26.0",
|
||||
"@babel/preset-env": "7.24.4",
|
||||
"@babel/preset-react": "7.24.1",
|
||||
"@babel/preset-typescript": "7.24.1",
|
||||
"@types/lodash": "4.14.195",
|
||||
"@types/react-lazyload": "3.2.3",
|
||||
"@types/redux-actions": "2.6.5",
|
||||
"@types/react-lazyload": "3.2.0",
|
||||
"@types/redux-actions": "2.6.2",
|
||||
"@typescript-eslint/eslint-plugin": "6.21.0",
|
||||
"@typescript-eslint/parser": "6.21.0",
|
||||
"autoprefixer": "10.4.20",
|
||||
"babel-loader": "9.2.1",
|
||||
"autoprefixer": "10.4.14",
|
||||
"babel-loader": "9.1.3",
|
||||
"babel-plugin-inline-classnames": "2.0.1",
|
||||
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
|
||||
"core-js": "3.39.0",
|
||||
"core-js": "3.37.0",
|
||||
"css-loader": "6.8.1",
|
||||
"css-modules-typescript-loader": "4.0.1",
|
||||
"eslint": "8.57.1",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-prettier": "8.10.0",
|
||||
"eslint-plugin-filenames": "1.3.2",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"eslint-plugin-json": "3.1.0",
|
||||
"eslint-plugin-prettier": "4.2.1",
|
||||
"eslint-plugin-react": "7.37.1",
|
||||
"eslint-plugin-react-hooks": "4.6.2",
|
||||
"eslint-plugin-simple-import-sort": "12.1.1",
|
||||
"eslint-plugin-react": "7.34.1",
|
||||
"eslint-plugin-react-hooks": "4.6.0",
|
||||
"eslint-plugin-simple-import-sort": "12.1.0",
|
||||
"file-loader": "6.2.0",
|
||||
"filemanager-webpack-plugin": "8.0.0",
|
||||
"fork-ts-checker-webpack-plugin": "8.0.0",
|
||||
"html-webpack-plugin": "5.6.0",
|
||||
"html-webpack-plugin": "5.5.3",
|
||||
"loader-utils": "^3.2.1",
|
||||
"mini-css-extract-plugin": "2.9.1",
|
||||
"postcss": "8.4.47",
|
||||
"mini-css-extract-plugin": "2.7.6",
|
||||
"postcss": "8.4.38",
|
||||
"postcss-color-function": "4.1.0",
|
||||
"postcss-loader": "7.3.0",
|
||||
"postcss-mixins": "9.0.4",
|
||||
"postcss-nested": "6.2.0",
|
||||
"postcss-nested": "6.0.1",
|
||||
"postcss-simple-vars": "7.0.1",
|
||||
"postcss-url": "10.1.3",
|
||||
"prettier": "2.8.8",
|
||||
"require-nocache": "1.0.0",
|
||||
"rimraf": "6.0.1",
|
||||
"style-loader": "3.3.4",
|
||||
"rimraf": "4.4.1",
|
||||
"run-sequence": "2.2.1",
|
||||
"streamqueue": "1.1.2",
|
||||
"style-loader": "3.3.3",
|
||||
"stylelint": "15.10.3",
|
||||
"stylelint-order": "6.0.4",
|
||||
"terser-webpack-plugin": "5.3.10",
|
||||
"ts-loader": "9.5.1",
|
||||
"stylelint-order": "6.0.3",
|
||||
"terser-webpack-plugin": "5.3.9",
|
||||
"ts-loader": "9.4.4",
|
||||
"typescript-plugin-css-modules": "5.0.1",
|
||||
"url-loader": "4.1.1",
|
||||
"webpack": "5.95.0",
|
||||
"webpack": "5.88.2",
|
||||
"webpack-cli": "5.1.4",
|
||||
"webpack-livereload-plugin": "3.0.2",
|
||||
"worker-loader": "3.0.8"
|
||||
},
|
||||
"volta": {
|
||||
"node": "20.11.1",
|
||||
"node": "16.17.0",
|
||||
"yarn": "1.22.19"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,35 +99,6 @@
|
||||
<RootNamespace Condition="'$(ReadarrProject)'=='true'">$(MSBuildProjectName.Replace('Readarr','NzbDrone'))</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TestProject)'!='true'">
|
||||
<!-- Annotates .NET assemblies with repository information including SHA -->
|
||||
<!-- Sentry uses this to link directly to GitHub at the exact version/file/line -->
|
||||
<!-- This is built-in on .NET 8 and can be removed once the project is updated -->
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Sentry specific configuration: Only in Release mode -->
|
||||
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
|
||||
<!-- https://docs.sentry.io/platforms/dotnet/configuration/msbuild/ -->
|
||||
<!-- OrgSlug, ProjectSlug and AuthToken are required.
|
||||
They can be set below, via argument to 'msbuild -p:' or environment variable -->
|
||||
<SentryOrg></SentryOrg>
|
||||
<SentryProject></SentryProject>
|
||||
<SentryUrl></SentryUrl> <!-- If empty, assumed to be sentry.io -->
|
||||
<SentryAuthToken></SentryAuthToken> <!-- Use env var instead: SENTRY_AUTH_TOKEN -->
|
||||
|
||||
<!-- Upload PDBs to Sentry, enabling stack traces with line numbers and file paths
|
||||
without the need to deploy the application with PDBs -->
|
||||
<SentryUploadSymbols>true</SentryUploadSymbols>
|
||||
|
||||
<!-- Source Link settings -->
|
||||
<!-- https://github.com/dotnet/sourcelink/blob/main/docs/README.md#publishrepositoryurl -->
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<!-- Embeds all source code in the respective PDB. This can make it a bit bigger but since it'll be uploaded
|
||||
to Sentry and not distributed to run on the server, it helps debug crashes while making releases smaller -->
|
||||
<EmbedAllSources>true</EmbedAllSources>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Standard testing packages -->
|
||||
<ItemGroup Condition="'$(TestProject)'=='true'">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
|
||||
@@ -4,30 +4,26 @@
|
||||
<PackageVersion Include="AutoFixture" Version="4.17.0" />
|
||||
<PackageVersion Include="coverlet.collector" Version="3.0.4-preview.27.ge7cb7c3b40" PrivateAssets="all" />
|
||||
<PackageVersion Include="Dapper" Version="2.0.151" />
|
||||
<PackageVersion Include="Diacritical.Net" Version="1.0.4" />
|
||||
<PackageVersion Include="DryIoc.dll" Version="5.4.3" />
|
||||
<PackageVersion Include="DryIoc.Microsoft.DependencyInjection" Version="6.2.0" />
|
||||
<PackageVersion Include="Equ" Version="2.3.0" />
|
||||
<PackageVersion Include="FluentAssertions" Version="5.10.3" />
|
||||
<PackageVersion Include="IPAddressRange" Version="6.1.0" />
|
||||
<PackageVersion Include="Polly" Version="8.5.2" />
|
||||
<PackageVersion Include="Polly" Version="8.4.1" />
|
||||
<PackageVersion Include="Servarr.FluentMigrator.Runner" Version="3.3.2.9" />
|
||||
<PackageVersion Include="Servarr.FluentMigrator.Runner.SQLite" Version="3.3.2.9" />
|
||||
<PackageVersion Include="Servarr.FluentMigrator.Runner.Postgres" Version="3.3.2.9" />
|
||||
<PackageVersion Include="FluentValidation" Version="9.5.4" />
|
||||
<PackageVersion Include="Ical.Net" Version="4.3.1" />
|
||||
<PackageVersion Include="Ical.Net" Version="4.2.0" />
|
||||
<PackageVersion Include="ImpromptuInterface" Version="7.0.1" />
|
||||
<PackageVersion Include="LazyCache" Version="2.4.0" />
|
||||
<PackageVersion Include="Mailkit" Version="4.8.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.35" />
|
||||
<PackageVersion Include="Microsoft.Data.SqlClient" Version="2.1.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="6.0.2" />
|
||||
<PackageVersion Include="Mailkit" Version="3.6.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.32" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" 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.Hosting.WindowsServices" Version="6.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="6.0.0" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
|
||||
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
|
||||
<PackageVersion Include="Microsoft.Win32.Registry" Version="5.0.0" />
|
||||
<PackageVersion Include="Mono.Posix.NETStandard" Version="5.20.1.34-servarr22" />
|
||||
<PackageVersion Include="Moq" Version="4.17.2" />
|
||||
@@ -37,36 +33,36 @@
|
||||
<PackageVersion Include="NLog.Extensions.Logging" Version="5.2.3" />
|
||||
<PackageVersion Include="NLog" Version="5.1.4" />
|
||||
<PackageVersion Include="NLog.Targets.Syslog" Version="7.0.0" />
|
||||
<PackageVersion Include="Npgsql" Version="7.0.10" />
|
||||
<PackageVersion Include="Npgsql" Version="7.0.7" />
|
||||
<PackageVersion Include="NUnit3TestAdapter" Version="4.2.1" />
|
||||
<PackageVersion Include="NUnit" Version="3.14.0" />
|
||||
<PackageVersion Include="NunitXml.TestLogger" Version="3.0.117" />
|
||||
<PackageVersion Include="PdfSharpCore" Version="1.3.65" />
|
||||
<PackageVersion Include="PdfSharpCore" Version="1.3.32" />
|
||||
<PackageVersion Include="RestSharp.Serializers.SystemTextJson" Version="106.15.0" />
|
||||
<PackageVersion Include="RestSharp" Version="106.15.0" />
|
||||
<PackageVersion Include="Selenium.Support" Version="3.141.0" />
|
||||
<PackageVersion Include="Selenium.WebDriver.ChromeDriver" Version="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="SixLabors.ImageSharp" Version="3.1.7" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.5" />
|
||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.1.118" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore.Annotations" Version="6.6.2" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.6.2" />
|
||||
<PackageVersion Include="System.Buffers" Version="4.6.0" />
|
||||
<PackageVersion Include="System.Buffers" Version="4.5.1" />
|
||||
<PackageVersion Include="System.Configuration.ConfigurationManager" Version="6.0.1" />
|
||||
<PackageVersion Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
||||
<PackageVersion Include="System.IO.Abstractions.TestingHelpers" Version="17.0.24" />
|
||||
<PackageVersion Include="System.IO.Abstractions" Version="17.0.24" />
|
||||
<PackageVersion Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
|
||||
<PackageVersion Include="System.Memory" Version="4.6.2" />
|
||||
<PackageVersion Include="System.Memory" Version="4.5.5" />
|
||||
<PackageVersion Include="System.Reflection.TypeExtensions" Version="4.7.0" />
|
||||
<PackageVersion Include="System.Resources.Extensions" Version="6.0.0" />
|
||||
<PackageVersion Include="System.Runtime.Loader" Version="4.3.0" />
|
||||
<PackageVersion Include="System.Security.Principal.Windows" Version="5.0.0" />
|
||||
<PackageVersion Include="System.ServiceProcess.ServiceController" Version="6.0.1" />
|
||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="6.0.0" />
|
||||
<PackageVersion Include="System.Text.Json" Version="6.0.10" />
|
||||
<PackageVersion Include="System.ValueTuple" Version="4.6.1" />
|
||||
<PackageVersion Include="System.Text.Json" Version="6.0.9" />
|
||||
<PackageVersion Include="System.ValueTuple" Version="4.5.0" />
|
||||
<PackageVersion Include="TagLibSharp-Lidarr" Version="2.2.0.19" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -21,28 +21,9 @@ namespace NzbDrone.Common.Test.ExtensionTests
|
||||
[TestCase("1.2.3.4")]
|
||||
[TestCase("172.55.0.1")]
|
||||
[TestCase("192.55.0.1")]
|
||||
[TestCase("100.64.0.1")]
|
||||
[TestCase("100.127.255.254")]
|
||||
public void should_return_false_for_public_ip_address(string ipAddress)
|
||||
{
|
||||
IPAddress.Parse(ipAddress).IsLocalAddress().Should().BeFalse();
|
||||
}
|
||||
|
||||
[TestCase("100.64.0.1")]
|
||||
[TestCase("100.127.255.254")]
|
||||
[TestCase("100.100.100.100")]
|
||||
public void should_return_true_for_cgnat_ip_address(string ipAddress)
|
||||
{
|
||||
IPAddress.Parse(ipAddress).IsCgnatIpAddress().Should().BeTrue();
|
||||
}
|
||||
|
||||
[TestCase("1.2.3.4")]
|
||||
[TestCase("192.168.5.1")]
|
||||
[TestCase("100.63.255.255")]
|
||||
[TestCase("100.128.0.0")]
|
||||
public void should_return_false_for_non_cgnat_ip_address(string ipAddress)
|
||||
{
|
||||
IPAddress.Parse(ipAddress).IsCgnatIpAddress().Should().BeFalse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,10 +89,6 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
||||
[TestCase(@"https://discord.com/api/webhooks/mySecret")]
|
||||
[TestCase(@"https://discord.com/api/webhooks/mySecret/01233210")]
|
||||
|
||||
// Telegram
|
||||
[TestCase(@"https://api.telegram.org/bot1234567890:mySecret/sendmessage: chat_id=123456&parse_mode=HTML&text=<text>")]
|
||||
[TestCase(@"https://api.telegram.org/bot1234567890:mySecret/")]
|
||||
|
||||
public void should_clean_message(string message)
|
||||
{
|
||||
var cleansedMessage = CleanseLogMessage.Cleanse(message);
|
||||
|
||||
@@ -4,7 +4,6 @@ using System.Linq;
|
||||
using FluentAssertions;
|
||||
using NLog;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Instrumentation.Sentry;
|
||||
using NzbDrone.Test.Common;
|
||||
|
||||
@@ -28,7 +27,7 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_subject = new SentryTarget("https://aaaaaaaaaaaaaaaaaaaaaaaaaa@sentry.io/111111", Mocker.GetMock<IAppFolderInfo>().Object);
|
||||
_subject = new SentryTarget("https://aaaaaaaaaaaaaaaaaaaaaaaaaa@sentry.io/111111");
|
||||
}
|
||||
|
||||
private LogEventInfo GivenLogEvent(LogLevel level, Exception ex, string message)
|
||||
|
||||
@@ -42,18 +42,17 @@ namespace NzbDrone.Common
|
||||
|
||||
public void CreateZip(string path, IEnumerable<string> files)
|
||||
{
|
||||
_logger.Debug("Creating archive {0}", path);
|
||||
|
||||
using var zipFile = ZipFile.Create(path);
|
||||
|
||||
zipFile.BeginUpdate();
|
||||
|
||||
foreach (var file in files)
|
||||
using (var zipFile = ZipFile.Create(path))
|
||||
{
|
||||
zipFile.Add(file, Path.GetFileName(file));
|
||||
}
|
||||
zipFile.BeginUpdate();
|
||||
|
||||
zipFile.CommitUpdate();
|
||||
foreach (var file in files)
|
||||
{
|
||||
zipFile.Add(file, Path.GetFileName(file));
|
||||
}
|
||||
|
||||
zipFile.CommitUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
private void ExtractZip(string compressedFile, string destination)
|
||||
|
||||
@@ -3,7 +3,6 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Abstractions;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using NLog;
|
||||
using NzbDrone.Common.EnsureThat;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
@@ -307,26 +306,9 @@ namespace NzbDrone.Common.Disk
|
||||
{
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
var files = GetFiles(path, recursive).ToList();
|
||||
var files = GetFiles(path, recursive);
|
||||
|
||||
files.ForEach(RemoveReadOnly);
|
||||
|
||||
var attempts = 0;
|
||||
|
||||
while (attempts < 3 && files.Any())
|
||||
{
|
||||
EmptyFolder(path);
|
||||
|
||||
if (GetFiles(path, recursive).Any())
|
||||
{
|
||||
// Wait for IO operations to complete after emptying the folder since they aren't always
|
||||
// instantly removed and it can lead to false positives that files are still present.
|
||||
Thread.Sleep(3000);
|
||||
}
|
||||
|
||||
attempts++;
|
||||
files = GetFiles(path, recursive).ToList();
|
||||
}
|
||||
files.ToList().ForEach(RemoveReadOnly);
|
||||
|
||||
_fileSystem.Directory.Delete(path, recursive);
|
||||
}
|
||||
|
||||
@@ -342,11 +342,10 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
var isCifs = targetDriveFormat == "cifs";
|
||||
var isBtrfs = sourceDriveFormat == "btrfs" && targetDriveFormat == "btrfs";
|
||||
var isZfs = sourceDriveFormat == "zfs" && targetDriveFormat == "zfs";
|
||||
|
||||
if (mode.HasFlag(TransferMode.Copy))
|
||||
{
|
||||
if (isBtrfs || isZfs)
|
||||
if (isBtrfs)
|
||||
{
|
||||
if (_diskProvider.TryCreateRefLink(sourcePath, targetPath))
|
||||
{
|
||||
@@ -360,7 +359,7 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
if (mode.HasFlag(TransferMode.Move))
|
||||
{
|
||||
if (isBtrfs || isZfs)
|
||||
if (isBtrfs)
|
||||
{
|
||||
if (isSameMount && _diskProvider.TryRenameFile(sourcePath, targetPath))
|
||||
{
|
||||
|
||||
@@ -39,24 +39,18 @@ namespace NzbDrone.Common.Extensions
|
||||
private static bool IsLocalIPv4(byte[] ipv4Bytes)
|
||||
{
|
||||
// Link local (no IP assigned by DHCP): 169.254.0.0 to 169.254.255.255 (169.254.0.0/16)
|
||||
var isLinkLocal = ipv4Bytes[0] == 169 && ipv4Bytes[1] == 254;
|
||||
bool IsLinkLocal() => ipv4Bytes[0] == 169 && ipv4Bytes[1] == 254;
|
||||
|
||||
// Class A private range: 10.0.0.0 – 10.255.255.255 (10.0.0.0/8)
|
||||
var isClassA = ipv4Bytes[0] == 10;
|
||||
bool IsClassA() => ipv4Bytes[0] == 10;
|
||||
|
||||
// Class B private range: 172.16.0.0 – 172.31.255.255 (172.16.0.0/12)
|
||||
var isClassB = ipv4Bytes[0] == 172 && ipv4Bytes[1] >= 16 && ipv4Bytes[1] <= 31;
|
||||
bool IsClassB() => ipv4Bytes[0] == 172 && ipv4Bytes[1] >= 16 && ipv4Bytes[1] <= 31;
|
||||
|
||||
// Class C private range: 192.168.0.0 – 192.168.255.255 (192.168.0.0/16)
|
||||
var isClassC = ipv4Bytes[0] == 192 && ipv4Bytes[1] == 168;
|
||||
bool IsClassC() => ipv4Bytes[0] == 192 && ipv4Bytes[1] == 168;
|
||||
|
||||
return isLinkLocal || isClassA || isClassC || isClassB;
|
||||
}
|
||||
|
||||
public static bool IsCgnatIpAddress(this IPAddress ipAddress)
|
||||
{
|
||||
var bytes = ipAddress.GetAddressBytes();
|
||||
return bytes.Length == 4 && bytes[0] == 100 && bytes[1] >= 64 && bytes[1] <= 127;
|
||||
return IsLinkLocal() || IsClassA() || IsClassC() || IsClassB();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System;
|
||||
using NzbDrone.Common.Extensions;
|
||||
|
||||
namespace NzbDrone.Common.Http.Proxy
|
||||
@@ -30,8 +29,7 @@ namespace NzbDrone.Common.Http.Proxy
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(BypassFilter))
|
||||
{
|
||||
var hostlist = BypassFilter.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
|
||||
var hostlist = BypassFilter.Split(',');
|
||||
for (var i = 0; i < hostlist.Length; i++)
|
||||
{
|
||||
if (hostlist[i].StartsWith("*"))
|
||||
@@ -43,7 +41,7 @@ namespace NzbDrone.Common.Http.Proxy
|
||||
return hostlist;
|
||||
}
|
||||
|
||||
return Array.Empty<string>();
|
||||
return new string[] { };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -54,10 +54,7 @@ namespace NzbDrone.Common.Instrumentation
|
||||
new (@"api/v[0-9]/notification/readarr/(?<secret>[\w-]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
// Discord
|
||||
new (@"discord.com/api/webhooks/((?<secret>[\w-]+)/)?(?<secret>[\w-]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
// Telegram
|
||||
new (@"api.telegram.org/bot(?<id>[\d]+):(?<secret>[\w-]+)/", RegexOptions.Compiled | RegexOptions.IgnoreCase)
|
||||
new (@"discord.com/api/webhooks/((?<secret>[\w-]+)/)?(?<secret>[\w-]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase)
|
||||
};
|
||||
|
||||
private static readonly Regex CleanseRemoteIPRegex = new (@"(?:Auth-\w+(?<!Failure|Unauthorized) ip|from) (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})", RegexOptions.Compiled);
|
||||
|
||||
@@ -41,7 +41,7 @@ namespace NzbDrone.Common.Instrumentation
|
||||
RegisterDebugger();
|
||||
}
|
||||
|
||||
RegisterSentry(updateApp, appFolderInfo);
|
||||
RegisterSentry(updateApp);
|
||||
|
||||
if (updateApp)
|
||||
{
|
||||
@@ -62,7 +62,7 @@ namespace NzbDrone.Common.Instrumentation
|
||||
LogManager.ReconfigExistingLoggers();
|
||||
}
|
||||
|
||||
private static void RegisterSentry(bool updateClient, IAppFolderInfo appFolderInfo)
|
||||
private static void RegisterSentry(bool updateClient)
|
||||
{
|
||||
string dsn;
|
||||
|
||||
@@ -77,7 +77,7 @@ namespace NzbDrone.Common.Instrumentation
|
||||
: "https://31e00a6c63ea42c8b5fe70358526a30d@sentry.servarr.com/4";
|
||||
}
|
||||
|
||||
var target = new SentryTarget(dsn, appFolderInfo)
|
||||
var target = new SentryTarget(dsn)
|
||||
{
|
||||
Name = "sentryTarget",
|
||||
Layout = "${message}"
|
||||
|
||||
@@ -9,7 +9,6 @@ using NLog;
|
||||
using NLog.Common;
|
||||
using NLog.Targets;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using Sentry;
|
||||
|
||||
namespace NzbDrone.Common.Instrumentation.Sentry
|
||||
@@ -100,7 +99,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
||||
public bool FilterEvents { get; set; }
|
||||
public bool SentryEnabled { get; set; }
|
||||
|
||||
public SentryTarget(string dsn, IAppFolderInfo appFolderInfo)
|
||||
public SentryTarget(string dsn)
|
||||
{
|
||||
_sdk = SentrySdk.Init(o =>
|
||||
{
|
||||
@@ -108,33 +107,9 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
||||
o.AttachStacktrace = true;
|
||||
o.MaxBreadcrumbs = 200;
|
||||
o.Release = $"{BuildInfo.AppName}@{BuildInfo.Release}";
|
||||
o.SetBeforeSend(x => SentryCleanser.CleanseEvent(x));
|
||||
o.SetBeforeBreadcrumb(x => SentryCleanser.CleanseBreadcrumb(x));
|
||||
o.BeforeSend = x => SentryCleanser.CleanseEvent(x);
|
||||
o.BeforeBreadcrumb = x => SentryCleanser.CleanseBreadcrumb(x);
|
||||
o.Environment = BuildInfo.Branch;
|
||||
|
||||
// Crash free run statistics (sends a ping for healthy and for crashes sessions)
|
||||
o.AutoSessionTracking = false;
|
||||
|
||||
// Caches files in the event device is offline
|
||||
// Sentry creates a 'sentry' sub directory, no need to concat here
|
||||
o.CacheDirectoryPath = appFolderInfo.GetAppDataPath();
|
||||
|
||||
// default environment is production
|
||||
if (!RuntimeInfo.IsProduction)
|
||||
{
|
||||
if (RuntimeInfo.IsDevelopment)
|
||||
{
|
||||
o.Environment = "development";
|
||||
}
|
||||
else if (RuntimeInfo.IsTesting)
|
||||
{
|
||||
o.Environment = "testing";
|
||||
}
|
||||
else
|
||||
{
|
||||
o.Environment = "other";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
InitializeScope();
|
||||
@@ -152,7 +127,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
||||
{
|
||||
SentrySdk.ConfigureScope(scope =>
|
||||
{
|
||||
scope.User = new SentryUser
|
||||
scope.User = new User
|
||||
{
|
||||
Id = HashUtil.AnonymousToken()
|
||||
};
|
||||
@@ -194,7 +169,9 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
||||
|
||||
private void OnError(Exception ex)
|
||||
{
|
||||
if (ex is WebException webException)
|
||||
var webException = ex as WebException;
|
||||
|
||||
if (webException != null)
|
||||
{
|
||||
var response = webException.Response as HttpWebResponse;
|
||||
var statusCode = response?.StatusCode;
|
||||
@@ -313,21 +290,13 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
||||
}
|
||||
}
|
||||
|
||||
var level = LoggingLevelMap[logEvent.Level];
|
||||
var sentryEvent = new SentryEvent(logEvent.Exception)
|
||||
{
|
||||
Level = level,
|
||||
Level = LoggingLevelMap[logEvent.Level],
|
||||
Logger = logEvent.LoggerName,
|
||||
Message = logEvent.FormattedMessage
|
||||
};
|
||||
|
||||
if (level is SentryLevel.Fatal && logEvent.Exception is not null)
|
||||
{
|
||||
// Usages of 'fatal' here indicates the process will crash. In Sentry this is represented with
|
||||
// the 'unhandled' exception flag
|
||||
logEvent.Exception.SetSentryMechanism("Logger.Fatal", "Logger.Fatal was called", false);
|
||||
}
|
||||
|
||||
sentryEvent.SetExtras(extras);
|
||||
sentryEvent.SetFingerprint(fingerPrint);
|
||||
|
||||
|
||||
@@ -6,5 +6,4 @@ public class AuthOptions
|
||||
public bool? Enabled { get; set; }
|
||||
public string Method { get; set; }
|
||||
public string Required { get; set; }
|
||||
public bool? TrustCgnatIpAddresses { get; set; }
|
||||
}
|
||||
|
||||
@@ -313,7 +313,7 @@ namespace NzbDrone.Common.Processes
|
||||
processInfo = new ProcessInfo();
|
||||
processInfo.Id = process.Id;
|
||||
processInfo.Name = process.ProcessName;
|
||||
processInfo.StartPath = process.MainModule?.FileName;
|
||||
processInfo.StartPath = process.MainModule.FileName;
|
||||
|
||||
if (process.Id != GetCurrentProcessId() && process.HasExited)
|
||||
{
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DryIoc.dll" />
|
||||
<PackageReference Include="IPAddressRange" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" />
|
||||
<PackageReference Include="NLog.Extensions.Logging" />
|
||||
|
||||
@@ -200,9 +200,17 @@ namespace NzbDrone.Core.Test.Download
|
||||
var seriesTags = new HashSet<int> { 2 };
|
||||
var clientTags = new HashSet<int> { 1 };
|
||||
|
||||
WithTorrentClient(0, clientTags);
|
||||
WithTorrentClient(0, clientTags);
|
||||
WithTorrentClient(0, clientTags);
|
||||
WithTorrentClient(0, clientTags);
|
||||
|
||||
Assert.Throws<DownloadClientUnavailableException>(() => Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags));
|
||||
var client1 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags);
|
||||
var client2 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags);
|
||||
var client3 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags);
|
||||
var client4 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags);
|
||||
|
||||
Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags).Should().BeNull();
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -312,12 +312,11 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DelugeTests
|
||||
[Test]
|
||||
public void should_return_status_with_outputdirs()
|
||||
{
|
||||
var configItems = new Dictionary<string, object>
|
||||
{
|
||||
{ "download_location", @"C:\Downloads\Downloading\deluge".AsOsAgnostic() },
|
||||
{ "move_completed_path", @"C:\Downloads\Finished\deluge".AsOsAgnostic() },
|
||||
{ "move_completed", true }
|
||||
};
|
||||
var configItems = new Dictionary<string, object>();
|
||||
|
||||
configItems.Add("download_location", @"C:\Downloads\Downloading\deluge".AsOsAgnostic());
|
||||
configItems.Add("move_completed_path", @"C:\Downloads\Finished\deluge".AsOsAgnostic());
|
||||
configItems.Add("move_completed", true);
|
||||
|
||||
Mocker.GetMock<IDelugeProxy>()
|
||||
.Setup(v => v.GetConfig(It.IsAny<DelugeSettings>()))
|
||||
@@ -329,18 +328,5 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DelugeTests
|
||||
result.OutputRootFolders.Should().NotBeNull();
|
||||
result.OutputRootFolders.First().Should().Be(@"C:\Downloads\Finished\deluge".AsOsAgnostic());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_status_with_outputdirs_for_directories_in_settings()
|
||||
{
|
||||
Subject.Definition.Settings.As<DelugeSettings>().DownloadDirectory = @"D:\Downloads\Downloading\deluge".AsOsAgnostic();
|
||||
Subject.Definition.Settings.As<DelugeSettings>().CompletedDirectory = @"D:\Downloads\Finished\deluge".AsOsAgnostic();
|
||||
|
||||
var result = Subject.GetStatus();
|
||||
|
||||
result.IsLocalhost.Should().BeTrue();
|
||||
result.OutputRootFolders.Should().NotBeNull();
|
||||
result.OutputRootFolders.First().Should().Be(@"D:\Downloads\Finished\deluge".AsOsAgnostic());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,9 +178,8 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
||||
VerifyWarning(item);
|
||||
}
|
||||
|
||||
[TestCase("pausedDL")]
|
||||
[TestCase("stoppedDL")]
|
||||
public void paused_item_should_have_required_properties(string state)
|
||||
[Test]
|
||||
public void paused_item_should_have_required_properties()
|
||||
{
|
||||
var torrent = new QBittorrentTorrent
|
||||
{
|
||||
@@ -189,7 +188,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
||||
Size = 1000,
|
||||
Progress = 0.7,
|
||||
Eta = 8640000,
|
||||
State = state,
|
||||
State = "pausedDL",
|
||||
Label = "",
|
||||
SavePath = ""
|
||||
};
|
||||
@@ -201,7 +200,6 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
||||
}
|
||||
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
[TestCase("queuedUP")]
|
||||
[TestCase("uploading")]
|
||||
[TestCase("stalledUP")]
|
||||
@@ -399,9 +397,8 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
||||
result.OutputPath.FullPath.Should().Be(Path.Combine(torrent.SavePath, "Droned.S01.12"));
|
||||
}
|
||||
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
public void api_261_should_use_content_path(string state)
|
||||
[Test]
|
||||
public void api_261_should_use_content_path()
|
||||
{
|
||||
var torrent = new QBittorrentTorrent
|
||||
{
|
||||
@@ -410,7 +407,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
||||
Size = 1000,
|
||||
Progress = 0.7,
|
||||
Eta = 8640000,
|
||||
State = state,
|
||||
State = "pausedUP",
|
||||
Label = "",
|
||||
SavePath = @"C:\Torrents".AsOsAgnostic(),
|
||||
ContentPath = @"C:\Torrents\Droned.S01.12".AsOsAgnostic()
|
||||
@@ -687,96 +684,44 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
||||
item.CanMoveFiles.Should().BeFalse();
|
||||
}
|
||||
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
public void should_not_be_removable_and_should_not_allow_move_files_if_max_ratio_is_not_set(string state)
|
||||
[Test]
|
||||
public void should_not_be_removable_and_should_not_allow_move_files_if_max_ratio_is_not_set()
|
||||
{
|
||||
GivenGlobalSeedLimits(-1);
|
||||
GivenCompletedTorrent(state, ratio: 1.0f);
|
||||
GivenCompletedTorrent("pausedUP", ratio: 1.0f);
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.CanBeRemoved.Should().BeFalse();
|
||||
item.CanMoveFiles.Should().BeFalse();
|
||||
}
|
||||
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
public void should_be_removable_and_should_allow_move_files_if_max_ratio_reached_and_paused(string state)
|
||||
[Test]
|
||||
public void should_be_removable_and_should_allow_move_files_if_max_ratio_reached_and_paused()
|
||||
{
|
||||
GivenGlobalSeedLimits(1.0f);
|
||||
GivenCompletedTorrent(state, ratio: 1.0f);
|
||||
GivenCompletedTorrent("pausedUP", ratio: 1.0f);
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.CanBeRemoved.Should().BeTrue();
|
||||
item.CanMoveFiles.Should().BeTrue();
|
||||
}
|
||||
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
public void should_be_removable_and_should_allow_move_files_if_max_ratio_reached_after_rounding_and_paused(string state)
|
||||
{
|
||||
GivenGlobalSeedLimits(1.0f);
|
||||
GivenCompletedTorrent(state, ratio: 1.1006066990976857f);
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.CanBeRemoved.Should().BeTrue();
|
||||
item.CanMoveFiles.Should().BeTrue();
|
||||
}
|
||||
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
public void should_be_removable_and_should_allow_move_files_if_just_under_max_ratio_reached_after_rounding_and_paused(string state)
|
||||
{
|
||||
GivenGlobalSeedLimits(1.0f);
|
||||
GivenCompletedTorrent(state, ratio: 0.9999f);
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.CanBeRemoved.Should().BeTrue();
|
||||
item.CanMoveFiles.Should().BeTrue();
|
||||
}
|
||||
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
public void should_be_removable_and_should_allow_move_files_if_overridden_max_ratio_reached_and_paused(string state)
|
||||
[Test]
|
||||
public void should_be_removable_and_should_allow_move_files_if_overridden_max_ratio_reached_and_paused()
|
||||
{
|
||||
GivenGlobalSeedLimits(2.0f);
|
||||
GivenCompletedTorrent(state, ratio: 1.0f, ratioLimit: 0.8f);
|
||||
GivenCompletedTorrent("pausedUP", ratio: 1.0f, ratioLimit: 0.8f);
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.CanBeRemoved.Should().BeTrue();
|
||||
item.CanMoveFiles.Should().BeTrue();
|
||||
}
|
||||
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
public void should_be_removable_and_should_allow_move_files_if_overridden_max_ratio_reached_after_rounding_and_paused(string state)
|
||||
{
|
||||
GivenGlobalSeedLimits(2.0f);
|
||||
GivenCompletedTorrent(state, ratio: 1.1006066990976857f, ratioLimit: 1.1f);
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.CanBeRemoved.Should().BeTrue();
|
||||
item.CanMoveFiles.Should().BeTrue();
|
||||
}
|
||||
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
public void should_be_removable_and_should_allow_move_files_if_just_under_overridden_max_ratio_reached_after_rounding_and_paused(string state)
|
||||
{
|
||||
GivenGlobalSeedLimits(2.0f);
|
||||
GivenCompletedTorrent(state, ratio: 0.9999f, ratioLimit: 1.0f);
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.CanBeRemoved.Should().BeTrue();
|
||||
item.CanMoveFiles.Should().BeTrue();
|
||||
}
|
||||
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
public void should_not_be_removable_if_overridden_max_ratio_not_reached_and_paused(string state)
|
||||
[Test]
|
||||
public void should_not_be_removable_if_overridden_max_ratio_not_reached_and_paused()
|
||||
{
|
||||
GivenGlobalSeedLimits(0.2f);
|
||||
GivenCompletedTorrent(state, ratio: 0.5f, ratioLimit: 0.8f);
|
||||
GivenCompletedTorrent("pausedUP", ratio: 0.5f, ratioLimit: 0.8f);
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.CanBeRemoved.Should().BeFalse();
|
||||
@@ -794,36 +739,33 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
||||
item.CanMoveFiles.Should().BeFalse();
|
||||
}
|
||||
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
public void should_be_removable_and_should_allow_move_files_if_max_seedingtime_reached_and_paused(string state)
|
||||
[Test]
|
||||
public void should_be_removable_and_should_allow_move_files_if_max_seedingtime_reached_and_paused()
|
||||
{
|
||||
GivenGlobalSeedLimits(-1, 20);
|
||||
GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 20);
|
||||
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 20);
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.CanBeRemoved.Should().BeTrue();
|
||||
item.CanMoveFiles.Should().BeTrue();
|
||||
}
|
||||
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
public void should_be_removable_and_should_allow_move_files_if_overridden_max_seedingtime_reached_and_paused(string state)
|
||||
[Test]
|
||||
public void should_be_removable_and_should_allow_move_files_if_overridden_max_seedingtime_reached_and_paused()
|
||||
{
|
||||
GivenGlobalSeedLimits(-1, 40);
|
||||
GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 20, seedingTimeLimit: 10);
|
||||
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 20, seedingTimeLimit: 10);
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.CanBeRemoved.Should().BeTrue();
|
||||
item.CanMoveFiles.Should().BeTrue();
|
||||
}
|
||||
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
public void should_not_be_removable_if_overridden_max_seedingtime_not_reached_and_paused(string state)
|
||||
[Test]
|
||||
public void should_not_be_removable_if_overridden_max_seedingtime_not_reached_and_paused()
|
||||
{
|
||||
GivenGlobalSeedLimits(-1, 20);
|
||||
GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 30, seedingTimeLimit: 40);
|
||||
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 30, seedingTimeLimit: 40);
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.CanBeRemoved.Should().BeFalse();
|
||||
@@ -841,72 +783,66 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
||||
item.CanMoveFiles.Should().BeFalse();
|
||||
}
|
||||
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
public void should_be_removable_and_should_allow_move_files_if_max_inactive_seedingtime_reached_and_paused(string state)
|
||||
[Test]
|
||||
public void should_be_removable_and_should_allow_move_files_if_max_inactive_seedingtime_reached_and_paused()
|
||||
{
|
||||
GivenGlobalSeedLimits(-1, maxInactiveSeedingTime: 20);
|
||||
GivenCompletedTorrent(state, ratio: 2.0f, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(25)).ToUnixTimeSeconds());
|
||||
GivenCompletedTorrent("pausedUP", ratio: 2.0f, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(25)).ToUnixTimeSeconds());
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.CanBeRemoved.Should().BeTrue();
|
||||
item.CanMoveFiles.Should().BeTrue();
|
||||
}
|
||||
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
public void should_be_removable_and_should_allow_move_files_if_overridden_max_inactive_seedingtime_reached_and_paused(string state)
|
||||
[Test]
|
||||
public void should_be_removable_and_should_allow_move_files_if_overridden_max_inactive_seedingtime_reached_and_paused()
|
||||
{
|
||||
GivenGlobalSeedLimits(-1, maxInactiveSeedingTime: 40);
|
||||
GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 20, inactiveSeedingTimeLimit: 10, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(15)).ToUnixTimeSeconds());
|
||||
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 20, inactiveSeedingTimeLimit: 10, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(15)).ToUnixTimeSeconds());
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.CanBeRemoved.Should().BeTrue();
|
||||
item.CanMoveFiles.Should().BeTrue();
|
||||
}
|
||||
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
public void should_not_be_removable_if_overridden_max_inactive_seedingtime_not_reached_and_paused(string state)
|
||||
[Test]
|
||||
public void should_not_be_removable_if_overridden_max_inactive_seedingtime_not_reached_and_paused()
|
||||
{
|
||||
GivenGlobalSeedLimits(-1, maxInactiveSeedingTime: 20);
|
||||
GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 30, inactiveSeedingTimeLimit: 40, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(30)).ToUnixTimeSeconds());
|
||||
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 30, inactiveSeedingTimeLimit: 40, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(30)).ToUnixTimeSeconds());
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.CanBeRemoved.Should().BeFalse();
|
||||
item.CanMoveFiles.Should().BeFalse();
|
||||
}
|
||||
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
public void should_be_removable_and_should_allow_move_files_if_max_seedingtime_reached_but_ratio_not_and_paused(string state)
|
||||
[Test]
|
||||
public void should_be_removable_and_should_allow_move_files_if_max_seedingtime_reached_but_ratio_not_and_paused()
|
||||
{
|
||||
GivenGlobalSeedLimits(2.0f, 20);
|
||||
GivenCompletedTorrent(state, ratio: 1.0f, seedingTime: 30);
|
||||
GivenCompletedTorrent("pausedUP", ratio: 1.0f, seedingTime: 30);
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.CanBeRemoved.Should().BeTrue();
|
||||
item.CanMoveFiles.Should().BeTrue();
|
||||
}
|
||||
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
public void should_be_removable_and_should_allow_move_files_if_max_inactive_seedingtime_reached_but_ratio_not_and_paused(string state)
|
||||
[Test]
|
||||
public void should_be_removable_and_should_allow_move_files_if_max_inactive_seedingtime_reached_but_ratio_not_and_paused()
|
||||
{
|
||||
GivenGlobalSeedLimits(2.0f, maxInactiveSeedingTime: 20);
|
||||
GivenCompletedTorrent(state, ratio: 1.0f, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(25)).ToUnixTimeSeconds());
|
||||
GivenCompletedTorrent("pausedUP", ratio: 1.0f, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(25)).ToUnixTimeSeconds());
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.CanBeRemoved.Should().BeTrue();
|
||||
item.CanMoveFiles.Should().BeTrue();
|
||||
}
|
||||
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
public void should_not_fetch_details_twice(string state)
|
||||
[Test]
|
||||
public void should_not_fetch_details_twice()
|
||||
{
|
||||
GivenGlobalSeedLimits(-1, 30);
|
||||
GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 20);
|
||||
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 20);
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.CanBeRemoved.Should().BeFalse();
|
||||
@@ -918,9 +854,8 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
||||
.Verify(p => p.GetTorrentProperties(It.IsAny<string>(), It.IsAny<QBittorrentSettings>()), Times.Once());
|
||||
}
|
||||
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
public void should_get_category_from_the_category_if_set(string state)
|
||||
[Test]
|
||||
public void should_get_category_from_the_category_if_set()
|
||||
{
|
||||
const string category = "music-readarr";
|
||||
GivenGlobalSeedLimits(1.0f);
|
||||
@@ -932,7 +867,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
||||
Size = 1000,
|
||||
Progress = 1.0,
|
||||
Eta = 8640000,
|
||||
State = state,
|
||||
State = "pausedUP",
|
||||
Category = category,
|
||||
SavePath = "",
|
||||
Ratio = 1.0f
|
||||
@@ -944,9 +879,8 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
||||
item.Category.Should().Be(category);
|
||||
}
|
||||
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
public void should_get_category_from_the_label_if_the_category_is_not_available(string state)
|
||||
[Test]
|
||||
public void should_get_category_from_the_label_if_the_category_is_not_available()
|
||||
{
|
||||
const string category = "music-readarr";
|
||||
GivenGlobalSeedLimits(1.0f);
|
||||
@@ -958,7 +892,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
||||
Size = 1000,
|
||||
Progress = 1.0,
|
||||
Eta = 8640000,
|
||||
State = state,
|
||||
State = "pausedUP",
|
||||
Label = category,
|
||||
SavePath = "",
|
||||
Ratio = 1.0f
|
||||
|
||||
@@ -478,37 +478,6 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests
|
||||
downloadClientInfo.RemovesCompletedDownloads.Should().BeTrue();
|
||||
}
|
||||
|
||||
[TestCase("all", 0)]
|
||||
[TestCase("days-archive", 15)]
|
||||
[TestCase("days-delete", 15)]
|
||||
public void should_set_history_removes_completed_downloads_false_for_separate_properties(string option, int number)
|
||||
{
|
||||
_config.Misc.history_retention_option = option;
|
||||
_config.Misc.history_retention_number = number;
|
||||
|
||||
var downloadClientInfo = Subject.GetStatus();
|
||||
|
||||
downloadClientInfo.RemovesCompletedDownloads.Should().BeFalse();
|
||||
}
|
||||
|
||||
[TestCase("number-archive", 10)]
|
||||
[TestCase("number-delete", 10)]
|
||||
[TestCase("number-archive", 0)]
|
||||
[TestCase("number-delete", 0)]
|
||||
[TestCase("days-archive", 3)]
|
||||
[TestCase("days-delete", 3)]
|
||||
[TestCase("all-archive", 0)]
|
||||
[TestCase("all-delete", 0)]
|
||||
public void should_set_history_removes_completed_downloads_true_for_separate_properties(string option, int number)
|
||||
{
|
||||
_config.Misc.history_retention_option = option;
|
||||
_config.Misc.history_retention_number = number;
|
||||
|
||||
var downloadClientInfo = Subject.GetStatus();
|
||||
|
||||
downloadClientInfo.RemovesCompletedDownloads.Should().BeTrue();
|
||||
}
|
||||
|
||||
[TestCase(@"Y:\nzbget\root", @"completed\downloads", @"vv", @"Y:\nzbget\root\completed\downloads", @"Y:\nzbget\root\completed\downloads\vv")]
|
||||
[TestCase(@"Y:\nzbget\root", @"completed", @"vv", @"Y:\nzbget\root\completed", @"Y:\nzbget\root\completed\vv")]
|
||||
[TestCase(@"/nzbget/root", @"completed/downloads", @"vv", @"/nzbget/root/completed/downloads", @"/nzbget/root/completed/downloads/vv")]
|
||||
|
||||
@@ -49,13 +49,10 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.TransmissionTests
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void magnet_download_should_be_returned_as_queued()
|
||||
public void magnet_download_should_not_return_the_item()
|
||||
{
|
||||
PrepareClientToReturnMagnetItem();
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
|
||||
item.Status.Should().Be(DownloadItemStatus.Queued);
|
||||
Subject.GetItems().Count().Should().Be(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -60,10 +60,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.VuzeTests
|
||||
public void magnet_download_should_not_return_the_item()
|
||||
{
|
||||
PrepareClientToReturnMagnetItem();
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
|
||||
item.Status.Should().Be(DownloadItemStatus.Queued);
|
||||
Subject.GetItems().Count().Should().Be(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -7,7 +7,6 @@ using NzbDrone.Core.HealthCheck.Checks;
|
||||
using NzbDrone.Core.Localization;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Core.Update;
|
||||
using NzbDrone.Test.Common;
|
||||
|
||||
namespace NzbDrone.Core.Test.HealthCheck.Checks
|
||||
{
|
||||
@@ -22,10 +21,28 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
|
||||
.Returns("Some Warning Message");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_error_when_app_folder_is_write_protected()
|
||||
{
|
||||
WindowsOnly();
|
||||
|
||||
Mocker.GetMock<IAppFolderInfo>()
|
||||
.Setup(s => s.StartUpFolder)
|
||||
.Returns(@"C:\NzbDrone");
|
||||
|
||||
Mocker.GetMock<IDiskProvider>()
|
||||
.Setup(c => c.FolderWritable(It.IsAny<string>()))
|
||||
.Returns(false);
|
||||
|
||||
Subject.Check().ShouldBeError();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_error_when_app_folder_is_write_protected_and_update_automatically_is_enabled()
|
||||
{
|
||||
var startupFolder = @"C:\NzbDrone".AsOsAgnostic();
|
||||
PosixOnly();
|
||||
|
||||
const string startupFolder = @"/opt/nzbdrone";
|
||||
|
||||
Mocker.GetMock<IConfigFileProvider>()
|
||||
.Setup(s => s.UpdateAutomatically)
|
||||
@@ -45,8 +62,10 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
|
||||
[Test]
|
||||
public void should_return_error_when_ui_folder_is_write_protected_and_update_automatically_is_enabled()
|
||||
{
|
||||
var startupFolder = @"C:\NzbDrone".AsOsAgnostic();
|
||||
var uiFolder = @"C:\NzbDrone\UI".AsOsAgnostic();
|
||||
PosixOnly();
|
||||
|
||||
const string startupFolder = @"/opt/nzbdrone";
|
||||
const string uiFolder = @"/opt/nzbdrone/UI";
|
||||
|
||||
Mocker.GetMock<IConfigFileProvider>()
|
||||
.Setup(s => s.UpdateAutomatically)
|
||||
@@ -70,7 +89,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
|
||||
[Test]
|
||||
public void should_not_return_error_when_app_folder_is_write_protected_and_external_script_enabled()
|
||||
{
|
||||
var startupFolder = @"C:\NzbDrone".AsOsAgnostic();
|
||||
PosixOnly();
|
||||
|
||||
Mocker.GetMock<IConfigFileProvider>()
|
||||
.Setup(s => s.UpdateAutomatically)
|
||||
@@ -82,7 +101,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
|
||||
|
||||
Mocker.GetMock<IAppFolderInfo>()
|
||||
.Setup(s => s.StartUpFolder)
|
||||
.Returns(startupFolder);
|
||||
.Returns(@"/opt/nzbdrone");
|
||||
|
||||
Mocker.GetMock<IDiskProvider>()
|
||||
.Verify(c => c.FolderWritable(It.IsAny<string>()), Times.Never());
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace NzbDrone.Core.Test.Http
|
||||
{
|
||||
private HttpProxySettings GetProxySettings()
|
||||
{
|
||||
return new HttpProxySettings(ProxyType.Socks5, "localhost", 8080, "*.httpbin.org,google.com,172.16.0.0/12", true, null, null);
|
||||
return new HttpProxySettings(ProxyType.Socks5, "localhost", 8080, "*.httpbin.org,google.com", true, null, null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -23,7 +23,6 @@ namespace NzbDrone.Core.Test.Http
|
||||
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://eu.httpbin.org/get")).Should().BeTrue();
|
||||
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://google.com/get")).Should().BeTrue();
|
||||
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://localhost:8654/get")).Should().BeTrue();
|
||||
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://172.21.0.1:8989/api/v3/indexer/schema")).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -32,7 +31,6 @@ namespace NzbDrone.Core.Test.Http
|
||||
var settings = GetProxySettings();
|
||||
|
||||
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://bing.com/get")).Should().BeFalse();
|
||||
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://172.3.0.1:8989/api/v3/indexer/schema")).Should().BeFalse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ using NzbDrone.Core.Test.Framework;
|
||||
namespace NzbDrone.Core.Test.MetadataSource.Goodreads
|
||||
{
|
||||
[TestFixture]
|
||||
[Ignore("Waiting for metadata to be back again", Until = "2025-05-15 00:00:00Z")]
|
||||
[Ignore("Waiting for metadata to be back again", Until = "2024-08-15 00:00:00Z")]
|
||||
public class BookInfoProxyFixture : CoreTest<BookInfoProxy>
|
||||
{
|
||||
private MetadataProfile _metadataProfile;
|
||||
|
||||
@@ -15,7 +15,7 @@ using NzbDrone.Test.Common;
|
||||
namespace NzbDrone.Core.Test.MetadataSource.Goodreads
|
||||
{
|
||||
[TestFixture]
|
||||
[Ignore("Waiting for metadata to be back again", Until = "2025-05-15 00:00:00Z")]
|
||||
[Ignore("Waiting for metadata to be back again", Until = "2024-08-15 00:00:00Z")]
|
||||
public class BookInfoProxySearchFixture : CoreTest<BookInfoProxy>
|
||||
{
|
||||
[SetUp]
|
||||
|
||||
@@ -40,7 +40,7 @@ namespace NzbDrone.Core.Test.MetadataSource.Goodreads
|
||||
|
||||
[TestCase("Harry Potter and the sorcerer's stone a detailed summary", 72245296)]
|
||||
[TestCase("B0192CTMYG", 61209488)]
|
||||
[TestCase("9780439554930", 3)]
|
||||
[TestCase("9780439554930", 48517161)]
|
||||
public void successful_book_search(string title, int expected)
|
||||
{
|
||||
var result = Subject.Search(title);
|
||||
|
||||
@@ -21,14 +21,14 @@ namespace NzbDrone.Core.Test.UpdateTests
|
||||
public void no_update_when_version_higher()
|
||||
{
|
||||
UseRealHttp();
|
||||
Subject.GetLatestUpdate("develop", new Version(10, 0)).Should().BeNull();
|
||||
Subject.GetLatestUpdate("nightly", new Version(10, 0)).Should().BeNull();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void finds_update_when_version_lower()
|
||||
{
|
||||
UseRealHttp();
|
||||
Subject.GetLatestUpdate("develop", new Version(0, 1)).Should().NotBeNull();
|
||||
Subject.GetLatestUpdate("nightly", new Version(0, 1)).Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -42,9 +42,10 @@ namespace NzbDrone.Core.Test.UpdateTests
|
||||
[Test]
|
||||
public void should_get_recent_updates()
|
||||
{
|
||||
const string branch = "develop";
|
||||
const string branch = "nightly";
|
||||
UseRealHttp();
|
||||
var recent = Subject.GetRecentUpdates(branch, new Version(0, 1), null);
|
||||
var recentWithChanges = recent.Where(c => c.Changes != null);
|
||||
|
||||
recent.Should().NotBeEmpty();
|
||||
recent.Should().OnlyContain(c => c.Hash.IsNotNullOrWhiteSpace());
|
||||
|
||||
@@ -66,19 +66,12 @@ namespace NzbDrone.Core.Backup
|
||||
{
|
||||
_logger.ProgressInfo("Starting Backup");
|
||||
|
||||
var backupFolder = GetBackupFolder(backupType);
|
||||
|
||||
_diskProvider.EnsureFolder(_backupTempFolder);
|
||||
_diskProvider.EnsureFolder(backupFolder);
|
||||
|
||||
if (!_diskProvider.FolderWritable(backupFolder))
|
||||
{
|
||||
throw new UnauthorizedAccessException($"Backup folder {backupFolder} is not writable");
|
||||
}
|
||||
_diskProvider.EnsureFolder(GetBackupFolder(backupType));
|
||||
|
||||
var dateNow = DateTime.Now;
|
||||
var backupFilename = $"readarr_backup_v{BuildInfo.Version}_{dateNow:yyyy.MM.dd_HH.mm.ss}.zip";
|
||||
var backupPath = Path.Combine(backupFolder, backupFilename);
|
||||
var backupPath = Path.Combine(GetBackupFolder(backupType), backupFilename);
|
||||
|
||||
Cleanup();
|
||||
|
||||
|
||||
@@ -102,9 +102,9 @@ namespace NzbDrone.Core.Books
|
||||
_logger.Error("ReadarrId {0} was not found, it may have been removed from Goodreads.", newAuthor.Metadata.Value.ForeignAuthorId);
|
||||
|
||||
throw new ValidationException(new List<ValidationFailure>
|
||||
{
|
||||
new ("ForeignAuthorId", "An author with this ID was not found", newAuthor.Metadata.Value.ForeignAuthorId)
|
||||
});
|
||||
{
|
||||
new ValidationFailure("MusicbrainzId", "An author with this ID was not found", newAuthor.Metadata.Value.ForeignAuthorId)
|
||||
});
|
||||
}
|
||||
|
||||
author.ApplyChanges(newAuthor);
|
||||
|
||||
@@ -53,7 +53,6 @@ namespace NzbDrone.Core.Configuration
|
||||
string SyslogServer { get; }
|
||||
int SyslogPort { get; }
|
||||
string SyslogLevel { get; }
|
||||
string Theme { get; }
|
||||
string PostgresHost { get; }
|
||||
int PostgresPort { get; }
|
||||
string PostgresUser { get; }
|
||||
@@ -61,7 +60,7 @@ namespace NzbDrone.Core.Configuration
|
||||
string PostgresMainDb { get; }
|
||||
string PostgresLogDb { get; }
|
||||
string PostgresCacheDb { get; }
|
||||
bool TrustCgnatIpAddresses { get; }
|
||||
string Theme { get; }
|
||||
}
|
||||
|
||||
public class ConfigFileProvider : IConfigFileProvider
|
||||
@@ -220,7 +219,7 @@ namespace NzbDrone.Core.Configuration
|
||||
// TODO: Change back to "master" for the first stable release
|
||||
public string Branch => _updateOptions.Branch ?? GetValue("Branch", "develop").ToLowerInvariant();
|
||||
|
||||
public string LogLevel => _logOptions.Level ?? GetValue("LogLevel", "debug").ToLowerInvariant();
|
||||
public string LogLevel => _logOptions.Level ?? GetValue("LogLevel", "info").ToLowerInvariant();
|
||||
public string ConsoleLogLevel => _logOptions.ConsoleLevel ?? GetValue("ConsoleLogLevel", string.Empty, persist: false);
|
||||
|
||||
public string PostgresHost => _postgresOptions?.Host ?? GetValue("PostgresHost", string.Empty, persist: false);
|
||||
@@ -254,23 +253,9 @@ namespace NzbDrone.Core.Configuration
|
||||
}
|
||||
|
||||
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", false, false);
|
||||
|
||||
public UpdateMechanism UpdateMechanism =>
|
||||
Enum.TryParse<UpdateMechanism>(_updateOptions.Mechanism, out var enumValue)
|
||||
@@ -372,7 +357,7 @@ namespace NzbDrone.Core.Configuration
|
||||
}
|
||||
|
||||
// If SSL is enabled and a cert hash is still in the config file or cert path is empty disable SSL
|
||||
if (EnableSsl && (GetValue("SslCertHash", string.Empty, false).IsNotNullOrWhiteSpace() || SslCertPath.IsNullOrWhiteSpace()))
|
||||
if (EnableSsl && (GetValue("SslCertHash", null).IsNotNullOrWhiteSpace() || SslCertPath.IsNullOrWhiteSpace()))
|
||||
{
|
||||
SetValue("EnableSsl", false);
|
||||
}
|
||||
@@ -477,7 +462,5 @@ namespace NzbDrone.Core.Configuration
|
||||
{
|
||||
SetValue("ApiKey", GenerateApiKey());
|
||||
}
|
||||
|
||||
public bool TrustCgnatIpAddresses => _authOptions.TrustCgnatIpAddresses ?? GetValueBoolean("TrustCgnatIpAddresses", false, persist: false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,12 +404,6 @@ namespace NzbDrone.Core.Configuration
|
||||
|
||||
public string ApplicationUrl => GetValue("ApplicationUrl", string.Empty);
|
||||
|
||||
public bool TrustCgnatIpAddresses
|
||||
{
|
||||
get { return GetValueBoolean("TrustCgnatIpAddresses", false); }
|
||||
set { SetValue("TrustCgnatIpAddresses", value); }
|
||||
}
|
||||
|
||||
private string GetValue(string key)
|
||||
{
|
||||
return GetValue(key, string.Empty);
|
||||
|
||||
@@ -213,18 +213,9 @@ namespace NzbDrone.Core.Download.Clients.Deluge
|
||||
{
|
||||
var config = _proxy.GetConfig(Settings);
|
||||
var label = _proxy.GetLabelOptions(Settings);
|
||||
|
||||
OsPath destDir;
|
||||
|
||||
if (Settings.CompletedDirectory.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
destDir = new OsPath(Settings.CompletedDirectory);
|
||||
}
|
||||
else if (Settings.DownloadDirectory.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
destDir = new OsPath(Settings.DownloadDirectory);
|
||||
}
|
||||
else if (label is { ApplyMoveCompleted: true, MoveCompleted: true })
|
||||
if (label != null && label.ApplyMoveCompleted && label.MoveCompleted)
|
||||
{
|
||||
// if label exists and a label completed path exists and is enabled use it instead of global
|
||||
destDir = new OsPath(label.MoveCompletedPath);
|
||||
@@ -240,7 +231,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge
|
||||
|
||||
var status = new DownloadClientInfo
|
||||
{
|
||||
IsLocalhost = Settings.Host is "127.0.0.1" or "localhost"
|
||||
IsLocalhost = Settings.Host == "127.0.0.1" || Settings.Host == "localhost"
|
||||
};
|
||||
|
||||
if (!destDir.IsEmpty)
|
||||
|
||||
@@ -239,7 +239,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
|
||||
// Avoid removing torrents that haven't reached the global max ratio.
|
||||
// Removal also requires the torrent to be paused, in case a higher max ratio was set on the torrent itself (which is not exposed by the api).
|
||||
item.CanMoveFiles = item.CanBeRemoved = torrent.State is "pausedUP" or "stoppedUP" && HasReachedSeedLimit(torrent, config);
|
||||
item.CanMoveFiles = item.CanBeRemoved = torrent.State == "pausedUP" && HasReachedSeedLimit(torrent, config);
|
||||
|
||||
switch (torrent.State)
|
||||
{
|
||||
@@ -248,8 +248,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
item.Message = "qBittorrent is reporting an error";
|
||||
break;
|
||||
|
||||
case "stoppedDL": // torrent is stopped and has NOT finished downloading
|
||||
case "pausedDL": // torrent is paused and has NOT finished downloading (qBittorrent < 5)
|
||||
case "pausedDL": // torrent is paused and has NOT finished downloading
|
||||
item.Status = DownloadItemStatus.Paused;
|
||||
break;
|
||||
|
||||
@@ -260,8 +259,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
item.Status = DownloadItemStatus.Queued;
|
||||
break;
|
||||
|
||||
case "pausedUP": // torrent is paused and has finished downloading (qBittorent < 5)
|
||||
case "stoppedUP": // torrent is stopped and has finished downloading
|
||||
case "pausedUP": // torrent is paused and has finished downloading
|
||||
case "uploading": // torrent is being seeded and data is being transferred
|
||||
case "stalledUP": // torrent is being seeded, but no connection were made
|
||||
case "queuedUP": // queuing is enabled and torrent is queued for upload
|
||||
@@ -620,14 +618,14 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
{
|
||||
if (torrent.RatioLimit >= 0)
|
||||
{
|
||||
if (torrent.RatioLimit - torrent.Ratio <= 0.001f)
|
||||
if (torrent.Ratio >= torrent.RatioLimit)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (torrent.RatioLimit == -2 && config.MaxRatioEnabled)
|
||||
{
|
||||
if (config.MaxRatio - torrent.Ratio <= 0.001f)
|
||||
if (Math.Round(torrent.Ratio, 2) >= config.MaxRatio)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
Dictionary<string, QBittorrentLabel> GetLabels(QBittorrentSettings settings);
|
||||
void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings);
|
||||
void MoveTorrentToTopInQueue(string hash, QBittorrentSettings settings);
|
||||
void PauseTorrent(string hash, QBittorrentSettings settings);
|
||||
void ResumeTorrent(string hash, QBittorrentSettings settings);
|
||||
void SetForceStart(string hash, bool enabled, QBittorrentSettings settings);
|
||||
}
|
||||
|
||||
|
||||
@@ -148,7 +148,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
{
|
||||
request.AddFormParameter("paused", false);
|
||||
}
|
||||
else if ((QBittorrentState)settings.InitialState == QBittorrentState.Stop)
|
||||
else if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause)
|
||||
{
|
||||
request.AddFormParameter("paused", true);
|
||||
}
|
||||
@@ -178,7 +178,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
{
|
||||
request.AddFormParameter("paused", false);
|
||||
}
|
||||
else if ((QBittorrentState)settings.InitialState == QBittorrentState.Stop)
|
||||
else if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause)
|
||||
{
|
||||
request.AddFormParameter("paused", true);
|
||||
}
|
||||
@@ -214,7 +214,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
catch (DownloadClientException ex)
|
||||
{
|
||||
// if setCategory fails due to method not being found, then try older setLabel command for qBittorrent < v.3.3.5
|
||||
if (ex.InnerException is HttpException httpException && httpException.Response.StatusCode == HttpStatusCode.NotFound)
|
||||
if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
var setLabelRequest = BuildRequest(settings).Resource("/command/setLabel")
|
||||
.Post()
|
||||
@@ -257,7 +257,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
catch (DownloadClientException ex)
|
||||
{
|
||||
// qBittorrent rejects all Prio commands with 403: Forbidden if Options -> BitTorrent -> Torrent Queueing is not enabled
|
||||
if (ex.InnerException is HttpException httpException && httpException.Response.StatusCode == HttpStatusCode.Forbidden)
|
||||
if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.Forbidden)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -266,6 +266,22 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
}
|
||||
}
|
||||
|
||||
public void PauseTorrent(string hash, QBittorrentSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings).Resource("/command/pause")
|
||||
.Post()
|
||||
.AddFormParameter("hash", hash);
|
||||
ProcessRequest(request, settings);
|
||||
}
|
||||
|
||||
public void ResumeTorrent(string hash, QBittorrentSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings).Resource("/command/resume")
|
||||
.Post()
|
||||
.AddFormParameter("hash", hash);
|
||||
ProcessRequest(request, settings);
|
||||
}
|
||||
|
||||
public void SetForceStart(string hash, bool enabled, QBittorrentSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings).Resource("/command/setForceStart")
|
||||
|
||||
@@ -246,20 +246,14 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
request.AddFormParameter("category", settings.MusicCategory);
|
||||
}
|
||||
|
||||
// Avoid extraneous API version check if initial state is ForceStart
|
||||
if ((QBittorrentState)settings.InitialState is QBittorrentState.Start or QBittorrentState.Stop)
|
||||
// Note: ForceStart is handled by separate api call
|
||||
if ((QBittorrentState)settings.InitialState == QBittorrentState.Start)
|
||||
{
|
||||
var stoppedParameterName = GetApiVersion(settings) >= new Version(2, 11, 0) ? "stopped" : "paused";
|
||||
|
||||
// Note: ForceStart is handled by separate api call
|
||||
if ((QBittorrentState)settings.InitialState == QBittorrentState.Start)
|
||||
{
|
||||
request.AddFormParameter(stoppedParameterName, false);
|
||||
}
|
||||
else if ((QBittorrentState)settings.InitialState == QBittorrentState.Stop)
|
||||
{
|
||||
request.AddFormParameter(stoppedParameterName, true);
|
||||
}
|
||||
request.AddFormParameter("paused", false);
|
||||
}
|
||||
else if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause)
|
||||
{
|
||||
request.AddFormParameter("paused", true);
|
||||
}
|
||||
|
||||
if (settings.SequentialOrder)
|
||||
@@ -297,7 +291,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
catch (DownloadClientException ex)
|
||||
{
|
||||
// setShareLimits was added in api v2.0.1 so catch it case of the unlikely event that someone has api v2.0
|
||||
if (ex.InnerException is HttpException httpException && httpException.Response.StatusCode == HttpStatusCode.NotFound)
|
||||
if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -319,7 +313,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
catch (DownloadClientException ex)
|
||||
{
|
||||
// qBittorrent rejects all Prio commands with 409: Conflict if Options -> BitTorrent -> Torrent Queueing is not enabled
|
||||
if (ex.InnerException is HttpException httpException && httpException.Response.StatusCode == HttpStatusCode.Conflict)
|
||||
if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.Conflict)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -328,6 +322,22 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
}
|
||||
}
|
||||
|
||||
public void PauseTorrent(string hash, QBittorrentSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings).Resource("/api/v2/torrents/pause")
|
||||
.Post()
|
||||
.AddFormParameter("hashes", hash);
|
||||
ProcessRequest(request, settings);
|
||||
}
|
||||
|
||||
public void ResumeTorrent(string hash, QBittorrentSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings).Resource("/api/v2/torrents/resume")
|
||||
.Post()
|
||||
.AddFormParameter("hashes", hash);
|
||||
ProcessRequest(request, settings);
|
||||
}
|
||||
|
||||
public void SetForceStart(string hash, bool enabled, QBittorrentSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings).Resource("/api/v2/torrents/setForceStart")
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
using NzbDrone.Core.Annotations;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
{
|
||||
public enum QBittorrentState
|
||||
{
|
||||
[FieldOption(Label = "Started")]
|
||||
Start = 0,
|
||||
|
||||
[FieldOption(Label = "Force Started")]
|
||||
ForceStart = 1,
|
||||
|
||||
[FieldOption(Label = "Stopped")]
|
||||
Stop = 2
|
||||
Pause = 2
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,7 +263,20 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
|
||||
status.OutputRootFolders = new List<OsPath> { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, category.FullPath) };
|
||||
}
|
||||
|
||||
status.RemovesCompletedDownloads = RemovesCompletedDownloads(config);
|
||||
if (config.Misc.history_retention.IsNullOrWhiteSpace())
|
||||
{
|
||||
status.RemovesCompletedDownloads = false;
|
||||
}
|
||||
else if (config.Misc.history_retention.EndsWith("d"))
|
||||
{
|
||||
int.TryParse(config.Misc.history_retention.AsSpan(0, config.Misc.history_retention.Length - 1),
|
||||
out var daysRetention);
|
||||
status.RemovesCompletedDownloads = daysRetention < 14;
|
||||
}
|
||||
else
|
||||
{
|
||||
status.RemovesCompletedDownloads = config.Misc.history_retention != "0";
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
@@ -505,43 +518,6 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
|
||||
return categories.Contains(category);
|
||||
}
|
||||
|
||||
private bool RemovesCompletedDownloads(SabnzbdConfig config)
|
||||
{
|
||||
var retention = config.Misc.history_retention;
|
||||
var option = config.Misc.history_retention_option;
|
||||
var number = config.Misc.history_retention_number;
|
||||
|
||||
switch (option)
|
||||
{
|
||||
case "all":
|
||||
return false;
|
||||
case "number-archive":
|
||||
case "number-delete":
|
||||
return true;
|
||||
case "days-archive":
|
||||
case "days-delete":
|
||||
return number < 14;
|
||||
case "all-archive":
|
||||
case "all-delete":
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO: Remove these checks once support for SABnzbd < 4.3 is removed
|
||||
if (retention.IsNullOrWhiteSpace())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (retention.EndsWith("d"))
|
||||
{
|
||||
int.TryParse(config.Misc.history_retention.AsSpan(0, config.Misc.history_retention.Length - 1),
|
||||
out var daysRetention);
|
||||
return daysRetention < 14;
|
||||
}
|
||||
|
||||
return retention != "0";
|
||||
}
|
||||
|
||||
private bool ValidatePath(DownloadClientItem downloadClientItem)
|
||||
{
|
||||
var downloadItemOutputPath = downloadClientItem.OutputPath;
|
||||
|
||||
@@ -30,8 +30,6 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
|
||||
public bool enable_date_sorting { get; set; }
|
||||
public bool pre_check { get; set; }
|
||||
public string history_retention { get; set; }
|
||||
public string history_retention_option { get; set; }
|
||||
public int history_retention_number { get; set; }
|
||||
}
|
||||
|
||||
public class SabnzbdCategory
|
||||
|
||||
@@ -41,6 +41,12 @@ namespace NzbDrone.Core.Download.Clients.Transmission
|
||||
|
||||
foreach (var torrent in torrents)
|
||||
{
|
||||
// If totalsize == 0 the torrent is a magnet downloading metadata
|
||||
if (torrent.TotalSize == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var outputPath = new OsPath(torrent.DownloadDir);
|
||||
|
||||
if (Settings.TvDirectory.IsNotNullOrWhiteSpace())
|
||||
@@ -91,10 +97,6 @@ namespace NzbDrone.Core.Download.Clients.Transmission
|
||||
item.Status = DownloadItemStatus.Warning;
|
||||
item.Message = torrent.ErrorString;
|
||||
}
|
||||
else if (torrent.TotalSize == 0)
|
||||
{
|
||||
item.Status = DownloadItemStatus.Queued;
|
||||
}
|
||||
else if (torrent.LeftUntilDone == 0 && (torrent.Status == TransmissionTorrentStatus.Stopped ||
|
||||
torrent.Status == TransmissionTorrentStatus.Seeding ||
|
||||
torrent.Status == TransmissionTorrentStatus.SeedingWait))
|
||||
|
||||
@@ -4,7 +4,6 @@ using System.Net;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Common.Serializer;
|
||||
@@ -209,7 +208,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission
|
||||
|
||||
private void AuthenticateClient(HttpRequestBuilder requestBuilder, TransmissionSettings settings, bool reauthenticate = false)
|
||||
{
|
||||
var authKey = $"{requestBuilder.BaseUrl}:{settings.Password}";
|
||||
var authKey = string.Format("{0}:{1}", requestBuilder.BaseUrl, settings.Password);
|
||||
|
||||
var sessionId = _authSessionIDCache.Find(authKey);
|
||||
|
||||
@@ -221,26 +220,24 @@ namespace NzbDrone.Core.Download.Clients.Transmission
|
||||
authLoginRequest.SuppressHttpError = true;
|
||||
|
||||
var response = _httpClient.Execute(authLoginRequest);
|
||||
|
||||
switch (response.StatusCode)
|
||||
if (response.StatusCode == HttpStatusCode.MovedPermanently)
|
||||
{
|
||||
case HttpStatusCode.MovedPermanently:
|
||||
var url = response.Headers.GetSingleValue("Location");
|
||||
var url = response.Headers.GetSingleValue("Location");
|
||||
|
||||
throw new DownloadClientException("Remote site redirected to " + url);
|
||||
case HttpStatusCode.Forbidden:
|
||||
throw new DownloadClientException($"Failed to authenticate with Transmission. It may be necessary to add {BuildInfo.AppName}'s IP address to RPC whitelist.");
|
||||
case HttpStatusCode.Conflict:
|
||||
sessionId = response.Headers.GetSingleValue("X-Transmission-Session-Id");
|
||||
throw new DownloadClientException("Remote site redirected to " + url);
|
||||
}
|
||||
else if (response.StatusCode == HttpStatusCode.Conflict)
|
||||
{
|
||||
sessionId = response.Headers.GetSingleValue("X-Transmission-Session-Id");
|
||||
|
||||
if (sessionId == null)
|
||||
{
|
||||
throw new DownloadClientException("Remote host did not return a Session Id.");
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
throw new DownloadClientAuthenticationException("Failed to authenticate with Transmission.");
|
||||
if (sessionId == null)
|
||||
{
|
||||
throw new DownloadClientException("Remote host did not return a Session Id.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new DownloadClientAuthenticationException("Failed to authenticate with Transmission.");
|
||||
}
|
||||
|
||||
_logger.Debug("Transmission authentication succeeded.");
|
||||
|
||||
@@ -41,23 +41,18 @@ namespace NzbDrone.Core.Download
|
||||
var blockedProviders = new HashSet<int>(_downloadClientStatusService.GetBlockedProviders().Select(v => v.ProviderId));
|
||||
var availableProviders = _downloadClientFactory.GetAvailableProviders().Where(v => v.Protocol == downloadProtocol).ToList();
|
||||
|
||||
if (!availableProviders.Any())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (tags is { Count: > 0 })
|
||||
if (tags != null)
|
||||
{
|
||||
var matchingTagsClients = availableProviders.Where(i => i.Definition.Tags.Intersect(tags).Any()).ToList();
|
||||
|
||||
availableProviders = matchingTagsClients.Count > 0 ?
|
||||
matchingTagsClients :
|
||||
availableProviders.Where(i => i.Definition.Tags.Empty()).ToList();
|
||||
}
|
||||
|
||||
if (!availableProviders.Any())
|
||||
{
|
||||
throw new DownloadClientUnavailableException("No download client was found without tags or a matching author tag. Please check your settings.");
|
||||
}
|
||||
if (!availableProviders.Any())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (indexerId > 0)
|
||||
|
||||
@@ -4,24 +4,24 @@ namespace NzbDrone.Core.Exceptions
|
||||
{
|
||||
public class AuthorNotFoundException : NzbDroneException
|
||||
{
|
||||
public string ForeignAuthorId { get; set; }
|
||||
public string MusicBrainzId { get; set; }
|
||||
|
||||
public AuthorNotFoundException(string foreignAuthorId)
|
||||
: base($"Author with id {foreignAuthorId} was not found, it may have been removed from the metadata server.")
|
||||
public AuthorNotFoundException(string musicbrainzId)
|
||||
: base(string.Format("Author with id {0} was not found, it may have been removed from the metadata server.", musicbrainzId))
|
||||
{
|
||||
ForeignAuthorId = foreignAuthorId;
|
||||
MusicBrainzId = musicbrainzId;
|
||||
}
|
||||
|
||||
public AuthorNotFoundException(string foreignAuthorId, string message, params object[] args)
|
||||
public AuthorNotFoundException(string musicbrainzId, string message, params object[] args)
|
||||
: base(message, args)
|
||||
{
|
||||
ForeignAuthorId = foreignAuthorId;
|
||||
MusicBrainzId = musicbrainzId;
|
||||
}
|
||||
|
||||
public AuthorNotFoundException(string foreignAuthorId, string message)
|
||||
public AuthorNotFoundException(string musicbrainzId, string message)
|
||||
: base(message)
|
||||
{
|
||||
ForeignAuthorId = foreignAuthorId;
|
||||
MusicBrainzId = musicbrainzId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,24 +4,24 @@ namespace NzbDrone.Core.Exceptions
|
||||
{
|
||||
public class BookNotFoundException : NzbDroneException
|
||||
{
|
||||
public string ForeignBookId { get; set; }
|
||||
public string MusicBrainzId { get; set; }
|
||||
|
||||
public BookNotFoundException(string foreignBookId)
|
||||
: base($"Book with id {foreignBookId} was not found, it may have been removed from metadata server.")
|
||||
public BookNotFoundException(string musicbrainzId)
|
||||
: base(string.Format("Book with id {0} was not found, it may have been removed from metadata server.", musicbrainzId))
|
||||
{
|
||||
ForeignBookId = foreignBookId;
|
||||
MusicBrainzId = musicbrainzId;
|
||||
}
|
||||
|
||||
public BookNotFoundException(string foreignBookId, string message, params object[] args)
|
||||
public BookNotFoundException(string musicbrainzId, string message, params object[] args)
|
||||
: base(message, args)
|
||||
{
|
||||
ForeignBookId = foreignBookId;
|
||||
MusicBrainzId = musicbrainzId;
|
||||
}
|
||||
|
||||
public BookNotFoundException(string foreignBookId, string message)
|
||||
public BookNotFoundException(string musicbrainzId, string message)
|
||||
: base(message)
|
||||
{
|
||||
ForeignBookId = foreignBookId;
|
||||
MusicBrainzId = musicbrainzId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,24 +4,24 @@ namespace NzbDrone.Core.Exceptions
|
||||
{
|
||||
public class EditionNotFoundException : NzbDroneException
|
||||
{
|
||||
public string ForeignEditionId { get; set; }
|
||||
public string MusicBrainzId { get; set; }
|
||||
|
||||
public EditionNotFoundException(string foreignEditionId)
|
||||
: base($"Edition with id {foreignEditionId} was not found, it may have been removed from metadata server.")
|
||||
public EditionNotFoundException(string musicbrainzId)
|
||||
: base(string.Format("Edition with id {0} was not found, it may have been removed from metadata server.", musicbrainzId))
|
||||
{
|
||||
ForeignEditionId = foreignEditionId;
|
||||
MusicBrainzId = musicbrainzId;
|
||||
}
|
||||
|
||||
public EditionNotFoundException(string foreignEditionId, string message, params object[] args)
|
||||
public EditionNotFoundException(string musicbrainzId, string message, params object[] args)
|
||||
: base(message, args)
|
||||
{
|
||||
ForeignEditionId = foreignEditionId;
|
||||
MusicBrainzId = musicbrainzId;
|
||||
}
|
||||
|
||||
public EditionNotFoundException(string foreignEditionId, string message)
|
||||
public EditionNotFoundException(string musicbrainzId, string message)
|
||||
: base(message)
|
||||
{
|
||||
ForeignEditionId = foreignEditionId;
|
||||
MusicBrainzId = musicbrainzId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
var startupFolder = _appFolderInfo.StartUpFolder;
|
||||
var uiFolder = Path.Combine(startupFolder, "UI");
|
||||
|
||||
if (_configFileProvider.UpdateAutomatically &&
|
||||
if ((OsInfo.IsWindows || _configFileProvider.UpdateAutomatically) &&
|
||||
_configFileProvider.UpdateMechanism == UpdateMechanism.BuiltIn &&
|
||||
!_osInfo.IsDocker)
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user