mirror of
https://github.com/Readarr/Readarr.git
synced 2026-03-24 17:24:30 -04:00
Compare commits
1 Commits
develop
...
sonarr-pul
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b1045aa5e |
20
README.md
20
README.md
@@ -1,23 +1,3 @@
|
|||||||
# Announcement: Retirement of Readarr
|
|
||||||
|
|
||||||
We would like to announce that the [Readarr project](<https://github.com/Readarr/Readarr>) has been retired. This difficult decision was made due to a combination of factors: the project's metadata has become unusable, we no longer have the time to remake or repair it, and the community effort to transition to using Open Library as the source has stalled without much progress.
|
|
||||||
|
|
||||||
Third-party metadata mirrors exist, but as we're not involved with them at all, we cannot provide support for them. Use of them is entirely at your own risk. The most popular mirror appears to be [rreading-glasses](<https://github.com/blampe/rreading-glasses>).
|
|
||||||
|
|
||||||
Without anyone to take over Readarr development, we expect it to wither away, so we still encourage you to seek alternatives to Readarr.
|
|
||||||
|
|
||||||
## Key Points:
|
|
||||||
- **Effective Immediately**: The retirement takes effect immediately. Please stay tuned for any possible further communications.
|
|
||||||
- **Support Window**: We will provide support during a brief transition period to help with troubleshooting non metadata related issues.
|
|
||||||
- **Alternative Solutions**: Users are encouraged to explore and adopt any other possible solutions as alternatives to Readarr.
|
|
||||||
- **Opportunities for Revival**: We are open to someone taking over and revitalizing the project. If you are interested, please get in touch.
|
|
||||||
- **Gratitude**: We extend our deepest gratitude to all the contributors and community members who supported Readarr over the years.
|
|
||||||
|
|
||||||
Thank you for being part of the Readarr journey. For any inquiries or assistance during this transition, please contact our team.
|
|
||||||
|
|
||||||
Sincerely,
|
|
||||||
The Servarr Team
|
|
||||||
|
|
||||||
# Readarr
|
# Readarr
|
||||||
|
|
||||||
[](https://dev.azure.com/Readarr/Readarr/_build/latest?definitionId=1&branchName=develop)
|
[](https://dev.azure.com/Readarr/Readarr/_build/latest?definitionId=1&branchName=develop)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ variables:
|
|||||||
testsFolder: './_tests'
|
testsFolder: './_tests'
|
||||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||||
majorVersion: '0.4.19'
|
majorVersion: '0.4.5'
|
||||||
minorVersion: $[counter('minorVersion', 1)]
|
minorVersion: $[counter('minorVersion', 1)]
|
||||||
readarrVersion: '$(majorVersion).$(minorVersion)'
|
readarrVersion: '$(majorVersion).$(minorVersion)'
|
||||||
buildName: '$(Build.SourceBranchName).$(readarrVersion)'
|
buildName: '$(Build.SourceBranchName).$(readarrVersion)'
|
||||||
@@ -19,7 +19,7 @@ variables:
|
|||||||
nodeVersion: '20.X'
|
nodeVersion: '20.X'
|
||||||
innoVersion: '6.2.0'
|
innoVersion: '6.2.0'
|
||||||
windowsImage: 'windows-2022'
|
windowsImage: 'windows-2022'
|
||||||
linuxImage: 'ubuntu-22.04'
|
linuxImage: 'ubuntu-20.04'
|
||||||
macImage: 'macOS-13'
|
macImage: 'macOS-13'
|
||||||
|
|
||||||
trigger:
|
trigger:
|
||||||
@@ -1102,19 +1102,19 @@ stages:
|
|||||||
vmImage: ${{ variables.windowsImage }}
|
vmImage: ${{ variables.windowsImage }}
|
||||||
steps:
|
steps:
|
||||||
- checkout: self # Need history for Sonar analysis
|
- checkout: self # Need history for Sonar analysis
|
||||||
- task: SonarCloudPrepare@3
|
- task: SonarCloudPrepare@2
|
||||||
env:
|
env:
|
||||||
SONAR_SCANNER_OPTS: ''
|
SONAR_SCANNER_OPTS: ''
|
||||||
inputs:
|
inputs:
|
||||||
SonarCloud: 'SonarCloud'
|
SonarCloud: 'SonarCloud'
|
||||||
organization: 'readarr'
|
organization: 'readarr'
|
||||||
scannerMode: 'cli'
|
scannerMode: 'CLI'
|
||||||
configMode: 'manual'
|
configMode: 'manual'
|
||||||
cliProjectKey: 'readarrui'
|
cliProjectKey: 'readarrui'
|
||||||
cliProjectName: 'ReadarrUI'
|
cliProjectName: 'ReadarrUI'
|
||||||
cliProjectVersion: '$(readarrVersion)'
|
cliProjectVersion: '$(readarrVersion)'
|
||||||
cliSources: './frontend'
|
cliSources: './frontend'
|
||||||
- task: SonarCloudAnalyze@3
|
- task: SonarCloudAnalyze@2
|
||||||
|
|
||||||
- job: Api_Docs
|
- job: Api_Docs
|
||||||
displayName: API Docs
|
displayName: API Docs
|
||||||
@@ -1190,12 +1190,12 @@ stages:
|
|||||||
submodules: true
|
submodules: true
|
||||||
- powershell: Set-Service SCardSvr -StartupType Manual
|
- powershell: Set-Service SCardSvr -StartupType Manual
|
||||||
displayName: Enable Windows Test Service
|
displayName: Enable Windows Test Service
|
||||||
- task: SonarCloudPrepare@3
|
- task: SonarCloudPrepare@2
|
||||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||||
inputs:
|
inputs:
|
||||||
SonarCloud: 'SonarCloud'
|
SonarCloud: 'SonarCloud'
|
||||||
organization: 'readarr'
|
organization: 'readarr'
|
||||||
scannerMode: 'dotnet'
|
scannerMode: 'MSBuild'
|
||||||
projectKey: 'Readarr_Readarr'
|
projectKey: 'Readarr_Readarr'
|
||||||
projectName: 'Readarr'
|
projectName: 'Readarr'
|
||||||
projectVersion: '$(readarrVersion)'
|
projectVersion: '$(readarrVersion)'
|
||||||
@@ -1208,7 +1208,7 @@ stages:
|
|||||||
./build.sh --backend -f net6.0 -r win-x64
|
./build.sh --backend -f net6.0 -r win-x64
|
||||||
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
|
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
|
||||||
displayName: Coverage Unit Tests
|
displayName: Coverage Unit Tests
|
||||||
- task: SonarCloudAnalyze@3
|
- task: SonarCloudAnalyze@2
|
||||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||||
displayName: Publish SonarCloud Results
|
displayName: Publish SonarCloud Results
|
||||||
- task: reportgenerator@5.3.11
|
- task: reportgenerator@5.3.11
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ module.exports = (env) => {
|
|||||||
loose: true,
|
loose: true,
|
||||||
debug: false,
|
debug: false,
|
||||||
useBuiltIns: 'entry',
|
useBuiltIns: 'entry',
|
||||||
corejs: '3.39'
|
corejs: 3
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -165,8 +165,7 @@ function HistoryDetails(props) {
|
|||||||
|
|
||||||
if (eventType === 'downloadFailed') {
|
if (eventType === 'downloadFailed') {
|
||||||
const {
|
const {
|
||||||
message,
|
message
|
||||||
indexer
|
|
||||||
} = data;
|
} = data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -178,21 +177,11 @@ function HistoryDetails(props) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{
|
{
|
||||||
indexer ?
|
!!message &&
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('Indexer')}
|
|
||||||
data={indexer}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
message ?
|
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
title={translate('Message')}
|
title={translate('Message')}
|
||||||
data={message}
|
data={message}
|
||||||
/> :
|
/>
|
||||||
null
|
|
||||||
}
|
}
|
||||||
</DescriptionList>
|
</DescriptionList>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import ModelBase from 'App/ModelBase';
|
import ModelBase from 'App/ModelBase';
|
||||||
|
|
||||||
export type AuthorStatus = 'continuing' | 'ended';
|
|
||||||
|
|
||||||
interface Author extends ModelBase {
|
interface Author extends ModelBase {
|
||||||
added: string;
|
added: string;
|
||||||
genres: string[];
|
genres: string[];
|
||||||
@@ -12,7 +10,6 @@ interface Author extends ModelBase {
|
|||||||
metadataProfileId: number;
|
metadataProfileId: number;
|
||||||
rootFolderPath: string;
|
rootFolderPath: string;
|
||||||
sortName: string;
|
sortName: string;
|
||||||
status: AuthorStatus;
|
|
||||||
tags: number[];
|
tags: number[];
|
||||||
authorName: string;
|
authorName: string;
|
||||||
isSaving?: boolean;
|
isSaving?: boolean;
|
||||||
|
|||||||
@@ -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 MonitoringOptionsModal from 'Author/MonitoringOptions/MonitoringOptionsModal';
|
||||||
import BookEditorFooter from 'Book/Editor/BookEditorFooter';
|
import BookEditorFooter from 'Book/Editor/BookEditorFooter';
|
||||||
import BookFileEditorTable from 'BookFile/Editor/BookFileEditorTable';
|
import BookFileEditorTable from 'BookFile/Editor/BookFileEditorTable';
|
||||||
import Alert from 'Components/Alert';
|
|
||||||
import IconButton from 'Components/Link/IconButton';
|
import IconButton from 'Components/Link/IconButton';
|
||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
@@ -18,7 +17,7 @@ import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
|||||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||||
import SwipeHeaderConnector from 'Components/Swipe/SwipeHeaderConnector';
|
import SwipeHeaderConnector from 'Components/Swipe/SwipeHeaderConnector';
|
||||||
import { align, icons, kinds } from 'Helpers/Props';
|
import { align, icons } from 'Helpers/Props';
|
||||||
import InteractiveSearchFilterMenuConnector from 'InteractiveSearch/InteractiveSearchFilterMenuConnector';
|
import InteractiveSearchFilterMenuConnector from 'InteractiveSearch/InteractiveSearchFilterMenuConnector';
|
||||||
import InteractiveSearchTable from 'InteractiveSearch/InteractiveSearchTable';
|
import InteractiveSearchTable from 'InteractiveSearch/InteractiveSearchTable';
|
||||||
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
|
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
|
||||||
@@ -413,25 +412,22 @@ class AuthorDetails extends Component {
|
|||||||
|
|
||||||
<div className={styles.contentContainer}>
|
<div className={styles.contentContainer}>
|
||||||
{
|
{
|
||||||
!isPopulated && !booksError && !bookFilesError ?
|
!isPopulated && !booksError && !bookFilesError &&
|
||||||
<LoadingIndicator /> :
|
<LoadingIndicator />
|
||||||
null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!isFetching && booksError ?
|
!isFetching && booksError &&
|
||||||
<Alert kind={kinds.DANGER}>
|
<div>
|
||||||
{translate('LoadingBooksFailed')}
|
{translate('LoadingBooksFailed')}
|
||||||
</Alert> :
|
</div>
|
||||||
null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!isFetching && bookFilesError ?
|
!isFetching && bookFilesError &&
|
||||||
<Alert kind={kinds.DANGER}>
|
<div>
|
||||||
{translate('LoadingBookFilesFailed')}
|
{translate('LoadingBookFilesFailed')}
|
||||||
</Alert> :
|
</div>
|
||||||
null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -54,6 +54,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
|
text-wrap: balance;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
font-size: 50px;
|
font-size: 50px;
|
||||||
line-height: 60px;
|
line-height: 60px;
|
||||||
@@ -127,6 +128,7 @@
|
|||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
text-wrap: balance;
|
||||||
font-size: $intermediateFontSize;
|
font-size: $intermediateFontSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import TextTruncate from 'react-text-truncate';
|
import TextTruncate from 'react-text-truncate';
|
||||||
import AuthorPoster from 'Author/AuthorPoster';
|
import AuthorPoster from 'Author/AuthorPoster';
|
||||||
import { getAuthorStatusDetails } from 'Author/AuthorStatus';
|
|
||||||
import HeartRating from 'Components/HeartRating';
|
import HeartRating from 'Components/HeartRating';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import Label from 'Components/Label';
|
import Label from 'Components/Label';
|
||||||
@@ -12,7 +11,7 @@ import MonitorToggleButton from 'Components/MonitorToggleButton';
|
|||||||
import Popover from 'Components/Tooltip/Popover';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||||
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
|
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
|
||||||
import QualityProfileName from 'Settings/Profiles/Quality/QualityProfileName';
|
import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector';
|
||||||
import fonts from 'Styles/Variables/fonts';
|
import fonts from 'Styles/Variables/fonts';
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
import stripHtml from 'Utilities/String/stripHtml';
|
import stripHtml from 'Utilities/String/stripHtml';
|
||||||
@@ -88,11 +87,11 @@ class AuthorDetailsHeader extends Component {
|
|||||||
titleWidth
|
titleWidth
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const statusDetails = getAuthorStatusDetails(status);
|
|
||||||
|
|
||||||
const fanartUrl = getFanartUrl(images);
|
const fanartUrl = getFanartUrl(images);
|
||||||
const marqueeWidth = titleWidth - (isSmallScreen ? 85 : 160);
|
const marqueeWidth = titleWidth - (isSmallScreen ? 85 : 160);
|
||||||
|
|
||||||
|
const continuing = status === 'continuing';
|
||||||
|
|
||||||
let bookFilesCountMessage = translate('BookFilesCountMessage');
|
let bookFilesCountMessage = translate('BookFilesCountMessage');
|
||||||
|
|
||||||
if (bookFileCount === 1) {
|
if (bookFileCount === 1) {
|
||||||
@@ -214,7 +213,7 @@ class AuthorDetailsHeader extends Component {
|
|||||||
|
|
||||||
<span className={styles.qualityProfileName}>
|
<span className={styles.qualityProfileName}>
|
||||||
{
|
{
|
||||||
<QualityProfileName
|
<QualityProfileNameConnector
|
||||||
qualityProfileId={qualityProfileId}
|
qualityProfileId={qualityProfileId}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -237,16 +236,16 @@ class AuthorDetailsHeader extends Component {
|
|||||||
|
|
||||||
<Label
|
<Label
|
||||||
className={styles.detailsLabel}
|
className={styles.detailsLabel}
|
||||||
title={statusDetails.message}
|
title={continuing ? translate('ContinuingMoreBooksAreExpected') : translate('ContinuingNoAdditionalBooksAreExpected')}
|
||||||
size={sizes.LARGE}
|
size={sizes.LARGE}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
name={statusDetails.icon}
|
name={continuing ? icons.AUTHOR_CONTINUING : icons.AUTHOR_ENDED}
|
||||||
size={17}
|
size={17}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span className={styles.qualityProfileName}>
|
<span className={styles.qualityProfileName}>
|
||||||
{statusDetails.title}
|
{continuing ? 'Continuing' : 'Deceased'}
|
||||||
</span>
|
</span>
|
||||||
</Label>
|
</Label>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { getAuthorStatusDetails } from 'Author/AuthorStatus';
|
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import VirtualTableRowCell from 'Components/Table/Cells/TableRowCell';
|
import VirtualTableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
@@ -16,8 +15,6 @@ function AuthorStatusCell(props) {
|
|||||||
...otherProps
|
...otherProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const statusDetails = getAuthorStatusDetails(status);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Component
|
<Component
|
||||||
className={className}
|
className={className}
|
||||||
@@ -31,8 +28,8 @@ function AuthorStatusCell(props) {
|
|||||||
|
|
||||||
<Icon
|
<Icon
|
||||||
className={styles.statusIcon}
|
className={styles.statusIcon}
|
||||||
name={statusDetails.icon}
|
name={status === 'ended' ? icons.AUTHOR_ENDED : icons.AUTHOR_CONTINUING}
|
||||||
title={`${statusDetails.title}: ${statusDetails.message}`}
|
title={status === 'ended' ? translate('StatusEndedDeceased') : translate('StatusEndedContinuing')}
|
||||||
/>
|
/>
|
||||||
</Component>
|
</Component>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import styles from './NoAuthor.css';
|
import styles from './NoAuthor.css';
|
||||||
|
|
||||||
function NoAuthor(props) {
|
function NoAuthor(props) {
|
||||||
@@ -32,7 +31,7 @@ function NoAuthor(props) {
|
|||||||
to="/settings/mediamanagement"
|
to="/settings/mediamanagement"
|
||||||
kind={kinds.PRIMARY}
|
kind={kinds.PRIMARY}
|
||||||
>
|
>
|
||||||
{translate('AddRootFolder')}
|
Add Root Folder
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -41,7 +40,7 @@ function NoAuthor(props) {
|
|||||||
to="/add/search"
|
to="/add/search"
|
||||||
kind={kinds.PRIMARY}
|
kind={kinds.PRIMARY}
|
||||||
>
|
>
|
||||||
{translate('AddNewAuthor')}
|
Add New Author
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import AuthorNameLink from 'Author/AuthorNameLink';
|
import AuthorNameLink from 'Author/AuthorNameLink';
|
||||||
import { getAuthorStatusDetails } from 'Author/AuthorStatus';
|
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||||
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
import BookshelfBook from './BookshelfBook';
|
import BookshelfBook from './BookshelfBook';
|
||||||
import styles from './BookshelfRow.css';
|
import styles from './BookshelfRow.css';
|
||||||
|
|
||||||
@@ -29,8 +30,6 @@ class BookshelfRow extends Component {
|
|||||||
onBookMonitoredPress
|
onBookMonitoredPress
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const statusDetails = getAuthorStatusDetails(status);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<VirtualTableSelectCell
|
<VirtualTableSelectCell
|
||||||
@@ -53,8 +52,8 @@ class BookshelfRow extends Component {
|
|||||||
<VirtualTableRowCell className={styles.status}>
|
<VirtualTableRowCell className={styles.status}>
|
||||||
<Icon
|
<Icon
|
||||||
className={styles.statusIcon}
|
className={styles.statusIcon}
|
||||||
name={statusDetails.icon}
|
name={status === 'ended' ? icons.AUTHOR_ENDED : icons.AUTHOR_CONTINUING}
|
||||||
title={statusDetails.title}
|
title={status === 'ended' ? translate('StatusEndedEnded') : translate('StatusEndedContinuing')}
|
||||||
/>
|
/>
|
||||||
</VirtualTableRowCell>
|
</VirtualTableRowCell>
|
||||||
|
|
||||||
|
|||||||
@@ -20,8 +20,6 @@ import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue';
|
|||||||
import TextInput from './TextInput';
|
import TextInput from './TextInput';
|
||||||
import styles from './EnhancedSelectInput.css';
|
import styles from './EnhancedSelectInput.css';
|
||||||
|
|
||||||
const MINIMUM_DISTANCE_FROM_EDGE = 10;
|
|
||||||
|
|
||||||
function isArrowKey(keyCode) {
|
function isArrowKey(keyCode) {
|
||||||
return keyCode === keyCodes.UP_ARROW || keyCode === keyCodes.DOWN_ARROW;
|
return keyCode === keyCodes.UP_ARROW || keyCode === keyCodes.DOWN_ARROW;
|
||||||
}
|
}
|
||||||
@@ -139,9 +137,18 @@ class EnhancedSelectInput extends Component {
|
|||||||
// Listeners
|
// Listeners
|
||||||
|
|
||||||
onComputeMaxHeight = (data) => {
|
onComputeMaxHeight = (data) => {
|
||||||
|
const {
|
||||||
|
top,
|
||||||
|
bottom
|
||||||
|
} = data.offsets.reference;
|
||||||
|
|
||||||
const windowHeight = window.innerHeight;
|
const windowHeight = window.innerHeight;
|
||||||
|
|
||||||
data.styles.maxHeight = windowHeight - MINIMUM_DISTANCE_FROM_EDGE;
|
if ((/^botton/).test(data.placement)) {
|
||||||
|
data.styles.maxHeight = windowHeight - bottom;
|
||||||
|
} else {
|
||||||
|
data.styles.maxHeight = top;
|
||||||
|
}
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
@@ -450,10 +457,6 @@ class EnhancedSelectInput extends Component {
|
|||||||
order: 851,
|
order: 851,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
fn: this.onComputeMaxHeight
|
fn: this.onComputeMaxHeight
|
||||||
},
|
|
||||||
preventOverflow: {
|
|
||||||
enabled: true,
|
|
||||||
boundariesElement: 'viewport'
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ function createMapStateToProps() {
|
|||||||
if (includeNoChange) {
|
if (includeNoChange) {
|
||||||
values.unshift({
|
values.unshift({
|
||||||
key: 'noChange',
|
key: 'noChange',
|
||||||
value: translate('NoChange'),
|
value: '',
|
||||||
|
name: translate('NoChange'),
|
||||||
isDisabled: includeNoChangeDisabled,
|
isDisabled: includeNoChangeDisabled,
|
||||||
isMissing: false
|
isMissing: false
|
||||||
});
|
});
|
||||||
@@ -38,6 +39,7 @@ function createMapStateToProps() {
|
|||||||
values.push({
|
values.push({
|
||||||
key: '',
|
key: '',
|
||||||
value: '',
|
value: '',
|
||||||
|
name: '',
|
||||||
isDisabled: true,
|
isDisabled: true,
|
||||||
isHidden: true
|
isHidden: true
|
||||||
});
|
});
|
||||||
@@ -54,7 +56,8 @@ function createMapStateToProps() {
|
|||||||
|
|
||||||
values.push({
|
values.push({
|
||||||
key: ADD_NEW_KEY,
|
key: ADD_NEW_KEY,
|
||||||
value: 'Add a new path'
|
value: '',
|
||||||
|
name: 'Add a new path'
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -102,27 +105,6 @@ class RootFolderSelectInputConnector extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
values,
|
|
||||||
onChange
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (prevProps.values === values) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!value && values.length && values.some((v) => !!v.key && v.key !== ADD_NEW_KEY)) {
|
|
||||||
const defaultValue = values[0];
|
|
||||||
|
|
||||||
if (defaultValue.key !== ADD_NEW_KEY) {
|
|
||||||
onChange({ name, value: defaultValue.key });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Render
|
// Render
|
||||||
|
|
||||||
|
|||||||
@@ -13,15 +13,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.value {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.authorFolder {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
color: var(--disabledColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
.freeSpace {
|
.freeSpace {
|
||||||
margin-left: 15px;
|
margin-left: 15px;
|
||||||
color: var(--darkGray);
|
color: var(--darkGray);
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
// This file is automatically generated.
|
// This file is automatically generated.
|
||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'authorFolder': string;
|
|
||||||
'freeSpace': string;
|
'freeSpace': string;
|
||||||
'isMissing': string;
|
'isMissing': string;
|
||||||
'isMobile': string;
|
'isMobile': string;
|
||||||
'optionText': string;
|
'optionText': string;
|
||||||
'value': string;
|
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
|
|||||||
@@ -7,24 +7,18 @@ import styles from './RootFolderSelectInputOption.css';
|
|||||||
|
|
||||||
function RootFolderSelectInputOption(props) {
|
function RootFolderSelectInputOption(props) {
|
||||||
const {
|
const {
|
||||||
id,
|
|
||||||
value,
|
value,
|
||||||
name,
|
name,
|
||||||
freeSpace,
|
freeSpace,
|
||||||
authorFolder,
|
|
||||||
isMissing,
|
isMissing,
|
||||||
isMobile,
|
isMobile,
|
||||||
isWindows,
|
|
||||||
...otherProps
|
...otherProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const slashCharacter = isWindows ? '\\' : '/';
|
const text = value === '' ? name : `${name} [${value}]`;
|
||||||
|
|
||||||
const text = name === '' ? value : `[${name}] ${value}`;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EnhancedSelectInputOption
|
<EnhancedSelectInputOption
|
||||||
id={id}
|
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
>
|
>
|
||||||
@@ -33,18 +27,7 @@ function RootFolderSelectInputOption(props) {
|
|||||||
isMobile && styles.isMobile
|
isMobile && styles.isMobile
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className={styles.value}>
|
<div>{text}</div>
|
||||||
{text}
|
|
||||||
|
|
||||||
{
|
|
||||||
authorFolder && id !== 'addNew' ?
|
|
||||||
<div className={styles.authorFolder}>
|
|
||||||
{slashCharacter}
|
|
||||||
{authorFolder}
|
|
||||||
</div> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
{
|
||||||
freeSpace == null ?
|
freeSpace == null ?
|
||||||
@@ -67,18 +50,11 @@ function RootFolderSelectInputOption(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
RootFolderSelectInputOption.propTypes = {
|
RootFolderSelectInputOption.propTypes = {
|
||||||
id: PropTypes.string.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
value: PropTypes.string.isRequired,
|
value: PropTypes.string.isRequired,
|
||||||
freeSpace: PropTypes.number,
|
freeSpace: PropTypes.number,
|
||||||
authorFolder: PropTypes.string,
|
|
||||||
isMissing: PropTypes.bool,
|
isMissing: PropTypes.bool,
|
||||||
isMobile: PropTypes.bool.isRequired,
|
isMobile: PropTypes.bool.isRequired
|
||||||
isWindows: PropTypes.bool
|
|
||||||
};
|
|
||||||
|
|
||||||
RootFolderSelectInputOption.defaultProps = {
|
|
||||||
name: ''
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RootFolderSelectInputOption;
|
export default RootFolderSelectInputOption;
|
||||||
|
|||||||
@@ -7,20 +7,10 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pathContainer {
|
|
||||||
@add-mixin truncate;
|
|
||||||
display: flex;
|
|
||||||
flex: 1 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.path {
|
.path {
|
||||||
flex: 0 1 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.authorFolder {
|
|
||||||
@add-mixin truncate;
|
@add-mixin truncate;
|
||||||
flex: 0 1 auto;
|
|
||||||
color: var(--disabledColor);
|
flex: 1 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.freeSpace {
|
.freeSpace {
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
// This file is automatically generated.
|
// This file is automatically generated.
|
||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'authorFolder': string;
|
|
||||||
'freeSpace': string;
|
'freeSpace': string;
|
||||||
'path': string;
|
'path': string;
|
||||||
'pathContainer': string;
|
|
||||||
'selectedValue': string;
|
'selectedValue': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
|
|||||||
@@ -9,34 +9,19 @@ function RootFolderSelectInputSelectedValue(props) {
|
|||||||
name,
|
name,
|
||||||
value,
|
value,
|
||||||
freeSpace,
|
freeSpace,
|
||||||
authorFolder,
|
|
||||||
includeFreeSpace,
|
includeFreeSpace,
|
||||||
isWindows,
|
|
||||||
...otherProps
|
...otherProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const slashCharacter = isWindows ? '\\' : '/';
|
const text = value === '' ? name : `${name} [${value}]`;
|
||||||
|
|
||||||
const text = name === '' ? value : `[${name}] ${value}`;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EnhancedSelectInputSelectedValue
|
<EnhancedSelectInputSelectedValue
|
||||||
className={styles.selectedValue}
|
className={styles.selectedValue}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
>
|
>
|
||||||
<div className={styles.pathContainer}>
|
<div className={styles.path}>
|
||||||
<div className={styles.path}>
|
{text}
|
||||||
{text}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
authorFolder ?
|
|
||||||
<div className={styles.authorFolder}>
|
|
||||||
{slashCharacter}
|
|
||||||
{authorFolder}
|
|
||||||
</div> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -53,13 +38,10 @@ RootFolderSelectInputSelectedValue.propTypes = {
|
|||||||
name: PropTypes.string,
|
name: PropTypes.string,
|
||||||
value: PropTypes.string,
|
value: PropTypes.string,
|
||||||
freeSpace: PropTypes.number,
|
freeSpace: PropTypes.number,
|
||||||
authorFolder: PropTypes.string,
|
|
||||||
isWindows: PropTypes.bool,
|
|
||||||
includeFreeSpace: PropTypes.bool.isRequired
|
includeFreeSpace: PropTypes.bool.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
RootFolderSelectInputSelectedValue.defaultProps = {
|
RootFolderSelectInputSelectedValue.defaultProps = {
|
||||||
name: '',
|
|
||||||
includeFreeSpace: true
|
includeFreeSpace: true
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ class SelectInput extends Component {
|
|||||||
const {
|
const {
|
||||||
key,
|
key,
|
||||||
value: optionValue,
|
value: optionValue,
|
||||||
isDisabled: optionIsDisabled = false,
|
|
||||||
...otherOptionProps
|
...otherOptionProps
|
||||||
} = option;
|
} = option;
|
||||||
|
|
||||||
@@ -60,7 +59,6 @@ class SelectInput extends Component {
|
|||||||
<option
|
<option
|
||||||
key={key}
|
key={key}
|
||||||
value={key}
|
value={key}
|
||||||
disabled={optionIsDisabled}
|
|
||||||
{...otherOptionProps}
|
{...otherOptionProps}
|
||||||
>
|
>
|
||||||
{typeof optionValue === 'function' ? optionValue() : optionValue}
|
{typeof optionValue === 'function' ? optionValue() : optionValue}
|
||||||
|
|||||||
@@ -83,6 +83,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointMedium) {
|
@media only screen and (max-width: $breakpointMedium) {
|
||||||
|
.modal.small,
|
||||||
|
.modal.medium {
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: $breakpointSmall) {
|
||||||
.modalContainer {
|
.modalContainer {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
line-height: 1.52857143;
|
line-height: 1.52857143;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointMedium) {
|
@media only screen and (max-width: $breakpointSmall) {
|
||||||
.cell {
|
.cell {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointMedium) {
|
@media only screen and (max-width: $breakpointSmall) {
|
||||||
.cell {
|
.cell {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointMedium) {
|
@media only screen and (max-width: $breakpointSmall) {
|
||||||
.tableContainer {
|
.tableContainer {
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointMedium) {
|
@media only screen and (max-width: $breakpointSmall) {
|
||||||
.headerCell {
|
.headerCell {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@
|
|||||||
height: 25px;
|
height: 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointMedium) {
|
@media only screen and (max-width: $breakpointSmall) {
|
||||||
.pager {
|
.pager {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointMedium) {
|
@media only screen and (max-width: $breakpointSmall) {
|
||||||
.headerCell {
|
.headerCell {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,12 +35,11 @@
|
|||||||
.message {
|
.message {
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-weight: 300;
|
|
||||||
font-size: $largeFontSize;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.helpText {
|
.helpText {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
font-weight: 300;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -82,8 +82,7 @@ class AddNewItem extends Component {
|
|||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
error,
|
error,
|
||||||
items,
|
items
|
||||||
hasExistingAuthors
|
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const term = this.state.term;
|
const term = this.state.term;
|
||||||
@@ -187,8 +186,7 @@ class AddNewItem extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
term ?
|
!term &&
|
||||||
null :
|
|
||||||
<div className={styles.message}>
|
<div className={styles.message}>
|
||||||
<div className={styles.helpText}>
|
<div className={styles.helpText}>
|
||||||
{translate('ItsEasyToAddANewAuthorOrBookJustStartTypingTheNameOfTheItemYouWantToAdd')}
|
{translate('ItsEasyToAddANewAuthorOrBookJustStartTypingTheNameOfTheItemYouWantToAdd')}
|
||||||
@@ -201,24 +199,6 @@ class AddNewItem extends Component {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
|
||||||
!term && !hasExistingAuthors ?
|
|
||||||
<div className={styles.message}>
|
|
||||||
<div className={styles.noAuthorsText}>
|
|
||||||
You haven't added any authors yet, do you want to add an existing library location (Root Folder) and update?
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
to="/settings/mediamanagement"
|
|
||||||
kind={kinds.PRIMARY}
|
|
||||||
>
|
|
||||||
{translate('AddRootFolder')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
<div />
|
<div />
|
||||||
</PageContentBody>
|
</PageContentBody>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
@@ -233,7 +213,6 @@ AddNewItem.propTypes = {
|
|||||||
isAdding: PropTypes.bool.isRequired,
|
isAdding: PropTypes.bool.isRequired,
|
||||||
addError: PropTypes.object,
|
addError: PropTypes.object,
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
hasExistingAuthors: PropTypes.bool.isRequired,
|
|
||||||
onSearchChange: PropTypes.func.isRequired,
|
onSearchChange: PropTypes.func.isRequired,
|
||||||
onClearSearch: PropTypes.func.isRequired
|
onClearSearch: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,15 +10,13 @@ import AddNewItem from './AddNewItem';
|
|||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state) => state.search,
|
(state) => state.search,
|
||||||
(state) => state.authors.items.length,
|
|
||||||
(state) => state.router.location,
|
(state) => state.router.location,
|
||||||
(search, existingAuthorsCount, location) => {
|
(search, location) => {
|
||||||
const { params } = parseUrl(location.search);
|
const { params } = parseUrl(location.search);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...search,
|
|
||||||
term: params.term,
|
term: params.term,
|
||||||
hasExistingAuthors: existingAuthorsCount > 0
|
...search
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import ModalContent from 'Components/Modal/ModalContent';
|
|||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import AddAuthorOptionsForm from '../Common/AddAuthorOptionsForm.js';
|
import AddAuthorOptionsForm from '../Common/AddAuthorOptionsForm.js';
|
||||||
import styles from './AddNewAuthorModalContent.css';
|
import styles from './AddNewAuthorModalContent.css';
|
||||||
|
|
||||||
@@ -55,7 +54,7 @@ class AddNewAuthorModalContent extends Component {
|
|||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
{translate('AddNewAuthor')}
|
Add new Author
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
@@ -134,7 +133,7 @@ class AddNewAuthorModalContent extends Component {
|
|||||||
|
|
||||||
AddNewAuthorModalContent.propTypes = {
|
AddNewAuthorModalContent.propTypes = {
|
||||||
authorName: PropTypes.string.isRequired,
|
authorName: PropTypes.string.isRequired,
|
||||||
disambiguation: PropTypes.string,
|
disambiguation: PropTypes.string.isRequired,
|
||||||
overview: PropTypes.string,
|
overview: PropTypes.string,
|
||||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
isAdding: PropTypes.bool.isRequired,
|
isAdding: PropTypes.bool.isRequired,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { connect } from 'react-redux';
|
|||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { addAuthor, setAuthorAddDefault } from 'Store/Actions/searchActions';
|
import { addAuthor, setAuthorAddDefault } from 'Store/Actions/searchActions';
|
||||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
|
||||||
import selectSettings from 'Store/Selectors/selectSettings';
|
import selectSettings from 'Store/Selectors/selectSettings';
|
||||||
import AddNewAuthorModalContent from './AddNewAuthorModalContent';
|
import AddNewAuthorModalContent from './AddNewAuthorModalContent';
|
||||||
|
|
||||||
@@ -13,8 +12,7 @@ function createMapStateToProps() {
|
|||||||
(state) => state.search,
|
(state) => state.search,
|
||||||
(state) => state.settings.metadataProfiles,
|
(state) => state.settings.metadataProfiles,
|
||||||
createDimensionsSelector(),
|
createDimensionsSelector(),
|
||||||
createSystemStatusSelector(),
|
(searchState, metadataProfiles, dimensions) => {
|
||||||
(searchState, metadataProfiles, dimensions, systemStatus) => {
|
|
||||||
const {
|
const {
|
||||||
isAdding,
|
isAdding,
|
||||||
addError,
|
addError,
|
||||||
@@ -34,7 +32,6 @@ function createMapStateToProps() {
|
|||||||
isSmallScreen: dimensions.isSmallScreen,
|
isSmallScreen: dimensions.isSmallScreen,
|
||||||
validationErrors,
|
validationErrors,
|
||||||
validationWarnings,
|
validationWarnings,
|
||||||
isWindows: systemStatus.isWindows,
|
|
||||||
...settings
|
...settings
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,7 +78,6 @@ class AddNewAuthorSearchResult extends Component {
|
|||||||
status,
|
status,
|
||||||
overview,
|
overview,
|
||||||
ratings,
|
ratings,
|
||||||
folder,
|
|
||||||
images,
|
images,
|
||||||
isExistingAuthor,
|
isExistingAuthor,
|
||||||
isSmallScreen
|
isSmallScreen
|
||||||
@@ -206,7 +205,6 @@ class AddNewAuthorSearchResult extends Component {
|
|||||||
disambiguation={disambiguation}
|
disambiguation={disambiguation}
|
||||||
year={year}
|
year={year}
|
||||||
overview={overview}
|
overview={overview}
|
||||||
folder={folder}
|
|
||||||
images={images}
|
images={images}
|
||||||
onModalClose={this.onAddAuthorModalClose}
|
onModalClose={this.onAddAuthorModalClose}
|
||||||
/>
|
/>
|
||||||
@@ -224,7 +222,6 @@ AddNewAuthorSearchResult.propTypes = {
|
|||||||
status: PropTypes.string.isRequired,
|
status: PropTypes.string.isRequired,
|
||||||
overview: PropTypes.string,
|
overview: PropTypes.string,
|
||||||
ratings: PropTypes.object.isRequired,
|
ratings: PropTypes.object.isRequired,
|
||||||
folder: PropTypes.string.isRequired,
|
|
||||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
isExistingAuthor: PropTypes.bool.isRequired,
|
isExistingAuthor: PropTypes.bool.isRequired,
|
||||||
isSmallScreen: PropTypes.bool.isRequired
|
isSmallScreen: PropTypes.bool.isRequired
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import ModalFooter from 'Components/Modal/ModalFooter';
|
|||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
import stripHtml from 'Utilities/String/stripHtml';
|
import stripHtml from 'Utilities/String/stripHtml';
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import AddAuthorOptionsForm from '../Common/AddAuthorOptionsForm.js';
|
import AddAuthorOptionsForm from '../Common/AddAuthorOptionsForm.js';
|
||||||
import styles from './AddNewBookModalContent.css';
|
import styles from './AddNewBookModalContent.css';
|
||||||
|
|
||||||
@@ -59,7 +58,7 @@ class AddNewBookModalContent extends Component {
|
|||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
{translate('AddNewBook')}
|
Add new Book
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { connect } from 'react-redux';
|
|||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { addBook, setBookAddDefault } from 'Store/Actions/searchActions';
|
import { addBook, setBookAddDefault } from 'Store/Actions/searchActions';
|
||||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
|
||||||
import selectSettings from 'Store/Selectors/selectSettings';
|
import selectSettings from 'Store/Selectors/selectSettings';
|
||||||
import AddNewBookModalContent from './AddNewBookModalContent';
|
import AddNewBookModalContent from './AddNewBookModalContent';
|
||||||
|
|
||||||
@@ -14,8 +13,7 @@ function createMapStateToProps() {
|
|||||||
(state) => state.search,
|
(state) => state.search,
|
||||||
(state) => state.settings.metadataProfiles,
|
(state) => state.settings.metadataProfiles,
|
||||||
createDimensionsSelector(),
|
createDimensionsSelector(),
|
||||||
createSystemStatusSelector(),
|
(isExistingAuthor, searchState, metadataProfiles, dimensions) => {
|
||||||
(isExistingAuthor, searchState, metadataProfiles, dimensions, systemStatus) => {
|
|
||||||
const {
|
const {
|
||||||
isAdding,
|
isAdding,
|
||||||
addError,
|
addError,
|
||||||
@@ -35,7 +33,6 @@ function createMapStateToProps() {
|
|||||||
isSmallScreen: dimensions.isSmallScreen,
|
isSmallScreen: dimensions.isSmallScreen,
|
||||||
validationErrors,
|
validationErrors,
|
||||||
validationWarnings,
|
validationWarnings,
|
||||||
isWindows: systemStatus.isWindows,
|
|
||||||
...settings
|
...settings
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -203,7 +203,6 @@ class AddNewBookSearchResult extends Component {
|
|||||||
disambiguation={disambiguation}
|
disambiguation={disambiguation}
|
||||||
authorName={author.authorName}
|
authorName={author.authorName}
|
||||||
overview={overview}
|
overview={overview}
|
||||||
folder={author.folder}
|
|
||||||
images={images}
|
images={images}
|
||||||
onModalClose={this.onAddBookModalClose}
|
onModalClose={this.onAddBookModalClose}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -39,9 +39,7 @@ class AddAuthorOptionsForm extends Component {
|
|||||||
includeNoneMetadataProfile,
|
includeNoneMetadataProfile,
|
||||||
includeSpecificBookMonitor,
|
includeSpecificBookMonitor,
|
||||||
showMetadataProfile,
|
showMetadataProfile,
|
||||||
folder,
|
|
||||||
tags,
|
tags,
|
||||||
isWindows,
|
|
||||||
onInputChange,
|
onInputChange,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
@@ -56,15 +54,6 @@ class AddAuthorOptionsForm extends Component {
|
|||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.ROOT_FOLDER_SELECT}
|
type={inputTypes.ROOT_FOLDER_SELECT}
|
||||||
name="rootFolderPath"
|
name="rootFolderPath"
|
||||||
valueOptions={{
|
|
||||||
authorFolder: folder,
|
|
||||||
isWindows
|
|
||||||
}}
|
|
||||||
selectedValueOptions={{
|
|
||||||
authorFolder: folder,
|
|
||||||
isWindows
|
|
||||||
}}
|
|
||||||
helpText={translate('AddNewAuthorRootFolderHelpText', { folder })}
|
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...rootFolderPath}
|
{...rootFolderPath}
|
||||||
/>
|
/>
|
||||||
@@ -190,14 +179,8 @@ AddAuthorOptionsForm.propTypes = {
|
|||||||
showMetadataProfile: PropTypes.bool.isRequired,
|
showMetadataProfile: PropTypes.bool.isRequired,
|
||||||
includeNoneMetadataProfile: PropTypes.bool.isRequired,
|
includeNoneMetadataProfile: PropTypes.bool.isRequired,
|
||||||
includeSpecificBookMonitor: PropTypes.bool.isRequired,
|
includeSpecificBookMonitor: PropTypes.bool.isRequired,
|
||||||
folder: PropTypes.string.isRequired,
|
|
||||||
tags: PropTypes.object.isRequired,
|
tags: PropTypes.object.isRequired,
|
||||||
isWindows: PropTypes.bool.isRequired,
|
|
||||||
onInputChange: PropTypes.func.isRequired
|
onInputChange: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
AddAuthorOptionsForm.defaultProps = {
|
|
||||||
includeSpecificBookMonitor: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AddAuthorOptionsForm;
|
export default AddAuthorOptionsForm;
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ import ModalBody from 'Components/Modal/ModalBody';
|
|||||||
import ModalContent from 'Components/Modal/ModalContent';
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import Column from 'Components/Table/Column';
|
|
||||||
import Table from 'Components/Table/Table';
|
import Table from 'Components/Table/Table';
|
||||||
import TableBody from 'Components/Table/TableBody';
|
import TableBody from 'Components/Table/TableBody';
|
||||||
import useSelectState from 'Helpers/Hooks/useSelectState';
|
import useSelectState from 'Helpers/Hooks/useSelectState';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import SortDirection from 'Helpers/Props/SortDirection';
|
||||||
import {
|
import {
|
||||||
bulkDeleteDownloadClients,
|
bulkDeleteDownloadClients,
|
||||||
bulkEditDownloadClients,
|
bulkEditDownloadClients,
|
||||||
@@ -35,7 +35,7 @@ type OnSelectedChangeCallback = React.ComponentProps<
|
|||||||
typeof ManageDownloadClientsModalRow
|
typeof ManageDownloadClientsModalRow
|
||||||
>['onSelectedChange'];
|
>['onSelectedChange'];
|
||||||
|
|
||||||
const COLUMNS: Column[] = [
|
const COLUMNS = [
|
||||||
{
|
{
|
||||||
name: 'name',
|
name: 'name',
|
||||||
label: () => translate('Name'),
|
label: () => translate('Name'),
|
||||||
@@ -82,6 +82,8 @@ const COLUMNS: Column[] = [
|
|||||||
|
|
||||||
interface ManageDownloadClientsModalContentProps {
|
interface ManageDownloadClientsModalContentProps {
|
||||||
onModalClose(): void;
|
onModalClose(): void;
|
||||||
|
sortKey?: string;
|
||||||
|
sortDirection?: SortDirection;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ManageDownloadClientsModalContent(
|
function ManageDownloadClientsModalContent(
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ function EditImportListExclusionModalContent(props) {
|
|||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{translate('ForeignId')}
|
{translate('MusicbrainzId')}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ import ModalBody from 'Components/Modal/ModalBody';
|
|||||||
import ModalContent from 'Components/Modal/ModalContent';
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import Column from 'Components/Table/Column';
|
|
||||||
import Table from 'Components/Table/Table';
|
import Table from 'Components/Table/Table';
|
||||||
import TableBody from 'Components/Table/TableBody';
|
import TableBody from 'Components/Table/TableBody';
|
||||||
import useSelectState from 'Helpers/Hooks/useSelectState';
|
import useSelectState from 'Helpers/Hooks/useSelectState';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import SortDirection from 'Helpers/Props/SortDirection';
|
||||||
import {
|
import {
|
||||||
bulkDeleteIndexers,
|
bulkDeleteIndexers,
|
||||||
bulkEditIndexers,
|
bulkEditIndexers,
|
||||||
@@ -35,7 +35,7 @@ type OnSelectedChangeCallback = React.ComponentProps<
|
|||||||
typeof ManageIndexersModalRow
|
typeof ManageIndexersModalRow
|
||||||
>['onSelectedChange'];
|
>['onSelectedChange'];
|
||||||
|
|
||||||
const COLUMNS: Column[] = [
|
const COLUMNS = [
|
||||||
{
|
{
|
||||||
name: 'name',
|
name: 'name',
|
||||||
label: () => translate('Name'),
|
label: () => translate('Name'),
|
||||||
@@ -82,6 +82,8 @@ const COLUMNS: Column[] = [
|
|||||||
|
|
||||||
interface ManageIndexersModalContentProps {
|
interface ManageIndexersModalContentProps {
|
||||||
onModalClose(): void;
|
onModalClose(): void;
|
||||||
|
sortKey?: string;
|
||||||
|
sortDirection?: SortDirection;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
||||||
|
|||||||
@@ -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,
|
isSortable: true,
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'books.lastSearchTime',
|
|
||||||
label: 'Last Searched',
|
|
||||||
isSortable: true,
|
|
||||||
isVisible: false
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'actions',
|
name: 'actions',
|
||||||
columnLabel: 'Actions',
|
columnLabel: 'Actions',
|
||||||
@@ -114,12 +108,6 @@ export const defaultState = {
|
|||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'books.lastSearchTime',
|
|
||||||
label: 'Last Searched',
|
|
||||||
isSortable: true,
|
|
||||||
isVisible: false
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'actions',
|
name: 'actions',
|
||||||
columnLabel: 'Actions',
|
columnLabel: 'Actions',
|
||||||
|
|||||||
@@ -27,12 +27,6 @@ export default function translate(
|
|||||||
key: string,
|
key: string,
|
||||||
tokens: Record<string, string | number | boolean> = {}
|
tokens: Record<string, string | number | boolean> = {}
|
||||||
) {
|
) {
|
||||||
const { isProduction = true } = window.Readarr;
|
|
||||||
|
|
||||||
if (!isProduction && !(key in translations)) {
|
|
||||||
console.warn(`Missing translation for key: ${key}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const translation = translations[key] || key;
|
const translation = translations[key] || key;
|
||||||
|
|
||||||
tokens.appName = 'Readarr';
|
tokens.appName = 'Readarr';
|
||||||
|
|||||||
@@ -131,15 +131,13 @@ class CutoffUnmetConnector extends Component {
|
|||||||
onSearchSelectedPress = (selected) => {
|
onSearchSelectedPress = (selected) => {
|
||||||
this.props.executeCommand({
|
this.props.executeCommand({
|
||||||
name: commandNames.BOOK_SEARCH,
|
name: commandNames.BOOK_SEARCH,
|
||||||
bookIds: selected,
|
bookIds: selected
|
||||||
commandFinished: this.repopulate
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onSearchAllCutoffUnmetPress = () => {
|
onSearchAllCutoffUnmetPress = () => {
|
||||||
this.props.executeCommand({
|
this.props.executeCommand({
|
||||||
name: commandNames.CUTOFF_UNMET_BOOK_SEARCH,
|
name: commandNames.CUTOFF_UNMET_BOOK_SEARCH
|
||||||
commandFinished: this.repopulate
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ function CutoffUnmetRow(props) {
|
|||||||
releaseDate,
|
releaseDate,
|
||||||
titleSlug,
|
titleSlug,
|
||||||
title,
|
title,
|
||||||
lastSearchTime,
|
|
||||||
disambiguation,
|
disambiguation,
|
||||||
isSelected,
|
isSelected,
|
||||||
columns,
|
columns,
|
||||||
@@ -69,15 +68,6 @@ function CutoffUnmetRow(props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name === 'books.lastSearchTime') {
|
|
||||||
return (
|
|
||||||
<RelativeDateCellConnector
|
|
||||||
key={name}
|
|
||||||
date={lastSearchTime}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'releaseDate') {
|
if (name === 'releaseDate') {
|
||||||
return (
|
return (
|
||||||
<RelativeDateCellConnector
|
<RelativeDateCellConnector
|
||||||
@@ -115,7 +105,6 @@ CutoffUnmetRow.propTypes = {
|
|||||||
releaseDate: PropTypes.string.isRequired,
|
releaseDate: PropTypes.string.isRequired,
|
||||||
titleSlug: PropTypes.string.isRequired,
|
titleSlug: PropTypes.string.isRequired,
|
||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
lastSearchTime: PropTypes.string,
|
|
||||||
disambiguation: PropTypes.string,
|
disambiguation: PropTypes.string,
|
||||||
isSelected: PropTypes.bool,
|
isSelected: PropTypes.bool,
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
|||||||
@@ -121,15 +121,13 @@ class MissingConnector extends Component {
|
|||||||
onSearchSelectedPress = (selected) => {
|
onSearchSelectedPress = (selected) => {
|
||||||
this.props.executeCommand({
|
this.props.executeCommand({
|
||||||
name: commandNames.BOOK_SEARCH,
|
name: commandNames.BOOK_SEARCH,
|
||||||
bookIds: selected,
|
bookIds: selected
|
||||||
commandFinished: this.repopulate
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onSearchAllMissingPress = () => {
|
onSearchAllMissingPress = () => {
|
||||||
this.props.executeCommand({
|
this.props.executeCommand({
|
||||||
name: commandNames.MISSING_BOOK_SEARCH,
|
name: commandNames.MISSING_BOOK_SEARCH
|
||||||
commandFinished: this.repopulate
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ function MissingRow(props) {
|
|||||||
releaseDate,
|
releaseDate,
|
||||||
titleSlug,
|
titleSlug,
|
||||||
title,
|
title,
|
||||||
lastSearchTime,
|
|
||||||
disambiguation,
|
disambiguation,
|
||||||
isSelected,
|
isSelected,
|
||||||
columns,
|
columns,
|
||||||
@@ -78,15 +77,6 @@ function MissingRow(props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name === 'books.lastSearchTime') {
|
|
||||||
return (
|
|
||||||
<RelativeDateCellConnector
|
|
||||||
key={name}
|
|
||||||
date={lastSearchTime}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'actions') {
|
if (name === 'actions') {
|
||||||
return (
|
return (
|
||||||
<BookSearchCellConnector
|
<BookSearchCellConnector
|
||||||
@@ -114,7 +104,6 @@ MissingRow.propTypes = {
|
|||||||
releaseDate: PropTypes.string.isRequired,
|
releaseDate: PropTypes.string.isRequired,
|
||||||
titleSlug: PropTypes.string.isRequired,
|
titleSlug: PropTypes.string.isRequired,
|
||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
lastSearchTime: PropTypes.string,
|
|
||||||
disambiguation: PropTypes.string,
|
disambiguation: PropTypes.string,
|
||||||
isSelected: PropTypes.bool,
|
isSelected: PropTypes.bool,
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
|||||||
1
frontend/typings/Globals.d.ts
vendored
1
frontend/typings/Globals.d.ts
vendored
@@ -7,6 +7,5 @@ interface Window {
|
|||||||
theme: string;
|
theme: string;
|
||||||
urlBase: string;
|
urlBase: string;
|
||||||
version: string;
|
version: string;
|
||||||
isProduction: boolean;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
24
package.json
24
package.json
@@ -25,10 +25,10 @@
|
|||||||
"defaults"
|
"defaults"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "6.7.1",
|
"@fortawesome/fontawesome-free": "6.6.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "6.7.1",
|
"@fortawesome/fontawesome-svg-core": "6.6.0",
|
||||||
"@fortawesome/free-regular-svg-icons": "6.7.1",
|
"@fortawesome/free-regular-svg-icons": "6.6.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "6.7.1",
|
"@fortawesome/free-solid-svg-icons": "6.6.0",
|
||||||
"@fortawesome/react-fontawesome": "0.2.2",
|
"@fortawesome/react-fontawesome": "0.2.2",
|
||||||
"@microsoft/signalr": "6.0.25",
|
"@microsoft/signalr": "6.0.25",
|
||||||
"@sentry/browser": "7.119.1",
|
"@sentry/browser": "7.119.1",
|
||||||
@@ -86,13 +86,13 @@
|
|||||||
"typescript": "5.1.6"
|
"typescript": "5.1.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.26.0",
|
"@babel/core": "7.25.8",
|
||||||
"@babel/eslint-parser": "7.25.9",
|
"@babel/eslint-parser": "7.25.8",
|
||||||
"@babel/plugin-proposal-export-default-from": "7.25.9",
|
"@babel/plugin-proposal-export-default-from": "7.25.8",
|
||||||
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
||||||
"@babel/preset-env": "7.26.0",
|
"@babel/preset-env": "7.25.8",
|
||||||
"@babel/preset-react": "7.26.3",
|
"@babel/preset-react": "7.25.7",
|
||||||
"@babel/preset-typescript": "7.26.0",
|
"@babel/preset-typescript": "7.25.7",
|
||||||
"@types/lodash": "4.14.195",
|
"@types/lodash": "4.14.195",
|
||||||
"@types/react-lazyload": "3.2.3",
|
"@types/react-lazyload": "3.2.3",
|
||||||
"@types/redux-actions": "2.6.5",
|
"@types/redux-actions": "2.6.5",
|
||||||
@@ -102,7 +102,7 @@
|
|||||||
"babel-loader": "9.2.1",
|
"babel-loader": "9.2.1",
|
||||||
"babel-plugin-inline-classnames": "2.0.1",
|
"babel-plugin-inline-classnames": "2.0.1",
|
||||||
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
|
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
|
||||||
"core-js": "3.39.0",
|
"core-js": "3.38.1",
|
||||||
"css-loader": "6.8.1",
|
"css-loader": "6.8.1",
|
||||||
"css-modules-typescript-loader": "4.0.1",
|
"css-modules-typescript-loader": "4.0.1",
|
||||||
"eslint": "8.57.1",
|
"eslint": "8.57.1",
|
||||||
@@ -142,7 +142,7 @@
|
|||||||
"worker-loader": "3.0.8"
|
"worker-loader": "3.0.8"
|
||||||
},
|
},
|
||||||
"volta": {
|
"volta": {
|
||||||
"node": "20.11.1",
|
"node": "16.17.0",
|
||||||
"yarn": "1.22.19"
|
"yarn": "1.22.19"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,35 +99,6 @@
|
|||||||
<RootNamespace Condition="'$(ReadarrProject)'=='true'">$(MSBuildProjectName.Replace('Readarr','NzbDrone'))</RootNamespace>
|
<RootNamespace Condition="'$(ReadarrProject)'=='true'">$(MSBuildProjectName.Replace('Readarr','NzbDrone'))</RootNamespace>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup Condition="'$(TestProject)'!='true'">
|
|
||||||
<!-- Annotates .NET assemblies with repository information including SHA -->
|
|
||||||
<!-- Sentry uses this to link directly to GitHub at the exact version/file/line -->
|
|
||||||
<!-- This is built-in on .NET 8 and can be removed once the project is updated -->
|
|
||||||
<PackageReference Include="Microsoft.SourceLink.GitHub" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<!-- Sentry specific configuration: Only in Release mode -->
|
|
||||||
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
|
|
||||||
<!-- https://docs.sentry.io/platforms/dotnet/configuration/msbuild/ -->
|
|
||||||
<!-- OrgSlug, ProjectSlug and AuthToken are required.
|
|
||||||
They can be set below, via argument to 'msbuild -p:' or environment variable -->
|
|
||||||
<SentryOrg></SentryOrg>
|
|
||||||
<SentryProject></SentryProject>
|
|
||||||
<SentryUrl></SentryUrl> <!-- If empty, assumed to be sentry.io -->
|
|
||||||
<SentryAuthToken></SentryAuthToken> <!-- Use env var instead: SENTRY_AUTH_TOKEN -->
|
|
||||||
|
|
||||||
<!-- Upload PDBs to Sentry, enabling stack traces with line numbers and file paths
|
|
||||||
without the need to deploy the application with PDBs -->
|
|
||||||
<SentryUploadSymbols>true</SentryUploadSymbols>
|
|
||||||
|
|
||||||
<!-- Source Link settings -->
|
|
||||||
<!-- https://github.com/dotnet/sourcelink/blob/main/docs/README.md#publishrepositoryurl -->
|
|
||||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
|
||||||
<!-- Embeds all source code in the respective PDB. This can make it a bit bigger but since it'll be uploaded
|
|
||||||
to Sentry and not distributed to run on the server, it helps debug crashes while making releases smaller -->
|
|
||||||
<EmbedAllSources>true</EmbedAllSources>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<!-- Standard testing packages -->
|
<!-- Standard testing packages -->
|
||||||
<ItemGroup Condition="'$(TestProject)'=='true'">
|
<ItemGroup Condition="'$(TestProject)'=='true'">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||||
|
|||||||
@@ -9,8 +9,7 @@
|
|||||||
<PackageVersion Include="DryIoc.Microsoft.DependencyInjection" Version="6.2.0" />
|
<PackageVersion Include="DryIoc.Microsoft.DependencyInjection" Version="6.2.0" />
|
||||||
<PackageVersion Include="Equ" Version="2.3.0" />
|
<PackageVersion Include="Equ" Version="2.3.0" />
|
||||||
<PackageVersion Include="FluentAssertions" Version="5.10.3" />
|
<PackageVersion Include="FluentAssertions" Version="5.10.3" />
|
||||||
<PackageVersion Include="IPAddressRange" Version="6.1.0" />
|
<PackageVersion Include="Polly" Version="8.4.2" />
|
||||||
<PackageVersion Include="Polly" Version="8.5.2" />
|
|
||||||
<PackageVersion Include="Servarr.FluentMigrator.Runner" Version="3.3.2.9" />
|
<PackageVersion Include="Servarr.FluentMigrator.Runner" Version="3.3.2.9" />
|
||||||
<PackageVersion Include="Servarr.FluentMigrator.Runner.SQLite" Version="3.3.2.9" />
|
<PackageVersion Include="Servarr.FluentMigrator.Runner.SQLite" Version="3.3.2.9" />
|
||||||
<PackageVersion Include="Servarr.FluentMigrator.Runner.Postgres" Version="3.3.2.9" />
|
<PackageVersion Include="Servarr.FluentMigrator.Runner.Postgres" Version="3.3.2.9" />
|
||||||
@@ -18,16 +17,14 @@
|
|||||||
<PackageVersion Include="Ical.Net" Version="4.3.1" />
|
<PackageVersion Include="Ical.Net" Version="4.3.1" />
|
||||||
<PackageVersion Include="ImpromptuInterface" Version="7.0.1" />
|
<PackageVersion Include="ImpromptuInterface" Version="7.0.1" />
|
||||||
<PackageVersion Include="LazyCache" Version="2.4.0" />
|
<PackageVersion Include="LazyCache" Version="2.4.0" />
|
||||||
<PackageVersion Include="Mailkit" Version="4.8.0" />
|
<PackageVersion Include="Mailkit" Version="3.6.0" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.35" />
|
<PackageVersion Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.35" />
|
||||||
<PackageVersion Include="Microsoft.Data.SqlClient" Version="2.1.7" />
|
|
||||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="6.0.2" />
|
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="6.0.2" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
|
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
|
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.2" />
|
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.2" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="6.0.0" />
|
<PackageVersion Include="Microsoft.Extensions.Logging" Version="6.0.0" />
|
||||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
|
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
|
||||||
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
|
|
||||||
<PackageVersion Include="Microsoft.Win32.Registry" Version="5.0.0" />
|
<PackageVersion Include="Microsoft.Win32.Registry" Version="5.0.0" />
|
||||||
<PackageVersion Include="Mono.Posix.NETStandard" Version="5.20.1.34-servarr22" />
|
<PackageVersion Include="Mono.Posix.NETStandard" Version="5.20.1.34-servarr22" />
|
||||||
<PackageVersion Include="Moq" Version="4.17.2" />
|
<PackageVersion Include="Moq" Version="4.17.2" />
|
||||||
@@ -37,28 +34,28 @@
|
|||||||
<PackageVersion Include="NLog.Extensions.Logging" Version="5.2.3" />
|
<PackageVersion Include="NLog.Extensions.Logging" Version="5.2.3" />
|
||||||
<PackageVersion Include="NLog" Version="5.1.4" />
|
<PackageVersion Include="NLog" Version="5.1.4" />
|
||||||
<PackageVersion Include="NLog.Targets.Syslog" Version="7.0.0" />
|
<PackageVersion Include="NLog.Targets.Syslog" Version="7.0.0" />
|
||||||
<PackageVersion Include="Npgsql" Version="7.0.10" />
|
<PackageVersion Include="Npgsql" Version="7.0.8" />
|
||||||
<PackageVersion Include="NUnit3TestAdapter" Version="4.2.1" />
|
<PackageVersion Include="NUnit3TestAdapter" Version="4.2.1" />
|
||||||
<PackageVersion Include="NUnit" Version="3.14.0" />
|
<PackageVersion Include="NUnit" Version="3.14.0" />
|
||||||
<PackageVersion Include="NunitXml.TestLogger" Version="3.0.117" />
|
<PackageVersion Include="NunitXml.TestLogger" Version="3.0.117" />
|
||||||
<PackageVersion Include="PdfSharpCore" Version="1.3.65" />
|
<PackageVersion Include="PdfSharpCore" Version="1.3.32" />
|
||||||
<PackageVersion Include="RestSharp.Serializers.SystemTextJson" Version="106.15.0" />
|
<PackageVersion Include="RestSharp.Serializers.SystemTextJson" Version="106.15.0" />
|
||||||
<PackageVersion Include="RestSharp" Version="106.15.0" />
|
<PackageVersion Include="RestSharp" Version="106.15.0" />
|
||||||
<PackageVersion Include="Selenium.Support" Version="3.141.0" />
|
<PackageVersion Include="Selenium.Support" Version="3.141.0" />
|
||||||
<PackageVersion Include="Selenium.WebDriver.ChromeDriver" Version="134.0.6998.16500" />
|
<PackageVersion Include="Selenium.WebDriver.ChromeDriver" Version="91.0.4472.10100" />
|
||||||
<PackageVersion Include="Sentry" Version="4.0.2" />
|
<PackageVersion Include="Sentry" Version="3.31.0" />
|
||||||
<PackageVersion Include="SharpZipLib" Version="1.4.2" />
|
<PackageVersion Include="SharpZipLib" Version="1.4.2" />
|
||||||
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.7" />
|
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.5" />
|
||||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.1.118" />
|
<PackageVersion Include="StyleCop.Analyzers" Version="1.1.118" />
|
||||||
<PackageVersion Include="Swashbuckle.AspNetCore.Annotations" Version="6.6.2" />
|
<PackageVersion Include="Swashbuckle.AspNetCore.Annotations" Version="6.6.2" />
|
||||||
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.6.2" />
|
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.6.2" />
|
||||||
<PackageVersion Include="System.Buffers" Version="4.6.0" />
|
<PackageVersion Include="System.Buffers" Version="4.5.1" />
|
||||||
<PackageVersion Include="System.Configuration.ConfigurationManager" Version="6.0.1" />
|
<PackageVersion Include="System.Configuration.ConfigurationManager" Version="6.0.1" />
|
||||||
<PackageVersion Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
<PackageVersion Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
||||||
<PackageVersion Include="System.IO.Abstractions.TestingHelpers" Version="17.0.24" />
|
<PackageVersion Include="System.IO.Abstractions.TestingHelpers" Version="17.0.24" />
|
||||||
<PackageVersion Include="System.IO.Abstractions" Version="17.0.24" />
|
<PackageVersion Include="System.IO.Abstractions" Version="17.0.24" />
|
||||||
<PackageVersion Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
|
<PackageVersion Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
|
||||||
<PackageVersion Include="System.Memory" Version="4.6.2" />
|
<PackageVersion Include="System.Memory" Version="4.5.5" />
|
||||||
<PackageVersion Include="System.Reflection.TypeExtensions" Version="4.7.0" />
|
<PackageVersion Include="System.Reflection.TypeExtensions" Version="4.7.0" />
|
||||||
<PackageVersion Include="System.Resources.Extensions" Version="6.0.0" />
|
<PackageVersion Include="System.Resources.Extensions" Version="6.0.0" />
|
||||||
<PackageVersion Include="System.Runtime.Loader" Version="4.3.0" />
|
<PackageVersion Include="System.Runtime.Loader" Version="4.3.0" />
|
||||||
@@ -66,7 +63,7 @@
|
|||||||
<PackageVersion Include="System.ServiceProcess.ServiceController" Version="6.0.1" />
|
<PackageVersion Include="System.ServiceProcess.ServiceController" Version="6.0.1" />
|
||||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="6.0.0" />
|
<PackageVersion Include="System.Text.Encoding.CodePages" Version="6.0.0" />
|
||||||
<PackageVersion Include="System.Text.Json" Version="6.0.10" />
|
<PackageVersion Include="System.Text.Json" Version="6.0.10" />
|
||||||
<PackageVersion Include="System.ValueTuple" Version="4.6.1" />
|
<PackageVersion Include="System.ValueTuple" Version="4.5.0" />
|
||||||
<PackageVersion Include="TagLibSharp-Lidarr" Version="2.2.0.19" />
|
<PackageVersion Include="TagLibSharp-Lidarr" Version="2.2.0.19" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -40,16 +40,15 @@ namespace NzbDrone.Automation.Test
|
|||||||
var service = ChromeDriverService.CreateDefaultService();
|
var service = ChromeDriverService.CreateDefaultService();
|
||||||
|
|
||||||
// Timeout as windows automation tests seem to take alot longer to get going
|
// Timeout as windows automation tests seem to take alot longer to get going
|
||||||
driver = new ChromeDriver(service, options, TimeSpan.FromMinutes(3));
|
driver = new ChromeDriver(service, options, new TimeSpan(0, 3, 0));
|
||||||
|
|
||||||
driver.Manage().Window.Size = new System.Drawing.Size(1920, 1080);
|
driver.Manage().Window.Size = new System.Drawing.Size(1920, 1080);
|
||||||
driver.Manage().Window.FullScreen();
|
|
||||||
|
|
||||||
_runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger(), null);
|
_runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger(), null);
|
||||||
_runner.KillAll();
|
_runner.KillAll();
|
||||||
_runner.Start(true);
|
_runner.Start(true);
|
||||||
|
|
||||||
driver.Navigate().GoToUrl("http://localhost:8787");
|
driver.Url = "http://localhost:8787";
|
||||||
|
|
||||||
var page = new PageBase(driver);
|
var page = new PageBase(driver);
|
||||||
page.WaitForNoSpinner();
|
page.WaitForNoSpinner();
|
||||||
@@ -69,7 +68,7 @@ namespace NzbDrone.Automation.Test
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var image = (driver as ITakesScreenshot).GetScreenshot();
|
var image = ((ITakesScreenshot)driver).GetScreenshot();
|
||||||
image.SaveAsFile($"./{name}_test_screenshot.png", ScreenshotImageFormat.Png);
|
image.SaveAsFile($"./{name}_test_screenshot.png", ScreenshotImageFormat.Png);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using OpenQA.Selenium;
|
using OpenQA.Selenium;
|
||||||
|
using OpenQA.Selenium.Remote;
|
||||||
using OpenQA.Selenium.Support.UI;
|
using OpenQA.Selenium.Support.UI;
|
||||||
|
|
||||||
namespace NzbDrone.Automation.Test.PageModel
|
namespace NzbDrone.Automation.Test.PageModel
|
||||||
{
|
{
|
||||||
public class PageBase
|
public class PageBase
|
||||||
{
|
{
|
||||||
private readonly IWebDriver _driver;
|
private readonly RemoteWebDriver _driver;
|
||||||
|
|
||||||
public PageBase(IWebDriver driver)
|
public PageBase(RemoteWebDriver driver)
|
||||||
{
|
{
|
||||||
_driver = driver;
|
_driver = driver;
|
||||||
|
driver.Manage().Window.Maximize();
|
||||||
}
|
}
|
||||||
|
|
||||||
public IWebElement FindByClass(string className, int timeout = 5)
|
public IWebElement FindByClass(string className, int timeout = 5)
|
||||||
|
|||||||
@@ -21,28 +21,9 @@ namespace NzbDrone.Common.Test.ExtensionTests
|
|||||||
[TestCase("1.2.3.4")]
|
[TestCase("1.2.3.4")]
|
||||||
[TestCase("172.55.0.1")]
|
[TestCase("172.55.0.1")]
|
||||||
[TestCase("192.55.0.1")]
|
[TestCase("192.55.0.1")]
|
||||||
[TestCase("100.64.0.1")]
|
|
||||||
[TestCase("100.127.255.254")]
|
|
||||||
public void should_return_false_for_public_ip_address(string ipAddress)
|
public void should_return_false_for_public_ip_address(string ipAddress)
|
||||||
{
|
{
|
||||||
IPAddress.Parse(ipAddress).IsLocalAddress().Should().BeFalse();
|
IPAddress.Parse(ipAddress).IsLocalAddress().Should().BeFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("100.64.0.1")]
|
|
||||||
[TestCase("100.127.255.254")]
|
|
||||||
[TestCase("100.100.100.100")]
|
|
||||||
public void should_return_true_for_cgnat_ip_address(string ipAddress)
|
|
||||||
{
|
|
||||||
IPAddress.Parse(ipAddress).IsCgnatIpAddress().Should().BeTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase("1.2.3.4")]
|
|
||||||
[TestCase("192.168.5.1")]
|
|
||||||
[TestCase("100.63.255.255")]
|
|
||||||
[TestCase("100.128.0.0")]
|
|
||||||
public void should_return_false_for_non_cgnat_ip_address(string ipAddress)
|
|
||||||
{
|
|
||||||
IPAddress.Parse(ipAddress).IsCgnatIpAddress().Should().BeFalse();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ using System.Linq;
|
|||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NzbDrone.Common.EnvironmentInfo;
|
|
||||||
using NzbDrone.Common.Instrumentation.Sentry;
|
using NzbDrone.Common.Instrumentation.Sentry;
|
||||||
using NzbDrone.Test.Common;
|
using NzbDrone.Test.Common;
|
||||||
|
|
||||||
@@ -28,7 +27,7 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
|||||||
[SetUp]
|
[SetUp]
|
||||||
public void Setup()
|
public void Setup()
|
||||||
{
|
{
|
||||||
_subject = new SentryTarget("https://aaaaaaaaaaaaaaaaaaaaaaaaaa@sentry.io/111111", Mocker.GetMock<IAppFolderInfo>().Object);
|
_subject = new SentryTarget("https://aaaaaaaaaaaaaaaaaaaaaaaaaa@sentry.io/111111");
|
||||||
}
|
}
|
||||||
|
|
||||||
private LogEventInfo GivenLogEvent(LogLevel level, Exception ex, string message)
|
private LogEventInfo GivenLogEvent(LogLevel level, Exception ex, string message)
|
||||||
|
|||||||
@@ -42,18 +42,17 @@ namespace NzbDrone.Common
|
|||||||
|
|
||||||
public void CreateZip(string path, IEnumerable<string> files)
|
public void CreateZip(string path, IEnumerable<string> files)
|
||||||
{
|
{
|
||||||
_logger.Debug("Creating archive {0}", path);
|
using (var zipFile = ZipFile.Create(path))
|
||||||
|
|
||||||
using var zipFile = ZipFile.Create(path);
|
|
||||||
|
|
||||||
zipFile.BeginUpdate();
|
|
||||||
|
|
||||||
foreach (var file in files)
|
|
||||||
{
|
{
|
||||||
zipFile.Add(file, Path.GetFileName(file));
|
zipFile.BeginUpdate();
|
||||||
}
|
|
||||||
|
|
||||||
zipFile.CommitUpdate();
|
foreach (var file in files)
|
||||||
|
{
|
||||||
|
zipFile.Add(file, Path.GetFileName(file));
|
||||||
|
}
|
||||||
|
|
||||||
|
zipFile.CommitUpdate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ExtractZip(string compressedFile, string destination)
|
private void ExtractZip(string compressedFile, string destination)
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ using System.Collections.Generic;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Abstractions;
|
using System.IO.Abstractions;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common.EnsureThat;
|
using NzbDrone.Common.EnsureThat;
|
||||||
using NzbDrone.Common.EnvironmentInfo;
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
@@ -307,26 +306,9 @@ namespace NzbDrone.Common.Disk
|
|||||||
{
|
{
|
||||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||||
|
|
||||||
var files = GetFiles(path, recursive).ToList();
|
var files = GetFiles(path, recursive);
|
||||||
|
|
||||||
files.ForEach(RemoveReadOnly);
|
files.ToList().ForEach(RemoveReadOnly);
|
||||||
|
|
||||||
var attempts = 0;
|
|
||||||
|
|
||||||
while (attempts < 3 && files.Any())
|
|
||||||
{
|
|
||||||
EmptyFolder(path);
|
|
||||||
|
|
||||||
if (GetFiles(path, recursive).Any())
|
|
||||||
{
|
|
||||||
// Wait for IO operations to complete after emptying the folder since they aren't always
|
|
||||||
// instantly removed and it can lead to false positives that files are still present.
|
|
||||||
Thread.Sleep(3000);
|
|
||||||
}
|
|
||||||
|
|
||||||
attempts++;
|
|
||||||
files = GetFiles(path, recursive).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
_fileSystem.Directory.Delete(path, recursive);
|
_fileSystem.Directory.Delete(path, recursive);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -342,11 +342,10 @@ namespace NzbDrone.Common.Disk
|
|||||||
|
|
||||||
var isCifs = targetDriveFormat == "cifs";
|
var isCifs = targetDriveFormat == "cifs";
|
||||||
var isBtrfs = sourceDriveFormat == "btrfs" && targetDriveFormat == "btrfs";
|
var isBtrfs = sourceDriveFormat == "btrfs" && targetDriveFormat == "btrfs";
|
||||||
var isZfs = sourceDriveFormat == "zfs" && targetDriveFormat == "zfs";
|
|
||||||
|
|
||||||
if (mode.HasFlag(TransferMode.Copy))
|
if (mode.HasFlag(TransferMode.Copy))
|
||||||
{
|
{
|
||||||
if (isBtrfs || isZfs)
|
if (isBtrfs)
|
||||||
{
|
{
|
||||||
if (_diskProvider.TryCreateRefLink(sourcePath, targetPath))
|
if (_diskProvider.TryCreateRefLink(sourcePath, targetPath))
|
||||||
{
|
{
|
||||||
@@ -360,7 +359,7 @@ namespace NzbDrone.Common.Disk
|
|||||||
|
|
||||||
if (mode.HasFlag(TransferMode.Move))
|
if (mode.HasFlag(TransferMode.Move))
|
||||||
{
|
{
|
||||||
if (isBtrfs || isZfs)
|
if (isBtrfs)
|
||||||
{
|
{
|
||||||
if (isSameMount && _diskProvider.TryRenameFile(sourcePath, targetPath))
|
if (isSameMount && _diskProvider.TryRenameFile(sourcePath, targetPath))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -39,24 +39,18 @@ namespace NzbDrone.Common.Extensions
|
|||||||
private static bool IsLocalIPv4(byte[] ipv4Bytes)
|
private static bool IsLocalIPv4(byte[] ipv4Bytes)
|
||||||
{
|
{
|
||||||
// Link local (no IP assigned by DHCP): 169.254.0.0 to 169.254.255.255 (169.254.0.0/16)
|
// Link local (no IP assigned by DHCP): 169.254.0.0 to 169.254.255.255 (169.254.0.0/16)
|
||||||
var isLinkLocal = ipv4Bytes[0] == 169 && ipv4Bytes[1] == 254;
|
bool IsLinkLocal() => ipv4Bytes[0] == 169 && ipv4Bytes[1] == 254;
|
||||||
|
|
||||||
// Class A private range: 10.0.0.0 – 10.255.255.255 (10.0.0.0/8)
|
// Class A private range: 10.0.0.0 – 10.255.255.255 (10.0.0.0/8)
|
||||||
var isClassA = ipv4Bytes[0] == 10;
|
bool IsClassA() => ipv4Bytes[0] == 10;
|
||||||
|
|
||||||
// Class B private range: 172.16.0.0 – 172.31.255.255 (172.16.0.0/12)
|
// Class B private range: 172.16.0.0 – 172.31.255.255 (172.16.0.0/12)
|
||||||
var isClassB = ipv4Bytes[0] == 172 && ipv4Bytes[1] >= 16 && ipv4Bytes[1] <= 31;
|
bool IsClassB() => ipv4Bytes[0] == 172 && ipv4Bytes[1] >= 16 && ipv4Bytes[1] <= 31;
|
||||||
|
|
||||||
// Class C private range: 192.168.0.0 – 192.168.255.255 (192.168.0.0/16)
|
// Class C private range: 192.168.0.0 – 192.168.255.255 (192.168.0.0/16)
|
||||||
var isClassC = ipv4Bytes[0] == 192 && ipv4Bytes[1] == 168;
|
bool IsClassC() => ipv4Bytes[0] == 192 && ipv4Bytes[1] == 168;
|
||||||
|
|
||||||
return isLinkLocal || isClassA || isClassC || isClassB;
|
return IsLinkLocal() || IsClassA() || IsClassC() || IsClassB();
|
||||||
}
|
|
||||||
|
|
||||||
public static bool IsCgnatIpAddress(this IPAddress ipAddress)
|
|
||||||
{
|
|
||||||
var bytes = ipAddress.GetAddressBytes();
|
|
||||||
return bytes.Length == 4 && bytes[0] == 100 && bytes[1] >= 64 && bytes[1] <= 127;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,15 +108,6 @@ namespace NzbDrone.Common.Extensions
|
|||||||
return Directory.GetParent(cleanPath)?.FullName;
|
return Directory.GetParent(cleanPath)?.FullName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string GetCleanPath(this string path)
|
|
||||||
{
|
|
||||||
var cleanPath = OsInfo.IsWindows
|
|
||||||
? PARENT_PATH_END_SLASH_REGEX.Replace(path, "")
|
|
||||||
: path.TrimEnd(Path.DirectorySeparatorChar);
|
|
||||||
|
|
||||||
return cleanPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool IsParentPath(this string parentPath, string childPath)
|
public static bool IsParentPath(this string parentPath, string childPath)
|
||||||
{
|
{
|
||||||
if (parentPath != "/" && !parentPath.EndsWith(":\\"))
|
if (parentPath != "/" && !parentPath.EndsWith(":\\"))
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
using System;
|
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
|
|
||||||
namespace NzbDrone.Common.Http.Proxy
|
namespace NzbDrone.Common.Http.Proxy
|
||||||
@@ -30,8 +29,7 @@ namespace NzbDrone.Common.Http.Proxy
|
|||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(BypassFilter))
|
if (!string.IsNullOrWhiteSpace(BypassFilter))
|
||||||
{
|
{
|
||||||
var hostlist = BypassFilter.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
var hostlist = BypassFilter.Split(',');
|
||||||
|
|
||||||
for (var i = 0; i < hostlist.Length; i++)
|
for (var i = 0; i < hostlist.Length; i++)
|
||||||
{
|
{
|
||||||
if (hostlist[i].StartsWith("*"))
|
if (hostlist[i].StartsWith("*"))
|
||||||
@@ -43,7 +41,7 @@ namespace NzbDrone.Common.Http.Proxy
|
|||||||
return hostlist;
|
return hostlist;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Array.Empty<string>();
|
return new string[] { };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,27 +4,27 @@ namespace NzbDrone.Common.Instrumentation.Extensions
|
|||||||
{
|
{
|
||||||
public static class LoggerExtensions
|
public static class LoggerExtensions
|
||||||
{
|
{
|
||||||
[MessageTemplateFormatMethod("message")]
|
|
||||||
public static void ProgressInfo(this Logger logger, string message, params object[] args)
|
public static void ProgressInfo(this Logger logger, string message, params object[] args)
|
||||||
{
|
{
|
||||||
LogProgressMessage(logger, LogLevel.Info, message, args);
|
var formattedMessage = string.Format(message, args);
|
||||||
|
LogProgressMessage(logger, LogLevel.Info, formattedMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
[MessageTemplateFormatMethod("message")]
|
|
||||||
public static void ProgressDebug(this Logger logger, string message, params object[] args)
|
public static void ProgressDebug(this Logger logger, string message, params object[] args)
|
||||||
{
|
{
|
||||||
LogProgressMessage(logger, LogLevel.Debug, message, args);
|
var formattedMessage = string.Format(message, args);
|
||||||
|
LogProgressMessage(logger, LogLevel.Debug, formattedMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
[MessageTemplateFormatMethod("message")]
|
|
||||||
public static void ProgressTrace(this Logger logger, string message, params object[] args)
|
public static void ProgressTrace(this Logger logger, string message, params object[] args)
|
||||||
{
|
{
|
||||||
LogProgressMessage(logger, LogLevel.Trace, message, args);
|
var formattedMessage = string.Format(message, args);
|
||||||
|
LogProgressMessage(logger, LogLevel.Trace, formattedMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void LogProgressMessage(Logger logger, LogLevel level, string message, object[] parameters)
|
private static void LogProgressMessage(Logger logger, LogLevel level, string message)
|
||||||
{
|
{
|
||||||
var logEvent = new LogEventInfo(level, logger.Name, null, message, parameters);
|
var logEvent = new LogEventInfo(level, logger.Name, message);
|
||||||
logEvent.Properties.Add("Status", "");
|
logEvent.Properties.Add("Status", "");
|
||||||
|
|
||||||
logger.Log(logEvent);
|
logger.Log(logEvent);
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ namespace NzbDrone.Common.Instrumentation
|
|||||||
RegisterDebugger();
|
RegisterDebugger();
|
||||||
}
|
}
|
||||||
|
|
||||||
RegisterSentry(updateApp, appFolderInfo);
|
RegisterSentry(updateApp);
|
||||||
|
|
||||||
if (updateApp)
|
if (updateApp)
|
||||||
{
|
{
|
||||||
@@ -62,7 +62,7 @@ namespace NzbDrone.Common.Instrumentation
|
|||||||
LogManager.ReconfigExistingLoggers();
|
LogManager.ReconfigExistingLoggers();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void RegisterSentry(bool updateClient, IAppFolderInfo appFolderInfo)
|
private static void RegisterSentry(bool updateClient)
|
||||||
{
|
{
|
||||||
string dsn;
|
string dsn;
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ namespace NzbDrone.Common.Instrumentation
|
|||||||
: "https://31e00a6c63ea42c8b5fe70358526a30d@sentry.servarr.com/4";
|
: "https://31e00a6c63ea42c8b5fe70358526a30d@sentry.servarr.com/4";
|
||||||
}
|
}
|
||||||
|
|
||||||
var target = new SentryTarget(dsn, appFolderInfo)
|
var target = new SentryTarget(dsn)
|
||||||
{
|
{
|
||||||
Name = "sentryTarget",
|
Name = "sentryTarget",
|
||||||
Layout = "${message}"
|
Layout = "${message}"
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ using NLog;
|
|||||||
using NLog.Common;
|
using NLog.Common;
|
||||||
using NLog.Targets;
|
using NLog.Targets;
|
||||||
using NzbDrone.Common.EnvironmentInfo;
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
using NzbDrone.Common.Extensions;
|
|
||||||
using Sentry;
|
using Sentry;
|
||||||
|
|
||||||
namespace NzbDrone.Common.Instrumentation.Sentry
|
namespace NzbDrone.Common.Instrumentation.Sentry
|
||||||
@@ -100,7 +99,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
|||||||
public bool FilterEvents { get; set; }
|
public bool FilterEvents { get; set; }
|
||||||
public bool SentryEnabled { get; set; }
|
public bool SentryEnabled { get; set; }
|
||||||
|
|
||||||
public SentryTarget(string dsn, IAppFolderInfo appFolderInfo)
|
public SentryTarget(string dsn)
|
||||||
{
|
{
|
||||||
_sdk = SentrySdk.Init(o =>
|
_sdk = SentrySdk.Init(o =>
|
||||||
{
|
{
|
||||||
@@ -108,33 +107,9 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
|||||||
o.AttachStacktrace = true;
|
o.AttachStacktrace = true;
|
||||||
o.MaxBreadcrumbs = 200;
|
o.MaxBreadcrumbs = 200;
|
||||||
o.Release = $"{BuildInfo.AppName}@{BuildInfo.Release}";
|
o.Release = $"{BuildInfo.AppName}@{BuildInfo.Release}";
|
||||||
o.SetBeforeSend(x => SentryCleanser.CleanseEvent(x));
|
o.BeforeSend = x => SentryCleanser.CleanseEvent(x);
|
||||||
o.SetBeforeBreadcrumb(x => SentryCleanser.CleanseBreadcrumb(x));
|
o.BeforeBreadcrumb = x => SentryCleanser.CleanseBreadcrumb(x);
|
||||||
o.Environment = BuildInfo.Branch;
|
o.Environment = BuildInfo.Branch;
|
||||||
|
|
||||||
// Crash free run statistics (sends a ping for healthy and for crashes sessions)
|
|
||||||
o.AutoSessionTracking = false;
|
|
||||||
|
|
||||||
// Caches files in the event device is offline
|
|
||||||
// Sentry creates a 'sentry' sub directory, no need to concat here
|
|
||||||
o.CacheDirectoryPath = appFolderInfo.GetAppDataPath();
|
|
||||||
|
|
||||||
// default environment is production
|
|
||||||
if (!RuntimeInfo.IsProduction)
|
|
||||||
{
|
|
||||||
if (RuntimeInfo.IsDevelopment)
|
|
||||||
{
|
|
||||||
o.Environment = "development";
|
|
||||||
}
|
|
||||||
else if (RuntimeInfo.IsTesting)
|
|
||||||
{
|
|
||||||
o.Environment = "testing";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
o.Environment = "other";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
InitializeScope();
|
InitializeScope();
|
||||||
@@ -152,7 +127,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
|||||||
{
|
{
|
||||||
SentrySdk.ConfigureScope(scope =>
|
SentrySdk.ConfigureScope(scope =>
|
||||||
{
|
{
|
||||||
scope.User = new SentryUser
|
scope.User = new User
|
||||||
{
|
{
|
||||||
Id = HashUtil.AnonymousToken()
|
Id = HashUtil.AnonymousToken()
|
||||||
};
|
};
|
||||||
@@ -194,7 +169,9 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
|||||||
|
|
||||||
private void OnError(Exception ex)
|
private void OnError(Exception ex)
|
||||||
{
|
{
|
||||||
if (ex is WebException webException)
|
var webException = ex as WebException;
|
||||||
|
|
||||||
|
if (webException != null)
|
||||||
{
|
{
|
||||||
var response = webException.Response as HttpWebResponse;
|
var response = webException.Response as HttpWebResponse;
|
||||||
var statusCode = response?.StatusCode;
|
var statusCode = response?.StatusCode;
|
||||||
@@ -313,21 +290,13 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var level = LoggingLevelMap[logEvent.Level];
|
|
||||||
var sentryEvent = new SentryEvent(logEvent.Exception)
|
var sentryEvent = new SentryEvent(logEvent.Exception)
|
||||||
{
|
{
|
||||||
Level = level,
|
Level = LoggingLevelMap[logEvent.Level],
|
||||||
Logger = logEvent.LoggerName,
|
Logger = logEvent.LoggerName,
|
||||||
Message = logEvent.FormattedMessage
|
Message = logEvent.FormattedMessage
|
||||||
};
|
};
|
||||||
|
|
||||||
if (level is SentryLevel.Fatal && logEvent.Exception is not null)
|
|
||||||
{
|
|
||||||
// Usages of 'fatal' here indicates the process will crash. In Sentry this is represented with
|
|
||||||
// the 'unhandled' exception flag
|
|
||||||
logEvent.Exception.SetSentryMechanism("Logger.Fatal", "Logger.Fatal was called", false);
|
|
||||||
}
|
|
||||||
|
|
||||||
sentryEvent.SetExtras(extras);
|
sentryEvent.SetExtras(extras);
|
||||||
sentryEvent.SetFingerprint(fingerPrint);
|
sentryEvent.SetFingerprint(fingerPrint);
|
||||||
|
|
||||||
|
|||||||
@@ -6,5 +6,4 @@ public class AuthOptions
|
|||||||
public bool? Enabled { get; set; }
|
public bool? Enabled { get; set; }
|
||||||
public string Method { get; set; }
|
public string Method { get; set; }
|
||||||
public string Required { get; set; }
|
public string Required { get; set; }
|
||||||
public bool? TrustCgnatIpAddresses { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ using System.ComponentModel;
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common.EnvironmentInfo;
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
using NzbDrone.Common.Model;
|
using NzbDrone.Common.Model;
|
||||||
@@ -118,9 +117,7 @@ namespace NzbDrone.Common.Processes
|
|||||||
UseShellExecute = false,
|
UseShellExecute = false,
|
||||||
RedirectStandardError = true,
|
RedirectStandardError = true,
|
||||||
RedirectStandardOutput = true,
|
RedirectStandardOutput = true,
|
||||||
RedirectStandardInput = true,
|
RedirectStandardInput = true
|
||||||
StandardOutputEncoding = Encoding.UTF8,
|
|
||||||
StandardErrorEncoding = Encoding.UTF8
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (environmentVariables != null)
|
if (environmentVariables != null)
|
||||||
@@ -316,7 +313,7 @@ namespace NzbDrone.Common.Processes
|
|||||||
processInfo = new ProcessInfo();
|
processInfo = new ProcessInfo();
|
||||||
processInfo.Id = process.Id;
|
processInfo.Id = process.Id;
|
||||||
processInfo.Name = process.ProcessName;
|
processInfo.Name = process.ProcessName;
|
||||||
processInfo.StartPath = process.MainModule?.FileName;
|
processInfo.StartPath = process.MainModule.FileName;
|
||||||
|
|
||||||
if (process.Id != GetCurrentProcessId() && process.HasExited)
|
if (process.Id != GetCurrentProcessId() && process.HasExited)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="DryIoc.dll" />
|
<PackageReference Include="DryIoc.dll" />
|
||||||
<PackageReference Include="IPAddressRange" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" />
|
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" />
|
||||||
<PackageReference Include="NLog.Extensions.Logging" />
|
<PackageReference Include="NLog.Extensions.Logging" />
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ namespace NzbDrone.Common.Reflection
|
|||||||
|
|
||||||
public static List<Type> ImplementationsOf<T>(this Assembly assembly)
|
public static List<Type> ImplementationsOf<T>(this Assembly assembly)
|
||||||
{
|
{
|
||||||
return assembly.GetExportedTypes().Where(c => typeof(T).IsAssignableFrom(c)).ToList();
|
return assembly.GetTypes().Where(c => typeof(T).IsAssignableFrom(c)).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool IsSimpleType(this Type type)
|
public static bool IsSimpleType(this Type type)
|
||||||
@@ -68,7 +68,7 @@ namespace NzbDrone.Common.Reflection
|
|||||||
|
|
||||||
public static Type FindTypeByName(this Assembly assembly, string name)
|
public static Type FindTypeByName(this Assembly assembly, string name)
|
||||||
{
|
{
|
||||||
return assembly.GetExportedTypes().SingleOrDefault(c => c.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase));
|
return assembly.GetTypes().SingleOrDefault(c => c.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool HasAttribute<TAttribute>(this Type type)
|
public static bool HasAttribute<TAttribute>(this Type type)
|
||||||
|
|||||||
@@ -711,30 +711,6 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||||||
item.CanMoveFiles.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("pausedUP")]
|
||||||
[TestCase("stoppedUP")]
|
[TestCase("stoppedUP")]
|
||||||
public void should_be_removable_and_should_allow_move_files_if_overridden_max_ratio_reached_and_paused(string state)
|
public void should_be_removable_and_should_allow_move_files_if_overridden_max_ratio_reached_and_paused(string state)
|
||||||
@@ -747,30 +723,6 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||||||
item.CanMoveFiles.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("pausedUP")]
|
||||||
[TestCase("stoppedUP")]
|
[TestCase("stoppedUP")]
|
||||||
public void should_not_be_removable_if_overridden_max_ratio_not_reached_and_paused(string state)
|
public void should_not_be_removable_if_overridden_max_ratio_not_reached_and_paused(string state)
|
||||||
|
|||||||
@@ -478,37 +478,6 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests
|
|||||||
downloadClientInfo.RemovesCompletedDownloads.Should().BeTrue();
|
downloadClientInfo.RemovesCompletedDownloads.Should().BeTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("all", 0)]
|
|
||||||
[TestCase("days-archive", 15)]
|
|
||||||
[TestCase("days-delete", 15)]
|
|
||||||
public void should_set_history_removes_completed_downloads_false_for_separate_properties(string option, int number)
|
|
||||||
{
|
|
||||||
_config.Misc.history_retention_option = option;
|
|
||||||
_config.Misc.history_retention_number = number;
|
|
||||||
|
|
||||||
var downloadClientInfo = Subject.GetStatus();
|
|
||||||
|
|
||||||
downloadClientInfo.RemovesCompletedDownloads.Should().BeFalse();
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase("number-archive", 10)]
|
|
||||||
[TestCase("number-delete", 10)]
|
|
||||||
[TestCase("number-archive", 0)]
|
|
||||||
[TestCase("number-delete", 0)]
|
|
||||||
[TestCase("days-archive", 3)]
|
|
||||||
[TestCase("days-delete", 3)]
|
|
||||||
[TestCase("all-archive", 0)]
|
|
||||||
[TestCase("all-delete", 0)]
|
|
||||||
public void should_set_history_removes_completed_downloads_true_for_separate_properties(string option, int number)
|
|
||||||
{
|
|
||||||
_config.Misc.history_retention_option = option;
|
|
||||||
_config.Misc.history_retention_number = number;
|
|
||||||
|
|
||||||
var downloadClientInfo = Subject.GetStatus();
|
|
||||||
|
|
||||||
downloadClientInfo.RemovesCompletedDownloads.Should().BeTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase(@"Y:\nzbget\root", @"completed\downloads", @"vv", @"Y:\nzbget\root\completed\downloads", @"Y:\nzbget\root\completed\downloads\vv")]
|
[TestCase(@"Y:\nzbget\root", @"completed\downloads", @"vv", @"Y:\nzbget\root\completed\downloads", @"Y:\nzbget\root\completed\downloads\vv")]
|
||||||
[TestCase(@"Y:\nzbget\root", @"completed", @"vv", @"Y:\nzbget\root\completed", @"Y:\nzbget\root\completed\vv")]
|
[TestCase(@"Y:\nzbget\root", @"completed", @"vv", @"Y:\nzbget\root\completed", @"Y:\nzbget\root\completed\vv")]
|
||||||
[TestCase(@"/nzbget/root", @"completed/downloads", @"vv", @"/nzbget/root/completed/downloads", @"/nzbget/root/completed/downloads/vv")]
|
[TestCase(@"/nzbget/root", @"completed/downloads", @"vv", @"/nzbget/root/completed/downloads", @"/nzbget/root/completed/downloads/vv")]
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ namespace NzbDrone.Core.Test.Http
|
|||||||
{
|
{
|
||||||
private HttpProxySettings GetProxySettings()
|
private HttpProxySettings GetProxySettings()
|
||||||
{
|
{
|
||||||
return new HttpProxySettings(ProxyType.Socks5, "localhost", 8080, "*.httpbin.org,google.com,172.16.0.0/12", true, null, null);
|
return new HttpProxySettings(ProxyType.Socks5, "localhost", 8080, "*.httpbin.org,google.com", true, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -23,7 +23,6 @@ namespace NzbDrone.Core.Test.Http
|
|||||||
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://eu.httpbin.org/get")).Should().BeTrue();
|
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://eu.httpbin.org/get")).Should().BeTrue();
|
||||||
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://google.com/get")).Should().BeTrue();
|
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://google.com/get")).Should().BeTrue();
|
||||||
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://localhost:8654/get")).Should().BeTrue();
|
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://localhost:8654/get")).Should().BeTrue();
|
||||||
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://172.21.0.1:8989/api/v3/indexer/schema")).Should().BeTrue();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -32,7 +31,6 @@ namespace NzbDrone.Core.Test.Http
|
|||||||
var settings = GetProxySettings();
|
var settings = GetProxySettings();
|
||||||
|
|
||||||
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://bing.com/get")).Should().BeFalse();
|
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://bing.com/get")).Should().BeFalse();
|
||||||
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://172.3.0.1:8989/api/v3/indexer/schema")).Should().BeFalse();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ using NzbDrone.Core.Test.Framework;
|
|||||||
namespace NzbDrone.Core.Test.MetadataSource.Goodreads
|
namespace NzbDrone.Core.Test.MetadataSource.Goodreads
|
||||||
{
|
{
|
||||||
[TestFixture]
|
[TestFixture]
|
||||||
[Ignore("Waiting for metadata to be back again", Until = "2026-01-15 00:00:00Z")]
|
[Ignore("Waiting for metadata to be back again", Until = "2024-12-15 00:00:00Z")]
|
||||||
public class BookInfoProxyFixture : CoreTest<BookInfoProxy>
|
public class BookInfoProxyFixture : CoreTest<BookInfoProxy>
|
||||||
{
|
{
|
||||||
private MetadataProfile _metadataProfile;
|
private MetadataProfile _metadataProfile;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ using NzbDrone.Test.Common;
|
|||||||
namespace NzbDrone.Core.Test.MetadataSource.Goodreads
|
namespace NzbDrone.Core.Test.MetadataSource.Goodreads
|
||||||
{
|
{
|
||||||
[TestFixture]
|
[TestFixture]
|
||||||
[Ignore("Waiting for metadata to be back again", Until = "2026-01-15 00:00:00Z")]
|
[Ignore("Waiting for metadata to be back again", Until = "2024-12-15 00:00:00Z")]
|
||||||
public class BookInfoProxySearchFixture : CoreTest<BookInfoProxy>
|
public class BookInfoProxySearchFixture : CoreTest<BookInfoProxy>
|
||||||
{
|
{
|
||||||
[SetUp]
|
[SetUp]
|
||||||
|
|||||||
@@ -21,14 +21,14 @@ namespace NzbDrone.Core.Test.UpdateTests
|
|||||||
public void no_update_when_version_higher()
|
public void no_update_when_version_higher()
|
||||||
{
|
{
|
||||||
UseRealHttp();
|
UseRealHttp();
|
||||||
Subject.GetLatestUpdate("develop", new Version(10, 0)).Should().BeNull();
|
Subject.GetLatestUpdate("nightly", new Version(10, 0)).Should().BeNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void finds_update_when_version_lower()
|
public void finds_update_when_version_lower()
|
||||||
{
|
{
|
||||||
UseRealHttp();
|
UseRealHttp();
|
||||||
Subject.GetLatestUpdate("develop", new Version(0, 1)).Should().NotBeNull();
|
Subject.GetLatestUpdate("nightly", new Version(0, 1)).Should().NotBeNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -42,9 +42,10 @@ namespace NzbDrone.Core.Test.UpdateTests
|
|||||||
[Test]
|
[Test]
|
||||||
public void should_get_recent_updates()
|
public void should_get_recent_updates()
|
||||||
{
|
{
|
||||||
const string branch = "develop";
|
const string branch = "nightly";
|
||||||
UseRealHttp();
|
UseRealHttp();
|
||||||
var recent = Subject.GetRecentUpdates(branch, new Version(0, 1), null);
|
var recent = Subject.GetRecentUpdates(branch, new Version(0, 1), null);
|
||||||
|
var recentWithChanges = recent.Where(c => c.Changes != null);
|
||||||
|
|
||||||
recent.Should().NotBeEmpty();
|
recent.Should().NotBeEmpty();
|
||||||
recent.Should().OnlyContain(c => c.Hash.IsNotNullOrWhiteSpace());
|
recent.Should().OnlyContain(c => c.Hash.IsNotNullOrWhiteSpace());
|
||||||
|
|||||||
@@ -66,19 +66,12 @@ namespace NzbDrone.Core.Backup
|
|||||||
{
|
{
|
||||||
_logger.ProgressInfo("Starting Backup");
|
_logger.ProgressInfo("Starting Backup");
|
||||||
|
|
||||||
var backupFolder = GetBackupFolder(backupType);
|
|
||||||
|
|
||||||
_diskProvider.EnsureFolder(_backupTempFolder);
|
_diskProvider.EnsureFolder(_backupTempFolder);
|
||||||
_diskProvider.EnsureFolder(backupFolder);
|
_diskProvider.EnsureFolder(GetBackupFolder(backupType));
|
||||||
|
|
||||||
if (!_diskProvider.FolderWritable(backupFolder))
|
|
||||||
{
|
|
||||||
throw new UnauthorizedAccessException($"Backup folder {backupFolder} is not writable");
|
|
||||||
}
|
|
||||||
|
|
||||||
var dateNow = DateTime.Now;
|
var dateNow = DateTime.Now;
|
||||||
var backupFilename = $"readarr_backup_v{BuildInfo.Version}_{dateNow:yyyy.MM.dd_HH.mm.ss}.zip";
|
var backupFilename = $"readarr_backup_v{BuildInfo.Version}_{dateNow:yyyy.MM.dd_HH.mm.ss}.zip";
|
||||||
var backupPath = Path.Combine(backupFolder, backupFilename);
|
var backupPath = Path.Combine(GetBackupFolder(backupType), backupFilename);
|
||||||
|
|
||||||
Cleanup();
|
Cleanup();
|
||||||
|
|
||||||
|
|||||||
@@ -102,9 +102,9 @@ namespace NzbDrone.Core.Books
|
|||||||
_logger.Error("ReadarrId {0} was not found, it may have been removed from Goodreads.", newAuthor.Metadata.Value.ForeignAuthorId);
|
_logger.Error("ReadarrId {0} was not found, it may have been removed from Goodreads.", newAuthor.Metadata.Value.ForeignAuthorId);
|
||||||
|
|
||||||
throw new ValidationException(new List<ValidationFailure>
|
throw new ValidationException(new List<ValidationFailure>
|
||||||
{
|
{
|
||||||
new ("ForeignAuthorId", "An author with this ID was not found", newAuthor.Metadata.Value.ForeignAuthorId)
|
new ValidationFailure("MusicbrainzId", "An author with this ID was not found", newAuthor.Metadata.Value.ForeignAuthorId)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
author.ApplyChanges(newAuthor);
|
author.ApplyChanges(newAuthor);
|
||||||
|
|||||||
@@ -222,7 +222,7 @@ namespace NzbDrone.Core.Books
|
|||||||
|
|
||||||
protected override void DeleteEntity(Author local, bool deleteFiles)
|
protected override void DeleteEntity(Author local, bool deleteFiles)
|
||||||
{
|
{
|
||||||
_authorService.DeleteAuthor(local.Id, deleteFiles);
|
_authorService.DeleteAuthor(local.Id, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override List<Book> GetRemoteChildren(Author local, Author remote)
|
protected override List<Book> GetRemoteChildren(Author local, Author remote)
|
||||||
|
|||||||
@@ -239,7 +239,7 @@ namespace NzbDrone.Core.Books
|
|||||||
|
|
||||||
protected override void DeleteEntity(Book local, bool deleteFiles)
|
protected override void DeleteEntity(Book local, bool deleteFiles)
|
||||||
{
|
{
|
||||||
_bookService.DeleteBook(local.Id, deleteFiles);
|
_bookService.DeleteBook(local.Id, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override List<Edition> GetRemoteChildren(Book local, Book remote)
|
protected override List<Edition> GetRemoteChildren(Book local, Book remote)
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ namespace NzbDrone.Core.Books
|
|||||||
if (ShouldDelete(local))
|
if (ShouldDelete(local))
|
||||||
{
|
{
|
||||||
_logger.Warn($"{typeof(TEntity).Name} {local} not found in metadata and is being deleted");
|
_logger.Warn($"{typeof(TEntity).Name} {local} not found in metadata and is being deleted");
|
||||||
DeleteEntity(local, false);
|
DeleteEntity(local, true);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ namespace NzbDrone.Core.Configuration
|
|||||||
string SyslogServer { get; }
|
string SyslogServer { get; }
|
||||||
int SyslogPort { get; }
|
int SyslogPort { get; }
|
||||||
string SyslogLevel { get; }
|
string SyslogLevel { get; }
|
||||||
string Theme { get; }
|
|
||||||
string PostgresHost { get; }
|
string PostgresHost { get; }
|
||||||
int PostgresPort { get; }
|
int PostgresPort { get; }
|
||||||
string PostgresUser { get; }
|
string PostgresUser { get; }
|
||||||
@@ -61,7 +60,7 @@ namespace NzbDrone.Core.Configuration
|
|||||||
string PostgresMainDb { get; }
|
string PostgresMainDb { get; }
|
||||||
string PostgresLogDb { get; }
|
string PostgresLogDb { get; }
|
||||||
string PostgresCacheDb { get; }
|
string PostgresCacheDb { get; }
|
||||||
bool TrustCgnatIpAddresses { get; }
|
string Theme { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ConfigFileProvider : IConfigFileProvider
|
public class ConfigFileProvider : IConfigFileProvider
|
||||||
@@ -254,21 +253,7 @@ namespace NzbDrone.Core.Configuration
|
|||||||
}
|
}
|
||||||
|
|
||||||
public string UiFolder => BuildInfo.IsDebug ? Path.Combine("..", "UI") : "UI";
|
public string UiFolder => BuildInfo.IsDebug ? Path.Combine("..", "UI") : "UI";
|
||||||
|
public string InstanceName => _appOptions.InstanceName ?? GetValue("InstanceName", BuildInfo.AppName);
|
||||||
public string InstanceName
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
var instanceName = _appOptions.InstanceName ?? GetValue("InstanceName", BuildInfo.AppName);
|
|
||||||
|
|
||||||
if (instanceName.Contains(BuildInfo.AppName, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return instanceName;
|
|
||||||
}
|
|
||||||
|
|
||||||
return BuildInfo.AppName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool UpdateAutomatically => _updateOptions.Automatically ?? GetValueBoolean("UpdateAutomatically", OsInfo.IsWindows, false);
|
public bool UpdateAutomatically => _updateOptions.Automatically ?? GetValueBoolean("UpdateAutomatically", OsInfo.IsWindows, false);
|
||||||
|
|
||||||
@@ -477,7 +462,5 @@ namespace NzbDrone.Core.Configuration
|
|||||||
{
|
{
|
||||||
SetValue("ApiKey", GenerateApiKey());
|
SetValue("ApiKey", GenerateApiKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TrustCgnatIpAddresses => _authOptions.TrustCgnatIpAddresses ?? GetValueBoolean("TrustCgnatIpAddresses", false, persist: false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -404,12 +404,6 @@ namespace NzbDrone.Core.Configuration
|
|||||||
|
|
||||||
public string ApplicationUrl => GetValue("ApplicationUrl", string.Empty);
|
public string ApplicationUrl => GetValue("ApplicationUrl", string.Empty);
|
||||||
|
|
||||||
public bool TrustCgnatIpAddresses
|
|
||||||
{
|
|
||||||
get { return GetValueBoolean("TrustCgnatIpAddresses", false); }
|
|
||||||
set { SetValue("TrustCgnatIpAddresses", value); }
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetValue(string key)
|
private string GetValue(string key)
|
||||||
{
|
{
|
||||||
return GetValue(key, string.Empty);
|
return GetValue(key, string.Empty);
|
||||||
|
|||||||
@@ -264,7 +264,7 @@ namespace NzbDrone.Core.Datastore
|
|||||||
|
|
||||||
protected void Delete(SqlBuilder builder)
|
protected void Delete(SqlBuilder builder)
|
||||||
{
|
{
|
||||||
var sql = builder.AddDeleteTemplate(typeof(TModel));
|
var sql = builder.AddDeleteTemplate(typeof(TModel)).LogQuery();
|
||||||
|
|
||||||
using (var conn = _database.OpenConnection())
|
using (var conn = _database.OpenConnection())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ namespace NzbDrone.Core.Datastore
|
|||||||
Environment.SetEnvironmentVariable("No_Expand", "true");
|
Environment.SetEnvironmentVariable("No_Expand", "true");
|
||||||
Environment.SetEnvironmentVariable("No_SQLiteXmlConfigFile", "true");
|
Environment.SetEnvironmentVariable("No_SQLiteXmlConfigFile", "true");
|
||||||
Environment.SetEnvironmentVariable("No_PreLoadSQLite", "true");
|
Environment.SetEnvironmentVariable("No_PreLoadSQLite", "true");
|
||||||
Environment.SetEnvironmentVariable("No_SQLiteFunctions", "true");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public DbFactory(IMigrationController migrationController,
|
public DbFactory(IMigrationController migrationController,
|
||||||
|
|||||||
@@ -620,14 +620,14 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
{
|
{
|
||||||
if (torrent.RatioLimit >= 0)
|
if (torrent.RatioLimit >= 0)
|
||||||
{
|
{
|
||||||
if (torrent.RatioLimit - torrent.Ratio <= 0.001f)
|
if (torrent.Ratio >= torrent.RatioLimit)
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (torrent.RatioLimit == -2 && config.MaxRatioEnabled)
|
else if (torrent.RatioLimit == -2 && config.MaxRatioEnabled)
|
||||||
{
|
{
|
||||||
if (config.MaxRatio - torrent.Ratio <= 0.001f)
|
if (Math.Round(torrent.Ratio, 2) >= config.MaxRatio)
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -263,7 +263,20 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
|
|||||||
status.OutputRootFolders = new List<OsPath> { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, category.FullPath) };
|
status.OutputRootFolders = new List<OsPath> { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, category.FullPath) };
|
||||||
}
|
}
|
||||||
|
|
||||||
status.RemovesCompletedDownloads = RemovesCompletedDownloads(config);
|
if (config.Misc.history_retention.IsNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
status.RemovesCompletedDownloads = false;
|
||||||
|
}
|
||||||
|
else if (config.Misc.history_retention.EndsWith("d"))
|
||||||
|
{
|
||||||
|
int.TryParse(config.Misc.history_retention.AsSpan(0, config.Misc.history_retention.Length - 1),
|
||||||
|
out var daysRetention);
|
||||||
|
status.RemovesCompletedDownloads = daysRetention < 14;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
status.RemovesCompletedDownloads = config.Misc.history_retention != "0";
|
||||||
|
}
|
||||||
|
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
@@ -505,43 +518,6 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
|
|||||||
return categories.Contains(category);
|
return categories.Contains(category);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool RemovesCompletedDownloads(SabnzbdConfig config)
|
|
||||||
{
|
|
||||||
var retention = config.Misc.history_retention;
|
|
||||||
var option = config.Misc.history_retention_option;
|
|
||||||
var number = config.Misc.history_retention_number;
|
|
||||||
|
|
||||||
switch (option)
|
|
||||||
{
|
|
||||||
case "all":
|
|
||||||
return false;
|
|
||||||
case "number-archive":
|
|
||||||
case "number-delete":
|
|
||||||
return true;
|
|
||||||
case "days-archive":
|
|
||||||
case "days-delete":
|
|
||||||
return number < 14;
|
|
||||||
case "all-archive":
|
|
||||||
case "all-delete":
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Remove these checks once support for SABnzbd < 4.3 is removed
|
|
||||||
if (retention.IsNullOrWhiteSpace())
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (retention.EndsWith("d"))
|
|
||||||
{
|
|
||||||
int.TryParse(config.Misc.history_retention.AsSpan(0, config.Misc.history_retention.Length - 1),
|
|
||||||
out var daysRetention);
|
|
||||||
return daysRetention < 14;
|
|
||||||
}
|
|
||||||
|
|
||||||
return retention != "0";
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool ValidatePath(DownloadClientItem downloadClientItem)
|
private bool ValidatePath(DownloadClientItem downloadClientItem)
|
||||||
{
|
{
|
||||||
var downloadItemOutputPath = downloadClientItem.OutputPath;
|
var downloadItemOutputPath = downloadClientItem.OutputPath;
|
||||||
|
|||||||
@@ -30,8 +30,6 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
|
|||||||
public bool enable_date_sorting { get; set; }
|
public bool enable_date_sorting { get; set; }
|
||||||
public bool pre_check { get; set; }
|
public bool pre_check { get; set; }
|
||||||
public string history_retention { get; set; }
|
public string history_retention { get; set; }
|
||||||
public string history_retention_option { get; set; }
|
|
||||||
public int history_retention_number { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SabnzbdCategory
|
public class SabnzbdCategory
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ using System.Net;
|
|||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common.Cache;
|
using NzbDrone.Common.Cache;
|
||||||
using NzbDrone.Common.EnvironmentInfo;
|
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Http;
|
using NzbDrone.Common.Http;
|
||||||
using NzbDrone.Common.Serializer;
|
using NzbDrone.Common.Serializer;
|
||||||
@@ -209,7 +208,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission
|
|||||||
|
|
||||||
private void AuthenticateClient(HttpRequestBuilder requestBuilder, TransmissionSettings settings, bool reauthenticate = false)
|
private void AuthenticateClient(HttpRequestBuilder requestBuilder, TransmissionSettings settings, bool reauthenticate = false)
|
||||||
{
|
{
|
||||||
var authKey = $"{requestBuilder.BaseUrl}:{settings.Password}";
|
var authKey = string.Format("{0}:{1}", requestBuilder.BaseUrl, settings.Password);
|
||||||
|
|
||||||
var sessionId = _authSessionIDCache.Find(authKey);
|
var sessionId = _authSessionIDCache.Find(authKey);
|
||||||
|
|
||||||
@@ -221,26 +220,24 @@ namespace NzbDrone.Core.Download.Clients.Transmission
|
|||||||
authLoginRequest.SuppressHttpError = true;
|
authLoginRequest.SuppressHttpError = true;
|
||||||
|
|
||||||
var response = _httpClient.Execute(authLoginRequest);
|
var response = _httpClient.Execute(authLoginRequest);
|
||||||
|
if (response.StatusCode == HttpStatusCode.MovedPermanently)
|
||||||
switch (response.StatusCode)
|
|
||||||
{
|
{
|
||||||
case HttpStatusCode.MovedPermanently:
|
var url = response.Headers.GetSingleValue("Location");
|
||||||
var url = response.Headers.GetSingleValue("Location");
|
|
||||||
|
|
||||||
throw new DownloadClientException("Remote site redirected to " + url);
|
throw new DownloadClientException("Remote site redirected to " + url);
|
||||||
case HttpStatusCode.Forbidden:
|
}
|
||||||
throw new DownloadClientException($"Failed to authenticate with Transmission. It may be necessary to add {BuildInfo.AppName}'s IP address to RPC whitelist.");
|
else if (response.StatusCode == HttpStatusCode.Conflict)
|
||||||
case HttpStatusCode.Conflict:
|
{
|
||||||
sessionId = response.Headers.GetSingleValue("X-Transmission-Session-Id");
|
sessionId = response.Headers.GetSingleValue("X-Transmission-Session-Id");
|
||||||
|
|
||||||
if (sessionId == null)
|
if (sessionId == null)
|
||||||
{
|
{
|
||||||
throw new DownloadClientException("Remote host did not return a Session Id.");
|
throw new DownloadClientException("Remote host did not return a Session Id.");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
break;
|
else
|
||||||
default:
|
{
|
||||||
throw new DownloadClientAuthenticationException("Failed to authenticate with Transmission.");
|
throw new DownloadClientAuthenticationException("Failed to authenticate with Transmission.");
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.Debug("Transmission authentication succeeded.");
|
_logger.Debug("Transmission authentication succeeded.");
|
||||||
|
|||||||
@@ -4,24 +4,24 @@ namespace NzbDrone.Core.Exceptions
|
|||||||
{
|
{
|
||||||
public class AuthorNotFoundException : NzbDroneException
|
public class AuthorNotFoundException : NzbDroneException
|
||||||
{
|
{
|
||||||
public string ForeignAuthorId { get; set; }
|
public string MusicBrainzId { get; set; }
|
||||||
|
|
||||||
public AuthorNotFoundException(string foreignAuthorId)
|
public AuthorNotFoundException(string musicbrainzId)
|
||||||
: base($"Author with id {foreignAuthorId} was not found, it may have been removed from the metadata server.")
|
: base(string.Format("Author with id {0} was not found, it may have been removed from the metadata server.", musicbrainzId))
|
||||||
{
|
{
|
||||||
ForeignAuthorId = foreignAuthorId;
|
MusicBrainzId = musicbrainzId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public AuthorNotFoundException(string foreignAuthorId, string message, params object[] args)
|
public AuthorNotFoundException(string musicbrainzId, string message, params object[] args)
|
||||||
: base(message, args)
|
: base(message, args)
|
||||||
{
|
{
|
||||||
ForeignAuthorId = foreignAuthorId;
|
MusicBrainzId = musicbrainzId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public AuthorNotFoundException(string foreignAuthorId, string message)
|
public AuthorNotFoundException(string musicbrainzId, string message)
|
||||||
: base(message)
|
: base(message)
|
||||||
{
|
{
|
||||||
ForeignAuthorId = foreignAuthorId;
|
MusicBrainzId = musicbrainzId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,24 +4,24 @@ namespace NzbDrone.Core.Exceptions
|
|||||||
{
|
{
|
||||||
public class BookNotFoundException : NzbDroneException
|
public class BookNotFoundException : NzbDroneException
|
||||||
{
|
{
|
||||||
public string ForeignBookId { get; set; }
|
public string MusicBrainzId { get; set; }
|
||||||
|
|
||||||
public BookNotFoundException(string foreignBookId)
|
public BookNotFoundException(string musicbrainzId)
|
||||||
: base($"Book with id {foreignBookId} was not found, it may have been removed from metadata server.")
|
: base(string.Format("Book with id {0} was not found, it may have been removed from metadata server.", musicbrainzId))
|
||||||
{
|
{
|
||||||
ForeignBookId = foreignBookId;
|
MusicBrainzId = musicbrainzId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public BookNotFoundException(string foreignBookId, string message, params object[] args)
|
public BookNotFoundException(string musicbrainzId, string message, params object[] args)
|
||||||
: base(message, args)
|
: base(message, args)
|
||||||
{
|
{
|
||||||
ForeignBookId = foreignBookId;
|
MusicBrainzId = musicbrainzId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public BookNotFoundException(string foreignBookId, string message)
|
public BookNotFoundException(string musicbrainzId, string message)
|
||||||
: base(message)
|
: base(message)
|
||||||
{
|
{
|
||||||
ForeignBookId = foreignBookId;
|
MusicBrainzId = musicbrainzId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,24 +4,24 @@ namespace NzbDrone.Core.Exceptions
|
|||||||
{
|
{
|
||||||
public class EditionNotFoundException : NzbDroneException
|
public class EditionNotFoundException : NzbDroneException
|
||||||
{
|
{
|
||||||
public string ForeignEditionId { get; set; }
|
public string MusicBrainzId { get; set; }
|
||||||
|
|
||||||
public EditionNotFoundException(string foreignEditionId)
|
public EditionNotFoundException(string musicbrainzId)
|
||||||
: base($"Edition with id {foreignEditionId} was not found, it may have been removed from metadata server.")
|
: base(string.Format("Edition with id {0} was not found, it may have been removed from metadata server.", musicbrainzId))
|
||||||
{
|
{
|
||||||
ForeignEditionId = foreignEditionId;
|
MusicBrainzId = musicbrainzId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public EditionNotFoundException(string foreignEditionId, string message, params object[] args)
|
public EditionNotFoundException(string musicbrainzId, string message, params object[] args)
|
||||||
: base(message, args)
|
: base(message, args)
|
||||||
{
|
{
|
||||||
ForeignEditionId = foreignEditionId;
|
MusicBrainzId = musicbrainzId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public EditionNotFoundException(string foreignEditionId, string message)
|
public EditionNotFoundException(string musicbrainzId, string message)
|
||||||
: base(message)
|
: base(message)
|
||||||
{
|
{
|
||||||
ForeignEditionId = foreignEditionId;
|
MusicBrainzId = musicbrainzId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,6 @@ namespace NzbDrone.Core.History
|
|||||||
{
|
{
|
||||||
public const string DOWNLOAD_CLIENT = "downloadClient";
|
public const string DOWNLOAD_CLIENT = "downloadClient";
|
||||||
public const string RELEASE_SOURCE = "releaseSource";
|
public const string RELEASE_SOURCE = "releaseSource";
|
||||||
public const string RELEASE_GROUP = "releaseGroup";
|
|
||||||
public const string SIZE = "size";
|
|
||||||
public const string INDEXER = "indexer";
|
|
||||||
|
|
||||||
public EntityHistory()
|
public EntityHistory()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -116,7 +116,6 @@ namespace NzbDrone.Core.History
|
|||||||
{
|
{
|
||||||
var builder = Builder()
|
var builder = Builder()
|
||||||
.Join<EntityHistory, Author>((h, a) => h.AuthorId == a.Id)
|
.Join<EntityHistory, Author>((h, a) => h.AuthorId == a.Id)
|
||||||
.LeftJoin<EntityHistory, Book>((h, b) => h.BookId == b.Id)
|
|
||||||
.Where<EntityHistory>(x => x.Date >= date);
|
.Where<EntityHistory>(x => x.Date >= date);
|
||||||
|
|
||||||
if (eventType.HasValue)
|
if (eventType.HasValue)
|
||||||
@@ -124,10 +123,9 @@ namespace NzbDrone.Core.History
|
|||||||
builder.Where<EntityHistory>(h => h.EventType == eventType);
|
builder.Where<EntityHistory>(h => h.EventType == eventType);
|
||||||
}
|
}
|
||||||
|
|
||||||
return _database.QueryJoined<EntityHistory, Author, Book>(builder, (history, author, book) =>
|
return _database.QueryJoined<EntityHistory, Author>(builder, (history, author) =>
|
||||||
{
|
{
|
||||||
history.Author = author;
|
history.Author = author;
|
||||||
history.Book = book;
|
|
||||||
return history;
|
return history;
|
||||||
}).OrderBy(h => h.Date).ToList();
|
}).OrderBy(h => h.Date).ToList();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -263,9 +263,7 @@ namespace NzbDrone.Core.History
|
|||||||
history.Data.Add("DownloadClient", message.DownloadClient);
|
history.Data.Add("DownloadClient", message.DownloadClient);
|
||||||
history.Data.Add("DownloadClientName", message.TrackedDownload?.DownloadItem.DownloadClientInfo.Name);
|
history.Data.Add("DownloadClientName", message.TrackedDownload?.DownloadItem.DownloadClientInfo.Name);
|
||||||
history.Data.Add("Message", message.Message);
|
history.Data.Add("Message", message.Message);
|
||||||
history.Data.Add("ReleaseGroup", message.TrackedDownload?.RemoteBook?.ParsedBookInfo?.ReleaseGroup ?? message.Data.GetValueOrDefault(EntityHistory.RELEASE_GROUP));
|
history.Data.Add("Size", message.TrackedDownload?.DownloadItem.TotalSize.ToString());
|
||||||
history.Data.Add("Size", message.TrackedDownload?.DownloadItem.TotalSize.ToString() ?? message.Data.GetValueOrDefault(EntityHistory.SIZE));
|
|
||||||
history.Data.Add("Indexer", message.TrackedDownload?.RemoteBook?.Release?.Indexer ?? message.Data.GetValueOrDefault(EntityHistory.INDEXER));
|
|
||||||
|
|
||||||
_historyRepository.Insert(history);
|
_historyRepository.Insert(history);
|
||||||
}
|
}
|
||||||
@@ -375,7 +373,6 @@ namespace NzbDrone.Core.History
|
|||||||
history.Data.Add("Message", message.Message);
|
history.Data.Add("Message", message.Message);
|
||||||
history.Data.Add("ReleaseGroup", message.TrackedDownload?.RemoteBook?.ParsedBookInfo?.ReleaseGroup);
|
history.Data.Add("ReleaseGroup", message.TrackedDownload?.RemoteBook?.ParsedBookInfo?.ReleaseGroup);
|
||||||
history.Data.Add("Size", message.TrackedDownload?.DownloadItem.TotalSize.ToString());
|
history.Data.Add("Size", message.TrackedDownload?.DownloadItem.TotalSize.ToString());
|
||||||
history.Data.Add("Indexer", message.TrackedDownload?.RemoteBook?.Release?.Indexer);
|
|
||||||
|
|
||||||
historyToAdd.Add(history);
|
historyToAdd.Add(history);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using NetTools;
|
|
||||||
using NzbDrone.Common.Http;
|
using NzbDrone.Common.Http;
|
||||||
using NzbDrone.Common.Http.Proxy;
|
using NzbDrone.Common.Http.Proxy;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
@@ -54,15 +52,7 @@ namespace NzbDrone.Core.Http
|
|||||||
//We are utilizing the WebProxy implementation here to save us having to re-implement it. This way we use Microsofts implementation
|
//We are utilizing the WebProxy implementation here to save us having to re-implement it. This way we use Microsofts implementation
|
||||||
var proxy = new WebProxy(proxySettings.Host + ":" + proxySettings.Port, proxySettings.BypassLocalAddress, proxySettings.BypassListAsArray);
|
var proxy = new WebProxy(proxySettings.Host + ":" + proxySettings.Port, proxySettings.BypassLocalAddress, proxySettings.BypassListAsArray);
|
||||||
|
|
||||||
return proxy.IsBypassed((Uri)url) || IsBypassedByIpAddressRange(proxySettings.BypassListAsArray, url.Host);
|
return proxy.IsBypassed((Uri)url);
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsBypassedByIpAddressRange(string[] bypassList, string host)
|
|
||||||
{
|
|
||||||
return bypassList.Any(bypass =>
|
|
||||||
IPAddressRange.TryParse(bypass, out var ipAddressRange) &&
|
|
||||||
IPAddress.TryParse(host, out var ipAddress) &&
|
|
||||||
ipAddressRange.Contains(ipAddress));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ namespace NzbDrone.Core.ImportLists
|
|||||||
report.BookGoodreadsId = remoteBook.ForeignBookId;
|
report.BookGoodreadsId = remoteBook.ForeignBookId;
|
||||||
report.Book = remoteBook.Title;
|
report.Book = remoteBook.Title;
|
||||||
report.Author ??= remoteBook.AuthorMetadata.Value.Name;
|
report.Author ??= remoteBook.AuthorMetadata.Value.Name;
|
||||||
report.AuthorGoodreadsId ??= remoteBook.AuthorMetadata.Value.ForeignAuthorId;
|
report.AuthorGoodreadsId ??= remoteBook.AuthorMetadata.Value.Name;
|
||||||
}
|
}
|
||||||
catch (BookNotFoundException)
|
catch (BookNotFoundException)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ namespace NzbDrone.Core.Indexers.FileList
|
|||||||
var url = new HttpUri(_settings.BaseUrl)
|
var url = new HttpUri(_settings.BaseUrl)
|
||||||
.CombinePath("download.php")
|
.CombinePath("download.php")
|
||||||
.AddQueryParam("id", torrentId)
|
.AddQueryParam("id", torrentId)
|
||||||
.AddQueryParam("passkey", _settings.Passkey.Trim());
|
.AddQueryParam("passkey", _settings.Passkey);
|
||||||
|
|
||||||
return url.FullUri;
|
return url.FullUri;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -653,7 +653,5 @@
|
|||||||
"UnmappedFiles": "المجلدات غير المعينة",
|
"UnmappedFiles": "المجلدات غير المعينة",
|
||||||
"UpdateAppDirectlyLoadError": "تعذر تحديث {appName} مباشرة ،",
|
"UpdateAppDirectlyLoadError": "تعذر تحديث {appName} مباشرة ،",
|
||||||
"Clone": "قريب",
|
"Clone": "قريب",
|
||||||
"BuiltIn": "مدمج",
|
"BuiltIn": "مدمج"
|
||||||
"AddNewAuthorRootFolderHelpText": "سيتم إنشاء المجلد الفرعي \"{folder}\" تلقائيًا",
|
|
||||||
"AddRootFolder": "إضافة مجلد جذر"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -653,49 +653,5 @@
|
|||||||
"Clone": "Близо",
|
"Clone": "Близо",
|
||||||
"DockerUpdater": "актуализирайте контейнера на докера, за да получите актуализацията",
|
"DockerUpdater": "актуализирайте контейнера на докера, за да получите актуализацията",
|
||||||
"InstallLatest": "Инсталирайте най-новите",
|
"InstallLatest": "Инсталирайте най-новите",
|
||||||
"OnLatestVersion": "Вече е инсталирана най-новата версия на {appName}",
|
"OnLatestVersion": "Вече е инсталирана най-новата версия на {appName}"
|
||||||
"BlocklistAndSearch": "Списък за блокиране и търсене",
|
|
||||||
"BlocklistMultipleOnlyHint": "Списък за блокиране без търсене на заместители",
|
|
||||||
"BlocklistAndSearchHint": "Започнете търсене на заместител след блокиране",
|
|
||||||
"BlocklistAndSearchMultipleHint": "Започнете търсене на заместители след блокиране",
|
|
||||||
"DoNotBlocklistHint": "Премахване без блокиране",
|
|
||||||
"Database": "База данни",
|
|
||||||
"DoNotBlocklist": "Не блокирайте",
|
|
||||||
"AutoRedownloadFailedFromInteractiveSearchHelpText": "Автоматично търсене и опит за изтегляне на различна версия, когато неуспешната версия е била взета от интерактивно търсене",
|
|
||||||
"DownloadClientDelugeSettingsDirectoryHelpText": "Незадължителна локация за изтеглянията, оставете празно, за да използвате мястото по подразбиране на Deluge",
|
|
||||||
"CustomFormatsSettingsTriggerInfo": "Персонализиран формат ще бъде приложен към издание или файл, когато съвпада с поне един от всеки от избраните различни типове условия.",
|
|
||||||
"AutomaticAdd": "Автоматично добавяне",
|
|
||||||
"BlocklistOnly": "Само списък за блокиране",
|
|
||||||
"BlocklistOnlyHint": "Списък за блокиране без търсене на заместител",
|
|
||||||
"DownloadClientDelugeSettingsDirectoryCompleted": "Директория за вече завършените изтегляния",
|
|
||||||
"DownloadClientDelugeSettingsDirectoryCompletedHelpText": "Незадължителна локация за преместване на вече завършените изтегляния, оставете празно, за да използвате мястото по подразбиране на Deluge",
|
|
||||||
"Library": "Библиотека",
|
|
||||||
"ApplicationURL": "URL адрес на приложението",
|
|
||||||
"ApplicationUrlHelpText": "Външният URL на това приложение, включително http(s)://, порт и базов URL",
|
|
||||||
"CustomFormatsSpecificationFlag": "Флаг",
|
|
||||||
"BypassIfAboveCustomFormatScore": "Пропусни, ако е над рейтинга на персонализирания формат",
|
|
||||||
"AppUpdated": "{appName} Актуализиран",
|
|
||||||
"AppUpdatedVersion": "{appName} е актуализиранa до версия `{version}`, за да получите най-новите промени, ще трябва да презаредите {appName}",
|
|
||||||
"CatalogNumber": "каталожен номер",
|
|
||||||
"AutoAdd": "Автоматично добавяне",
|
|
||||||
"CustomFormatsSpecificationRegularExpression": "Регулярни изрази",
|
|
||||||
"CustomFormatsSpecificationRegularExpressionHelpText": "Персонализираният формат RegEx не е чувствителен към главни и малки букви",
|
|
||||||
"Label": "Етикет",
|
|
||||||
"AutomaticUpdatesDisabledDocker": "Автоматичните актуализации не се поддържат директно при използване на механизма за актуализация на Docker. Ще трябва да актуализирате Image-a на контейнера извън {appName} или да използвате скрипт",
|
|
||||||
"NoCutoffUnmetItems": "Няма неизпълнени елементи за прекъсване",
|
|
||||||
"Publisher": "Издател",
|
|
||||||
"Series": "Сериали",
|
|
||||||
"Theme": "Тема",
|
|
||||||
"BypassIfAboveCustomFormatScoreHelpText": "Активиране на пропускане, когато изданието има резултат, по-висок от конфигурирания минимален резултат за потребителски формат",
|
|
||||||
"MinimumCustomFormatScoreHelpText": "Минимална резултат на персонализирания формат, необходима за пропускане на забавянето за предпочитания протокол",
|
|
||||||
"DownloadClientDelugeSettingsDirectory": "Директория за изтегляне",
|
|
||||||
"AuthenticationMethodHelpTextWarning": "Моля, изберете валиден метод за удостоверяване",
|
|
||||||
"AuthenticationMethod": "Метод за удостоверяване",
|
|
||||||
"AuthenticationRequiredHelpText": "Променете за кои заявки се изисква удостоверяване. Не променяйте, освен ако не разбирате рисковете.",
|
|
||||||
"AuthenticationRequired": "Изисква се удостоверяване",
|
|
||||||
"AuthenticationRequiredPasswordHelpTextWarning": "Въведете нова парола",
|
|
||||||
"ApplyChanges": "Прилагане на промените",
|
|
||||||
"AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Потвърдете новата парола",
|
|
||||||
"AddNewAuthorRootFolderHelpText": "Подпапката „{0}“ ще бъде създадена автоматично",
|
|
||||||
"AddRootFolder": "Добавяне на коренна папка"
|
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user