Compare commits

...

57 Commits

Author SHA1 Message Date
Stevie Robinson 4dc3d69a11 Fix RemoveHelpTextWarning > RemoveFromDownloadClientHelpTextWarning
(cherry picked from commit 901b6d20841bfcb2a3724fe27b0fbddf5e41d669)
2023-08-12 04:21:38 +00:00
Weblate 695781dde5 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/
Translation: Servarr/Readarr
2023-08-11 21:02:05 +03:00
Servarr 4e8ddd3018 Automated API Docs update [skip ci] 2023-08-11 20:04:43 +03:00
Bogdan eb67231a45 New: Show successful grabs in Interactive Search with green icon
(cherry picked from commit 366b2b8b52d8375f1f41719a09893136009a5b48)

Closes #2780
2023-08-11 19:51:21 +03:00
Mark McDowall 3d3a458828 New: Add additional logging when renaming extra files
(cherry picked from commit 1ae0dc81f73ef74078f07fd5536a7d9058df649d)

Closes #2782
2023-08-11 19:48:46 +03:00
Bogdan a11930a03f add @types/lodash 2023-08-11 19:40:21 +03:00
Bogdan abaf39d67e Add simplified translations 2023-08-11 19:40:01 +03:00
Bogdan 894a5943e4 Simplify column translations
(cherry picked from commit 551ea18caf50353c4c8dbeba5e42d266dbbfb54d)

Closes #2759
2023-08-11 19:14:44 +03:00
Bogdan be26647afb New: More translations for columns
(cherry picked from commit aee8579d1823b7dfb94c0055fe33b5fb5a7fbf17)

Towards #2733
2023-08-11 18:56:33 +03:00
Mark McDowall b319a4bce7 Fixed: Translations for columns
(cherry picked from commit 6d53d2a153a98070c42d0619c15902b6bd5dfab4)

Closes #2702
2023-08-11 18:53:50 +03:00
Mark McDowall f03fd7e95e Fixed: Improve translation loading
(cherry picked from commit 73c5ec1da4dd00301e1b0dddbcea37590a99b045)

Closes #2699
2023-08-11 18:52:34 +03:00
Mark McDowall 7f25a3c4b1 UI loading improvements
Fixed: Caching for dynamically loaded JS files
Fixed: Incorrect caching of initialize.js
(cherry picked from commit f0cb5b81f140c67fa84162e094cc4e0f3476f5da)

Closes #2690
Closes #2696
2023-08-11 18:21:47 +03:00
Weblate e3247dc505 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Ivan Mazzoli <dreadtank27@gmail.com>
Co-authored-by: Nir Israel Hen <nirisraelh@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: byakurau <byakurau1@gmail.com>
Co-authored-by: wilfriedarma <wilfriedarma.collet@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ro/
Translation: Servarr/Readarr
2023-08-10 19:59:14 +03:00
Weblate 3677fd6d34 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Ivan Mazzoli <dreadtank27@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: byakurau <byakurau1@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translation: Servarr/Readarr
2023-08-10 19:58:44 +03:00
Bogdan 4f6901b1ff Fixed: Ensure failing providers are marked as failed when testing all
(cherry picked from commit f6c05d4456a5667398319e249614e2eed115621e)
2023-08-10 19:58:25 +03:00
Bogdan ce820f6f73 Fixed: Detect Docker when using control group v2 2023-08-09 10:48:37 +03:00
Bogdan 53e6cb24b7 Bump version to 0.3.2 2023-08-06 19:27:59 +03:00
Bogdan 7c1ca8acc1 New: Health check for indexers with invalid download client
(cherry picked from commit 377fce6fe15c0875c4bd33f1371a31af79c9310c)

Closes #2760
2023-08-06 19:25:00 +03:00
Bogdan 5e9e578101 Ensure path is valid before watching it
(cherry picked from commit 1245b2c58b5a1b5fb4aee9a4f974ecfb131de2bd)
2023-08-06 19:24:58 +03:00
Bogdan 156407c541 Add @types/redux-actions 2023-08-06 19:12:55 +03:00
Mark McDowall 1ef6c60318 Sync Popover with upstream
(cherry picked from commit bdcfef80d627e777d7932c54cda04cbe7c656ffc)
2023-08-06 19:12:04 +03:00
servarr[bot] 73b3b1848b Filter user issues from Sentry
(cherry picked from commit 03d361f5537bfc0caba1b86085f974570942fdbc)

Co-authored-by: Qstick <qstick@gmail.com>
2023-08-05 21:56:25 +03:00
Weblate 33fbd95707 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Albert <zuozl1992@foxmail.com>
Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Magnus <magnus.fladvad@gmail.com>
Co-authored-by: Stjepan <stjepstjepanovic@gmail.com>
Co-authored-by: Thirrian <matthiaslantermann@gmail.com>
Co-authored-by: stormaac <yxc.frank@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/hr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_CN/
Translation: Servarr/Readarr
2023-08-03 22:55:45 +03:00
Mark McDowall 704635f758 New: Book interactive search modal size
(cherry picked from commit 1f619e27f1e8905bc96ce54c483171469d204650)

Closes #2243
2023-08-01 18:32:14 +03:00
Bogdan 263e807de2 Ensure yarn packages are installed when running only LintUI 2023-07-31 08:33:52 +03:00
Mark McDowall 9ac9bd25c1 Re-order frontend build steps
(cherry picked from commit 97ad6682f7d54af8886144bc5a179fa7242f1f1f)
2023-07-31 08:01:50 +03:00
Bogdan 4ead5186ae Bump version to 0.3.1 2023-07-30 09:12:56 +03:00
Bogdan dea797c375 Fixed: (UI) Ensure root folders are populated in Author Editor 2023-07-30 05:10:21 +03:00
PearsonFlyer 58ba24762b Fixed: Correctly calculate books count on Author page
Closes #1931
2023-07-30 05:10:07 +03:00
Servarr fbd7b4fe33 Automated API Docs update [skip ci] 2023-07-30 05:09:25 +03:00
Mark McDowall fee7fbbff6 New: Add result to commands to report commands that did not complete successfully
(cherry picked from commit 103ce3def4636ef891e72bd687ef8f46b5125233)
2023-07-30 03:44:14 +03:00
Taloth Saldono 18253a298e Log Goodreads connection failures with more info.
(cherry picked from commit 6672650b6b5e152e82fb3ad38a0a158d66c0b83d)
2023-07-30 03:44:14 +03:00
Bogdan 22f92150c3 Ensure original data is shown when no matches are made 2023-07-29 18:32:54 +03:00
Bogdan 4d7a762ee8 Fix book tests 2023-07-29 14:59:47 +03:00
Stevie Robinson b11517e2ac Extend InlineMarkdown to handle code blocks in backticks
(cherry picked from commit e1c5533efa397632becc606c17232f97055e371b)
2023-07-29 09:59:50 +03:00
Bogdan d5af254f47 Fix AuthorLookupFixture 2023-07-29 09:04:22 +03:00
Bogdan f09da06f80 More test fixes 2023-07-29 07:48:47 +03:00
Bogdan d3443510b4 Rename formatPreferredWordScore to formatCustomFormatScore
Closes #2731
2023-07-29 06:35:19 +03:00
Bogdan d73eb1b5f9 Validation for Custom Format specifications
Co-authored-by: Qstick <qstick@gmail.com>
(cherry picked from commit 3d6cf24d7c91f8ff697c34264c249f7450894106)

Closes #2726
2023-07-29 06:32:29 +03:00
Bogdan 39778a95bf Dedupe releases based on indexer priority
(cherry picked from commit 38c717bcef6fa5fcd2ff1c7901639eb888a94a8a)

Closes #2727
2023-07-29 06:28:23 +03:00
Taloth Saldono 9fccca1154 Fixed up some errors and do the guid cache fix on the module instead of backend coz that would cause other issues.
(cherry picked from commit 8eaab46488f00a74197c517c6ef773626aec5173)
2023-07-29 06:27:34 +03:00
Mark McDowall e165663616 Fixed: Sorting in Interactive search duplicates results
(cherry picked from commit a6637b2911f7818e596c1518e94bd111cff0120b)

Closes #739
Closes #743
2023-07-29 06:27:31 +03:00
Bogdan b49d2312ab Fixed: Check only enabled Jackett indexers for '/all' endpoint
(cherry picked from commit ae3dd5730e05c5229e7f7092f15c33859524863b)

Closes #2730
2023-07-29 05:38:15 +03:00
Bogdan 52221c7cf4 Fixed: Ensure failing indexers are marked as failed when testing all
(cherry picked from commit b407eba61284d5fb855df6a2868805853aa6f448)

Closes #2735
2023-07-29 05:36:23 +03:00
bakerboy448 ad7b110a0b New: Use better page size for Newznab/Torznab (up to 100) when supported by the indexer
(cherry picked from commit ddb25b109575cc378462a1c3a64705f2003f01f0)

Closes #2181
2023-07-29 05:33:52 +03:00
Weblate b04b483f86 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Fixer <ygj59783@zslsz.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ro/
Translation: Servarr/Readarr
2023-07-28 12:55:31 +03:00
Bogdan b79941e0a1 Fix tests 2023-07-26 14:00:10 +03:00
Weblate 84d47b1f23 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Fixer <ygj59783@zslsz.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ro/
Translation: Servarr/Readarr
2023-07-26 07:53:53 +03:00
Weblate 17df4d47fb Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: SHUAI.W <x@ousui.org>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_CN/
Translation: Servarr/Readarr
2023-07-25 01:04:44 +03:00
Bogdan b9f89dddc9 Bump version to 0.3.0 2023-07-23 09:38:17 +03:00
Weblate e3fc469cd3 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/
Translation: Servarr/Readarr
2023-07-23 05:05:02 +03:00
Bogdan 4304685a65 Add support for deprecated values in field select options
(cherry picked from commit d9786887f3fe30ef60ad9c50b3272bf60dfef309)

Closes #2718
2023-07-23 05:03:40 +03:00
Bogdan 7d77b1fbe5 Trim spaces from a split list in GetValueConverter 2023-07-23 05:02:18 +03:00
Bogdan 1989174801 Fix typo in SkipRedownload
Closes #2711
2023-07-23 05:01:08 +03:00
Bogdan ac4ae9bb4d Fixed: Ensure Monitoring Options resets to No Change
(cherry picked from commit 180153cd8440df88c9aa5694c67c6cae537dc595)
2023-07-23 04:52:30 +03:00
Bogdan f399d27470 Cache busting for CSS files
(cherry picked from commit 38f263931ff8faba050762abe5fb692a5bc0d515)
2023-07-23 03:03:51 +03:00
Weblate c5fd2e3aa0 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translation: Servarr/Readarr
2023-07-21 13:37:45 +03:00
148 changed files with 1423 additions and 716 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ variables:
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '0.2.4'
majorVersion: '0.3.2'
minorVersion: $[counter('minorVersion', 1)]
readarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(readarrVersion)'
+5 -6
View File
@@ -391,20 +391,19 @@ then
fi
fi
if [ "$FRONTEND" = "YES" ];
if [[ "$LINT" = "YES" || "$FRONTEND" = "YES" ]];
then
YarnInstall
RunWebpack
fi
if [ "$LINT" = "YES" ];
then
if [ -z "$FRONTEND" ];
then
YarnInstall
LintUI
fi
LintUI
if [ "$FRONTEND" = "YES" ];
then
RunWebpack
fi
if [ "$PACKAGES" = "YES" ];
+5 -3
View File
@@ -36,7 +36,7 @@ module.exports = (env) => {
},
entry: {
index: 'index.js'
index: 'index.ts'
},
resolve: {
@@ -91,13 +91,15 @@ module.exports = (env) => {
}),
new MiniCssExtractPlugin({
filename: 'Content/styles.css'
filename: 'Content/styles.css',
chunkFilename: 'Content/[id]-[chunkhash].css'
}),
new HtmlWebpackPlugin({
template: 'frontend/src/index.ejs',
filename: 'index.html',
publicPath: '/'
publicPath: '/',
inject: false
}),
new FileManagerPlugin({
@@ -9,7 +9,7 @@ import Link from 'Components/Link/Link';
import { icons } from 'Helpers/Props';
import formatDateTime from 'Utilities/Date/formatDateTime';
import formatAge from 'Utilities/Number/formatAge';
import formatPreferredWordScore from 'Utilities/Number/formatPreferredWordScore';
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
import translate from 'Utilities/String/translate';
import styles from './HistoryDetails.css';
@@ -108,7 +108,7 @@ function HistoryDetails(props) {
customFormatScore && customFormatScore !== '0' ?
<DescriptionListItem
title={translate('CustomFormatScore')}
data={formatPreferredWordScore(customFormatScore)}
data={formatCustomFormatScore(customFormatScore)}
/> :
null
}
@@ -225,7 +225,7 @@ function HistoryDetails(props) {
customFormatScore && customFormatScore !== '0' ?
<DescriptionListItem
title={translate('CustomFormatScore')}
data={formatPreferredWordScore(customFormatScore)}
data={formatCustomFormatScore(customFormatScore)}
/> :
null
}
@@ -271,7 +271,7 @@ function HistoryDetails(props) {
customFormatScore && customFormatScore !== '0' ?
<DescriptionListItem
title={translate('CustomFormatScore')}
data={formatPreferredWordScore(customFormatScore)}
data={formatCustomFormatScore(customFormatScore)}
/> :
null
}
+2 -2
View File
@@ -10,7 +10,7 @@ import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, tooltipPositions } from 'Helpers/Props';
import formatPreferredWordScore from 'Utilities/Number/formatPreferredWordScore';
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
import HistoryDetailsModal from './Details/HistoryDetailsModal';
import HistoryEventTypeCell from './HistoryEventTypeCell';
import styles from './HistoryRow.css';
@@ -180,7 +180,7 @@ class HistoryRow extends Component {
className={styles.customFormatScore}
>
<Tooltip
anchor={formatPreferredWordScore(
anchor={formatCustomFormatScore(
customFormatScore,
customFormats.length
)}
+4 -4
View File
@@ -18,7 +18,7 @@ import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
import formatBytes from 'Utilities/Number/formatBytes';
import formatPreferredWordScore from 'Utilities/Number/formatPreferredWordScore';
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
import translate from 'Utilities/String/translate';
import QueueStatusCell from './QueueStatusCell';
import RemoveQueueItemModal from './RemoveQueueItemModal';
@@ -46,14 +46,14 @@ class QueueRow extends Component {
this.setState({ isRemoveQueueItemModalOpen: true });
};
onRemoveQueueItemModalConfirmed = (blocklist, skipredownload) => {
onRemoveQueueItemModalConfirmed = (blocklist, skipRedownload) => {
const {
onRemoveQueueItemPress,
onQueueRowModalOpenOrClose
} = this.props;
onQueueRowModalOpenOrClose(false);
onRemoveQueueItemPress(blocklist, skipredownload);
onRemoveQueueItemPress(blocklist, skipRedownload);
this.setState({ isRemoveQueueItemModalOpen: false });
};
@@ -232,7 +232,7 @@ class QueueRow extends Component {
className={styles.customFormatScore}
>
<Tooltip
anchor={formatPreferredWordScore(
anchor={formatCustomFormatScore(
customFormatScore,
customFormats.length
)}
@@ -23,7 +23,7 @@ class RemoveQueueItemModal extends Component {
this.state = {
remove: true,
blocklist: false,
skipredownload: false
skipRedownload: false
};
}
@@ -34,7 +34,7 @@ class RemoveQueueItemModal extends Component {
this.setState({
remove: true,
blocklist: false,
skipredownload: false
skipRedownload: false
});
};
@@ -49,8 +49,8 @@ class RemoveQueueItemModal extends Component {
this.setState({ blocklist: value });
};
onSkipReDownloadChange = ({ value }) => {
this.setState({ skipredownload: value });
onSkipRedownloadChange = ({ value }) => {
this.setState({ skipRedownload: value });
};
onRemoveConfirmed = () => {
@@ -76,7 +76,7 @@ class RemoveQueueItemModal extends Component {
isPending
} = this.props;
const { remove, blocklist, skipredownload } = this.state;
const { remove, blocklist, skipRedownload } = this.state;
return (
<Modal
@@ -108,7 +108,7 @@ class RemoveQueueItemModal extends Component {
type={inputTypes.CHECK}
name="remove"
value={remove}
helpTextWarning={translate('RemoveHelpTextWarning')}
helpTextWarning={translate('RemoveFromDownloadClientHelpTextWarning')}
isDisabled={!canIgnore}
onChange={this.onRemoveChange}
/>
@@ -137,10 +137,10 @@ class RemoveQueueItemModal extends Component {
</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="skipredownload"
value={skipredownload}
helpText={translate('SkipredownloadHelpText')}
onChange={this.onSkipReDownloadChange}
name="skipRedownload"
value={skipRedownload}
helpText={translate('SkipRedownloadHelpText')}
onChange={this.onSkipRedownloadChange}
/>
</FormGroup>
}
@@ -24,7 +24,7 @@ class RemoveQueueItemsModal extends Component {
this.state = {
remove: true,
blocklist: false,
skipredownload: false
skipRedownload: false
};
}
@@ -35,7 +35,7 @@ class RemoveQueueItemsModal extends Component {
this.setState({
remove: true,
blocklist: false,
skipredownload: false
skipRedownload: false
});
};
@@ -50,8 +50,8 @@ class RemoveQueueItemsModal extends Component {
this.setState({ blocklist: value });
};
onSkipReDownloadChange = ({ value }) => {
this.setState({ skipredownload: value });
onSkipRedownloadChange = ({ value }) => {
this.setState({ skipRedownload: value });
};
onRemoveConfirmed = () => {
@@ -77,7 +77,7 @@ class RemoveQueueItemsModal extends Component {
allPending
} = this.props;
const { remove, blocklist, skipredownload } = this.state;
const { remove, blocklist, skipRedownload } = this.state;
return (
<Modal
@@ -138,10 +138,10 @@ class RemoveQueueItemsModal extends Component {
</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="skipredownload"
value={skipredownload}
helpText={translate('SkipredownloadHelpText')}
onChange={this.onSkipReDownloadChange}
name="skipRedownload"
value={skipRedownload}
helpText={translate('SkipRedownloadHelpText')}
onChange={this.onSkipRedownloadChange}
/>
</FormGroup>
}
+3 -4
View File
@@ -7,13 +7,13 @@ import PageConnector from 'Components/Page/PageConnector';
import ApplyTheme from './ApplyTheme';
import AppRoutes from './AppRoutes';
function App({ store, history, hasTranslationsError }) {
function App({ store, history }) {
return (
<DocumentTitle title={window.Readarr.instanceName}>
<Provider store={store}>
<ConnectedRouter history={history}>
<ApplyTheme>
<PageConnector hasTranslationsError={hasTranslationsError}>
<PageConnector>
<AppRoutes app={App} />
</PageConnector>
</ApplyTheme>
@@ -25,8 +25,7 @@ function App({ store, history, hasTranslationsError }) {
App.propTypes = {
store: PropTypes.object.isRequired,
history: PropTypes.object.isRequired,
hasTranslationsError: PropTypes.bool.isRequired
history: PropTypes.object.isRequired
};
export default App;
@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import MoveAuthorModal from 'Author/MoveAuthor/MoveAuthorModal';
import MetadataProfileSelectInputConnector from 'Components/Form/MetadataProfileSelectInputConnector';
import MonitorNewItemsSelectInput from 'Components/Form/MonitorNewItemsSelectInput';
@@ -9,6 +10,7 @@ import SelectInput from 'Components/Form/SelectInput';
import SpinnerButton from 'Components/Link/SpinnerButton';
import PageContentFooter from 'Components/Page/PageContentFooter';
import { kinds } from 'Helpers/Props';
import { fetchRootFolders } from 'Store/Actions/Settings/rootFolders';
import translate from 'Utilities/String/translate';
import AuthorEditorFooterLabel from './AuthorEditorFooterLabel';
import DeleteAuthorModal from './Delete/DeleteAuthorModal';
@@ -17,6 +19,10 @@ import styles from './AuthorEditorFooter.css';
const NO_CHANGE = 'noChange';
const mapDispatchToProps = {
dispatchFetchRootFolders: fetchRootFolders
};
class AuthorEditorFooter extends Component {
//
@@ -39,6 +45,13 @@ class AuthorEditorFooter extends Component {
};
}
//
// Lifecycle
componentDidMount() {
this.props.dispatchFetchRootFolders();
}
componentDidUpdate(prevProps) {
const {
isSaving,
@@ -341,7 +354,8 @@ AuthorEditorFooter.propTypes = {
showMetadataProfile: PropTypes.bool.isRequired,
onSaveSelected: PropTypes.func.isRequired,
onOrganizeAuthorPress: PropTypes.func.isRequired,
onRetagAuthorPress: PropTypes.func.isRequired
onRetagAuthorPress: PropTypes.func.isRequired,
dispatchFetchRootFolders: PropTypes.func.isRequired
};
export default AuthorEditorFooter;
export default connect(undefined, mapDispatchToProps)(AuthorEditorFooter);
@@ -14,14 +14,39 @@ import { inputTypes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
const nameOptions = [
{ key: 'firstLast', value: translate('NameFirstLast') },
{ key: 'lastFirst', value: translate('NameLastFirst') }
{
key: 'firstLast',
get value() {
return translate('NameFirstLast');
}
},
{
key: 'lastFirst',
get value() {
return translate('NameLastFirst');
}
}
];
const posterSizeOptions = [
{ key: 'small', value: 'Small' },
{ key: 'medium', value: 'Medium' },
{ key: 'large', value: 'Large' }
{
key: 'small',
get value() {
return translate('Small');
}
},
{
key: 'medium',
get value() {
return translate('Medium');
}
},
{
key: 'large',
get value() {
return translate('Large');
}
}
];
class AuthorIndexOverviewOptionsModalContent extends Component {
@@ -14,15 +14,45 @@ import { inputTypes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
const posterSizeOptions = [
{ key: 'small', value: 'Small' },
{ key: 'medium', value: 'Medium' },
{ key: 'large', value: 'Large' }
{
key: 'small',
get value() {
return translate('Small');
}
},
{
key: 'medium',
get value() {
return translate('Medium');
}
},
{
key: 'large',
get value() {
return translate('Large');
}
}
];
const nameOptions = [
{ key: 'no', value: translate('NoName') },
{ key: 'firstLast', value: translate('NameFirstLast') },
{ key: 'lastFirst', value: translate('NameLastFirst') }
{
key: 'no',
get value() {
return translate('NoName');
}
},
{
key: 'firstLast',
get value() {
return translate('NameFirstLast');
}
},
{
key: 'lastFirst',
get value() {
return translate('NameLastFirst');
}
}
];
class AuthorIndexPosterOptionsModalContent extends Component {
@@ -17,8 +17,8 @@ function AuthorIndexProgressBar(props) {
detailedProgressBar
} = props;
const progress = bookCount ? bookFileCount / bookCount * 100 : 100;
const text = `${bookFileCount} / ${bookCount}`;
const progress = bookCount ? bookCount / totalBookCount * 100 : 100;
const text = `${bookCount} / ${totalBookCount}`;
return (
<ProgressBar
@@ -297,7 +297,7 @@ class AuthorIndexRow extends Component {
progress={progress}
kind={getProgressBarKind(status, monitored, progress)}
showText={true}
text={`${bookFileCount} / ${bookCount}`}
text={`${bookCount} / ${totalBookCount}`}
title={translate('BookFileCountBookCountTotalTotalBookCountInterp', [bookFileCount, bookCount, totalBookCount])}
width={125}
/>
@@ -7,8 +7,18 @@ import { inputTypes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
const nameOptions = [
{ key: 'firstLast', value: translate('NameFirstLast') },
{ key: 'lastFirst', value: translate('NameLastFirst') }
{
key: 'firstLast',
get value() {
return translate('NameFirstLast');
}
},
{
key: 'lastFirst',
get value() {
return translate('NameLastFirst');
}
}
];
class AuthorIndexTableOptions extends Component {
@@ -6,4 +6,5 @@
.statusIcon {
width: 20px !important;
text-align: center;
}
@@ -33,7 +33,7 @@ class MonitoringOptionsModalContent extends Component {
const {
isSaving,
saveError
} = prevProps;
} = this.props;
if (prevProps.isSaving && !isSaving && !saveError) {
this.setState({
@@ -1,6 +1,7 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import BookInteractiveSearchModalContent from './BookInteractiveSearchModalContent';
function BookInteractiveSearchModal(props) {
@@ -14,6 +15,7 @@ function BookInteractiveSearchModal(props) {
return (
<Modal
isOpen={isOpen}
size={sizes.EXTRA_LARGE}
closeOnBackgroundClick={false}
onModalClose={onModalClose}
>
+1 -1
View File
@@ -30,7 +30,7 @@ class BookshelfFooter extends Component {
const {
isSaving,
saveError
} = prevProps;
} = this.props;
if (prevProps.isSaving && !isSaving && !saveError) {
this.setState({
@@ -206,9 +206,11 @@ class FilterBuilderRow extends Component {
const selectedFilterBuilderProp = this.selectedFilterBuilderProp;
const keyOptions = filterBuilderProps.map((availablePropFilter) => {
const { name, label } = availablePropFilter;
return {
key: availablePropFilter.name,
value: availablePropFilter.label
key: name,
value: typeof label === 'function' ? label() : label
};
}).sort((a, b) => a.value.localeCompare(b.value));
+2 -2
View File
@@ -61,7 +61,7 @@ class SelectInput extends Component {
value={key}
{...otherOptionProps}
>
{optionValue}
{typeof optionValue === 'function' ? optionValue() : optionValue}
</option>
);
})
@@ -75,7 +75,7 @@ SelectInput.propTypes = {
className: PropTypes.string,
disabledClassName: PropTypes.string,
name: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.func]).isRequired,
values: PropTypes.arrayOf(PropTypes.object).isRequired,
isDisabled: PropTypes.bool,
hasError: PropTypes.bool,
+2 -2
View File
@@ -41,7 +41,7 @@ class Icon extends PureComponent {
return (
<span
className={containerClassName}
title={title}
title={typeof title === 'function' ? title() : title}
>
{icon}
</span>
@@ -58,7 +58,7 @@ Icon.propTypes = {
name: PropTypes.object.isRequired,
kind: PropTypes.string.isRequired,
size: PropTypes.number.isRequired,
title: PropTypes.string,
title: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
isSpinning: PropTypes.bool.isRequired,
fixedWidth: PropTypes.bool.isRequired
};
@@ -13,24 +13,51 @@ class InlineMarkdown extends Component {
data
} = this.props;
// For now only replace links
// For now only replace links or code blocks (not both)
const markdownBlocks = [];
if (data) {
const regex = RegExp(/\[(.+?)\]\((.+?)\)/g);
const linkRegex = RegExp(/\[(.+?)\]\((.+?)\)/g);
let endIndex = 0;
let match = null;
while ((match = regex.exec(data)) !== null) {
while ((match = linkRegex.exec(data)) !== null) {
if (match.index > endIndex) {
markdownBlocks.push(data.substr(endIndex, match.index - endIndex));
}
markdownBlocks.push(<Link key={match.index} to={match[2]}>{match[1]}</Link>);
endIndex = match.index + match[0].length;
}
if (endIndex !== data.length) {
if (endIndex !== data.length && markdownBlocks.length > 0) {
markdownBlocks.push(data.substr(endIndex, data.length - endIndex));
}
const codeRegex = RegExp(/(?=`)`(?!`)[^`]*(?=`)`(?!`)/g);
endIndex = 0;
match = null;
let matchedCode = false;
while ((match = codeRegex.exec(data)) !== null) {
matchedCode = true;
if (match.index > endIndex) {
markdownBlocks.push(data.substr(endIndex, match.index - endIndex));
}
markdownBlocks.push(<code key={`code-${match.index}`}>{match[0].substring(1, match[0].length - 1)}</code>);
endIndex = match.index + match[0].length;
}
if (endIndex !== data.length && markdownBlocks.length > 0 && matchedCode) {
markdownBlocks.push(data.substr(endIndex, data.length - endIndex));
}
if (markdownBlocks.length === 0) {
markdownBlocks.push(data);
}
}
return <span className={className}>{markdownBlocks}</span>;
@@ -32,7 +32,7 @@ class FilterMenuContent extends Component {
selectedFilterKey={selectedFilterKey}
onPress={onFilterSelect}
>
{filter.label}
{typeof filter.label === 'function' ? filter.label() : filter.label}
</FilterMenuItem>
);
})
+4 -4
View File
@@ -7,7 +7,7 @@ function ErrorPage(props) {
const {
version,
isLocalStorageSupported,
hasTranslationsError,
translationsError,
authorError,
customFiltersError,
tagsError,
@@ -21,8 +21,8 @@ function ErrorPage(props) {
if (!isLocalStorageSupported) {
errorMessage = 'Local Storage is not supported or disabled. A plugin or private browsing may have disabled it.';
} else if (hasTranslationsError) {
errorMessage = 'Failed to load translations from API';
} else if (translationsError) {
errorMessage = getErrorMessage(translationsError, 'Failed to load translations from API');
} else if (authorError) {
errorMessage = getErrorMessage(authorError, 'Failed to load author from API');
} else if (customFiltersError) {
@@ -55,7 +55,7 @@ function ErrorPage(props) {
ErrorPage.propTypes = {
version: PropTypes.string.isRequired,
isLocalStorageSupported: PropTypes.bool.isRequired,
hasTranslationsError: PropTypes.bool.isRequired,
translationsError: PropTypes.object,
authorError: PropTypes.object,
customFiltersError: PropTypes.object,
tagsError: PropTypes.object,
+20 -10
View File
@@ -3,7 +3,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { createSelector } from 'reselect';
import { saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
import { fetchTranslations, saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
import { fetchAuthor } from 'Store/Actions/authorActions';
import { fetchBooks } from 'Store/Actions/bookActions';
import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
@@ -52,6 +52,7 @@ const selectIsPopulated = createSelector(
(state) => state.settings.metadataProfiles.isPopulated,
(state) => state.settings.importLists.isPopulated,
(state) => state.system.status.isPopulated,
(state) => state.app.translations.isPopulated,
(
customFiltersIsPopulated,
tagsIsPopulated,
@@ -60,7 +61,8 @@ const selectIsPopulated = createSelector(
qualityProfilesIsPopulated,
metadataProfilesIsPopulated,
importListsIsPopulated,
systemStatusIsPopulated
systemStatusIsPopulated,
translationsIsPopulated
) => {
return (
customFiltersIsPopulated &&
@@ -70,7 +72,8 @@ const selectIsPopulated = createSelector(
qualityProfilesIsPopulated &&
metadataProfilesIsPopulated &&
importListsIsPopulated &&
systemStatusIsPopulated
systemStatusIsPopulated &&
translationsIsPopulated
);
}
);
@@ -84,6 +87,7 @@ const selectErrors = createSelector(
(state) => state.settings.metadataProfiles.error,
(state) => state.settings.importLists.error,
(state) => state.system.status.error,
(state) => state.app.translations.error,
(
customFiltersError,
tagsError,
@@ -92,7 +96,8 @@ const selectErrors = createSelector(
qualityProfilesError,
metadataProfilesError,
importListsError,
systemStatusError
systemStatusError,
translationsError
) => {
const hasError = !!(
customFiltersError ||
@@ -102,7 +107,8 @@ const selectErrors = createSelector(
qualityProfilesError ||
metadataProfilesError ||
importListsError ||
systemStatusError
systemStatusError ||
translationsError
);
return {
@@ -114,7 +120,8 @@ const selectErrors = createSelector(
qualityProfilesError,
metadataProfilesError,
importListsError,
systemStatusError
systemStatusError,
translationsError
};
}
);
@@ -176,6 +183,9 @@ function createMapDispatchToProps(dispatch, props) {
dispatchFetchStatus() {
dispatch(fetchStatus());
},
dispatchFetchTranslations() {
dispatch(fetchTranslations());
},
onResize(dimensions) {
dispatch(saveDimensions(dimensions));
},
@@ -210,6 +220,7 @@ class PageConnector extends Component {
this.props.dispatchFetchImportLists();
this.props.dispatchFetchUISettings();
this.props.dispatchFetchStatus();
this.props.dispatchFetchTranslations();
}
}
@@ -225,7 +236,6 @@ class PageConnector extends Component {
render() {
const {
hasTranslationsError,
isPopulated,
hasError,
dispatchFetchAuthor,
@@ -237,15 +247,15 @@ class PageConnector extends Component {
dispatchFetchImportLists,
dispatchFetchUISettings,
dispatchFetchStatus,
dispatchFetchTranslations,
...otherProps
} = this.props;
if (hasTranslationsError || hasError || !this.state.isLocalStorageSupported) {
if (hasError || !this.state.isLocalStorageSupported) {
return (
<ErrorPage
{...this.state}
{...otherProps}
hasTranslationsError={hasTranslationsError}
/>
);
}
@@ -266,7 +276,6 @@ class PageConnector extends Component {
}
PageConnector.propTypes = {
hasTranslationsError: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
hasError: PropTypes.bool.isRequired,
isSidebarVisible: PropTypes.bool.isRequired,
@@ -280,6 +289,7 @@ PageConnector.propTypes = {
dispatchFetchImportLists: PropTypes.func.isRequired,
dispatchFetchUISettings: PropTypes.func.isRequired,
dispatchFetchStatus: PropTypes.func.isRequired,
dispatchFetchTranslations: PropTypes.func.isRequired,
onSidebarVisibleChange: PropTypes.func.isRequired
};
@@ -21,28 +21,28 @@ const SIDEBAR_WIDTH = parseInt(dimensions.sidebarWidth);
const links = [
{
iconName: icons.AUTHOR_CONTINUING,
title: 'Library',
title: () => translate('Library'),
to: '/',
alias: '/authors',
children: [
{
title: 'Authors',
title: () => translate('Authors'),
to: '/authors'
},
{
title: 'Books',
title: () => translate('Books'),
to: '/books'
},
{
title: 'Add New',
title: () => translate('AddNew'),
to: '/add/search'
},
{
title: 'Bookshelf',
title: () => translate('Bookshelf'),
to: '/shelf'
},
{
title: 'Unmapped Files',
title: () => translate('UnmappedFiles'),
to: '/unmapped'
}
]
@@ -50,26 +50,26 @@ const links = [
{
iconName: icons.CALENDAR,
title: 'Calendar',
title: () => translate('Calendar'),
to: '/calendar'
},
{
iconName: icons.ACTIVITY,
title: 'Activity',
title: () => translate('Activity'),
to: '/activity/queue',
children: [
{
title: 'Queue',
title: () => translate('Queue'),
to: '/activity/queue',
statusComponent: QueueStatusConnector
},
{
title: 'History',
title: () => translate('History'),
to: '/activity/history'
},
{
title: 'Blocklist',
title: () => translate('Blocklist'),
to: '/activity/blocklist'
}
]
@@ -77,15 +77,15 @@ const links = [
{
iconName: icons.WARNING,
title: 'Wanted',
title: () => translate('Wanted'),
to: '/wanted/missing',
children: [
{
title: 'Missing',
title: () => translate('Missing'),
to: '/wanted/missing'
},
{
title: 'Cutoff Unmet',
title: () => translate('CutoffUnmet'),
to: '/wanted/cutoffunmet'
}
]
@@ -93,55 +93,55 @@ const links = [
{
iconName: icons.SETTINGS,
title: 'Settings',
title: () => translate('Settings'),
to: '/settings',
children: [
{
title: 'Media Management',
title: () => translate('MediaManagement'),
to: '/settings/mediamanagement'
},
{
title: 'Profiles',
title: () => translate('Profiles'),
to: '/settings/profiles'
},
{
title: 'Quality',
title: () => translate('Quality'),
to: '/settings/quality'
},
{
title: translate('CustomFormats'),
title: () => translate('CustomFormats'),
to: '/settings/customformats'
},
{
title: translate('Indexers'),
title: () => translate('Indexers'),
to: '/settings/indexers'
},
{
title: 'Download Clients',
title: () => translate('DownloadClients'),
to: '/settings/downloadclients'
},
{
title: 'Import Lists',
title: () => translate('ImportLists'),
to: '/settings/importlists'
},
{
title: 'Connect',
title: () => translate('Connect'),
to: '/settings/connect'
},
{
title: 'Metadata',
title: () => translate('Metadata'),
to: '/settings/metadata'
},
{
title: 'Tags',
title: () => translate('Tags'),
to: '/settings/tags'
},
{
title: 'General',
title: () => translate('General'),
to: '/settings/general'
},
{
title: 'UI',
title: () => translate('Ui'),
to: '/settings/ui'
}
]
@@ -149,32 +149,32 @@ const links = [
{
iconName: icons.SYSTEM,
title: 'System',
title: () => translate('System'),
to: '/system/status',
children: [
{
title: 'Status',
title: () => translate('Status'),
to: '/system/status',
statusComponent: HealthStatusConnector
},
{
title: 'Tasks',
title: () => translate('Tasks'),
to: '/system/tasks'
},
{
title: 'Backup',
title: () => translate('Backup'),
to: '/system/backup'
},
{
title: 'Updates',
title: () => translate('Updates'),
to: '/system/updates'
},
{
title: 'Events',
title: () => translate('Events'),
to: '/system/events'
},
{
title: 'Log Files',
title: () => translate('LogFiles'),
to: '/system/logs/files'
}
]
@@ -64,7 +64,7 @@ class PageSidebarItem extends Component {
}
<span className={isChildItem ? styles.noIcon : null}>
{title}
{typeof title === 'function' ? title() : title}
</span>
{
@@ -88,7 +88,7 @@ class PageSidebarItem extends Component {
PageSidebarItem.propTypes = {
iconName: PropTypes.object,
title: PropTypes.string.isRequired,
title: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
to: PropTypes.string.isRequired,
isActive: PropTypes.bool,
isActiveParent: PropTypes.bool,
+3 -1
View File
@@ -1,8 +1,10 @@
import React from 'react';
type PropertyFunction<T> = () => T;
interface Column {
name: string;
label: string | React.ReactNode;
label: string | PropertyFunction<string> | React.ReactNode;
columnLabel?: string;
isSortable?: boolean;
isVisible: boolean;
+1 -1
View File
@@ -107,7 +107,7 @@ function Table(props) {
{...getTableHeaderCellProps(otherProps)}
{...column}
>
{column.label}
{typeof column.label === 'function' ? column.label() : column.label}
</TableHeaderCell>
);
})
@@ -30,6 +30,7 @@ class TableHeaderCell extends Component {
const {
className,
name,
label,
columnLabel,
isSortable,
isVisible,
@@ -53,7 +54,8 @@ class TableHeaderCell extends Component {
{...otherProps}
component="th"
className={className}
title={columnLabel}
label={typeof label === 'function' ? label() : label}
title={typeof columnLabel === 'function' ? columnLabel() : columnLabel}
onPress={this.onPress}
>
{children}
@@ -77,7 +79,8 @@ class TableHeaderCell extends Component {
TableHeaderCell.propTypes = {
className: PropTypes.string,
name: PropTypes.string.isRequired,
columnLabel: PropTypes.string,
label: PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.node]),
columnLabel: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
isSortable: PropTypes.bool,
isVisible: PropTypes.bool,
isModifiable: PropTypes.bool,
@@ -35,7 +35,7 @@ function TableOptionsColumn(props) {
isDisabled={isModifiable === false}
onChange={onVisibleChange}
/>
{label}
{typeof label === 'function' ? label() : label}
</label>
{
@@ -56,7 +56,7 @@ function TableOptionsColumn(props) {
TableOptionsColumn.propTypes = {
name: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
label: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
isVisible: PropTypes.bool.isRequired,
isModifiable: PropTypes.bool.isRequired,
index: PropTypes.number.isRequired,
@@ -112,7 +112,7 @@ class TableOptionsColumnDragSource extends Component {
<TableOptionsColumn
name={name}
label={label}
label={typeof label === 'function' ? label() : label}
isVisible={isVisible}
isModifiable={isModifiable}
index={index}
@@ -138,7 +138,7 @@ class TableOptionsColumnDragSource extends Component {
TableOptionsColumnDragSource.propTypes = {
name: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
label: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
isVisible: PropTypes.bool.isRequired,
isModifiable: PropTypes.bool.isRequired,
index: PropTypes.number.isRequired,
+7 -1
View File
@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React from 'react';
import { tooltipPositions } from 'Helpers/Props';
import Tooltip from './Tooltip';
import styles from './Popover.css';
@@ -30,8 +31,13 @@ function Popover(props) {
}
Popover.propTypes = {
className: PropTypes.string,
bodyClassName: PropTypes.string,
anchor: PropTypes.node.isRequired,
title: PropTypes.string.isRequired,
body: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired
body: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired,
position: PropTypes.oneOf(tooltipPositions.all),
canFlip: PropTypes.bool
};
export default Popover;
@@ -0,0 +1,8 @@
enum TooltipPosition {
Top = 'top',
Right = 'right',
Bottom = 'bottom',
Left = 'left',
}
export default TooltipPosition;
@@ -69,7 +69,7 @@ const columns = [
name: 'customFormats',
label: React.createElement(Icon, {
name: icons.INTERACTIVE,
title: translate('CustomFormat')
title: () => translate('CustomFormat')
}),
isSortable: true,
isVisible: true
@@ -91,9 +91,9 @@ const filterExistingFilesOptions = {
};
const importModeOptions = [
{ key: 'chooseImportMode', value: translate('ChooseImportMethod'), disabled: true },
{ key: 'move', value: translate('MoveFiles') },
{ key: 'copy', value: translate('HardlinkCopyFiles') }
{ key: 'chooseImportMode', value: () => translate('ChooseImportMethod'), disabled: true },
{ key: 'move', value: () => translate('MoveFiles') },
{ key: 'copy', value: () => translate('HardlinkCopyFiles') }
];
const SELECT = 'select';
@@ -56,7 +56,7 @@ const columns = [
name: 'customFormatScore',
label: React.createElement(Icon, {
name: icons.SCORE,
title: translate('CustomFormatScore')
title: () => translate('CustomFormatScore')
}),
isSortable: true,
isVisible: true
@@ -15,7 +15,7 @@ import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import formatDateTime from 'Utilities/Date/formatDateTime';
import formatAge from 'Utilities/Number/formatAge';
import formatBytes from 'Utilities/Number/formatBytes';
import formatPreferredWordScore from 'Utilities/Number/formatPreferredWordScore';
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
import translate from 'Utilities/String/translate';
import Peers from './Peers';
import styles from './InteractiveSearchRow.css';
@@ -32,6 +32,18 @@ function getDownloadIcon(isGrabbing, isGrabbed, grabError) {
return icons.DOWNLOAD;
}
function getDownloadKind(isGrabbed, grabError, downloadAllowed) {
if (isGrabbed) {
return kinds.SUCCESS;
}
if (grabError || !downloadAllowed) {
return kinds.DANGER;
}
return kinds.DEFAULT;
}
function getDownloadTooltip(isGrabbing, isGrabbed, grabError) {
if (isGrabbing) {
return '';
@@ -172,7 +184,7 @@ class InteractiveSearchRow extends Component {
<TableRowCell className={styles.customFormatScore}>
<Tooltip
anchor={
formatPreferredWordScore(customFormatScore, customFormats.length)
formatCustomFormatScore(customFormatScore, customFormats.length)
}
tooltip={<BookFormats formats={customFormats} />}
position={tooltipPositions.BOTTOM}
@@ -212,7 +224,7 @@ class InteractiveSearchRow extends Component {
{
<SpinnerIconButton
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
kind={grabError || !downloadAllowed ? kinds.DANGER : kinds.DEFAULT}
kind={getDownloadKind(isGrabbed, grabError, downloadAllowed)}
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
isSpinning={isGrabbing}
onPress={downloadAllowed ? this.onGrabPress : this.onConfirmGrabPress}
@@ -27,9 +27,25 @@ interface ManageDownloadClientsEditModalContentProps {
const NO_CHANGE = 'noChange';
const enableOptions = [
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true },
{ key: 'enabled', value: translate('Enabled') },
{ key: 'disabled', value: translate('Disabled') },
{
key: NO_CHANGE,
get value() {
return translate('NoChange');
},
disabled: true,
},
{
key: 'enabled',
get value() {
return translate('Enabled');
},
},
{
key: 'disabled',
get value() {
return translate('Disabled');
},
},
];
function ManageDownloadClientsEditModalContent(
@@ -36,37 +36,37 @@ type OnSelectedChangeCallback = React.ComponentProps<
const COLUMNS = [
{
name: 'name',
label: translate('Name'),
label: () => translate('Name'),
isSortable: true,
isVisible: true,
},
{
name: 'implementation',
label: translate('Implementation'),
label: () => translate('Implementation'),
isSortable: true,
isVisible: true,
},
{
name: 'enable',
label: translate('Enabled'),
label: () => translate('Enabled'),
isSortable: true,
isVisible: true,
},
{
name: 'priority',
label: translate('Priority'),
label: () => translate('Priority'),
isSortable: true,
isVisible: true,
},
{
name: 'removeCompletedDownloads',
label: translate('RemoveCompleted'),
label: () => translate('RemoveCompleted'),
isSortable: true,
isVisible: true,
},
{
name: 'removeFailedDownloads',
label: translate('RemoveFailed'),
label: () => translate('RemoveFailed'),
isSortable: true,
isVisible: true,
},
@@ -72,9 +72,24 @@ function TagsModalContent(props: TagsModalContentProps) {
}, [tags, applyTags, onApplyTagsPress]);
const applyTagsOptions = [
{ key: 'add', value: translate('Add') },
{ key: 'remove', value: translate('Remove') },
{ key: 'replace', value: translate('Replace') },
{
key: 'add',
get value() {
return translate('Add');
},
},
{
key: 'remove',
get value() {
return translate('Remove');
},
},
{
key: 'replace',
get value() {
return translate('Replace');
},
},
];
return (
@@ -27,9 +27,25 @@ interface ManageImportListsEditModalContentProps {
const NO_CHANGE = 'noChange';
const autoAddOptions = [
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true },
{ key: 'enabled', value: translate('Enabled') },
{ key: 'disabled', value: translate('Disabled') },
{
key: NO_CHANGE,
get value() {
return translate('NoChange');
},
disabled: true,
},
{
key: 'enabled',
get value() {
return translate('Enabled');
},
},
{
key: 'disabled',
get value() {
return translate('Disabled');
},
},
];
function ManageImportListsEditModalContent(
@@ -36,43 +36,43 @@ type OnSelectedChangeCallback = React.ComponentProps<
const COLUMNS = [
{
name: 'name',
label: translate('Name'),
label: () => translate('Name'),
isSortable: true,
isVisible: true,
},
{
name: 'implementation',
label: translate('Implementation'),
label: () => translate('Implementation'),
isSortable: true,
isVisible: true,
},
{
name: 'qualityProfileId',
label: translate('QualityProfile'),
label: () => translate('QualityProfile'),
isSortable: true,
isVisible: true,
},
{
name: 'metadataProfileId',
label: translate('MetadataProfile'),
label: () => translate('MetadataProfile'),
isSortable: true,
isVisible: true,
},
{
name: 'rootFolderPath',
label: translate('RootFolder'),
label: () => translate('RootFolder'),
isSortable: true,
isVisible: true,
},
{
name: 'enableAutomaticAdd',
label: translate('AutoAdd'),
label: () => translate('AutoAdd'),
isSortable: true,
isVisible: true,
},
{
name: 'tags',
label: translate('Tags'),
label: () => translate('Tags'),
isSortable: true,
isVisible: true,
},
@@ -70,9 +70,24 @@ function TagsModalContent(props: TagsModalContentProps) {
}, [tags, applyTags, onApplyTagsPress]);
const applyTagsOptions = [
{ key: 'add', value: translate('Add') },
{ key: 'remove', value: translate('Remove') },
{ key: 'replace', value: translate('Replace') },
{
key: 'add',
get value() {
return translate('Add');
},
},
{
key: 'remove',
get value() {
return translate('Remove');
},
},
{
key: 'replace',
get value() {
return translate('Replace');
},
},
];
return (
@@ -27,9 +27,25 @@ interface ManageIndexersEditModalContentProps {
const NO_CHANGE = 'noChange';
const enableOptions = [
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true },
{ key: 'enabled', value: translate('Enabled') },
{ key: 'disabled', value: translate('Disabled') },
{
key: NO_CHANGE,
get value() {
return translate('NoChange');
},
disabled: true,
},
{
key: 'enabled',
get value() {
return translate('Enabled');
},
},
{
key: 'disabled',
get value() {
return translate('Disabled');
},
},
];
function ManageIndexersEditModalContent(
@@ -36,43 +36,43 @@ type OnSelectedChangeCallback = React.ComponentProps<
const COLUMNS = [
{
name: 'name',
label: translate('Name'),
label: () => translate('Name'),
isSortable: true,
isVisible: true,
},
{
name: 'implementation',
label: translate('Implementation'),
label: () => translate('Implementation'),
isSortable: true,
isVisible: true,
},
{
name: 'enableRss',
label: translate('EnableRSS'),
label: () => translate('EnableRSS'),
isSortable: true,
isVisible: true,
},
{
name: 'enableAutomaticSearch',
label: translate('EnableAutomaticSearch'),
label: () => translate('EnableAutomaticSearch'),
isSortable: true,
isVisible: true,
},
{
name: 'enableInteractiveSearch',
label: translate('EnableInteractiveSearch'),
label: () => translate('EnableInteractiveSearch'),
isSortable: true,
isVisible: true,
},
{
name: 'priority',
label: translate('Priority'),
label: () => translate('Priority'),
isSortable: true,
isVisible: true,
},
{
name: 'tags',
label: translate('Tags'),
label: () => translate('Tags'),
isSortable: true,
isVisible: true,
},
@@ -70,9 +70,24 @@ function TagsModalContent(props: TagsModalContentProps) {
}, [tags, applyTags, onApplyTagsPress]);
const applyTagsOptions = [
{ key: 'add', value: translate('Add') },
{ key: 'remove', value: translate('Remove') },
{ key: 'replace', value: translate('Replace') },
{
key: 'add',
get value() {
return translate('Add');
},
},
{
key: 'remove',
get value() {
return translate('Remove');
},
},
{
key: 'replace',
get value() {
return translate('Replace');
},
},
];
return (
@@ -11,16 +11,51 @@ import { inputTypes, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
const writeAudioTagOptions = [
{ key: 'no', value: translate('WriteTagsNo') },
{ key: 'sync', value: translate('WriteTagsSync') },
{ key: 'allFiles', value: translate('WriteTagsAll') },
{ key: 'newFiles', value: translate('WriteTagsNew') }
{
key: 'no',
get value() {
return translate('WriteTagsNo');
}
},
{
key: 'sync',
get value() {
return translate('WriteTagsSync');
}
},
{
key: 'allFiles',
get value() {
return translate('WriteTagsAll');
}
},
{
key: 'newFiles',
get value() {
return translate('WriteTagsNew');
}
}
];
const writeBookTagOptions = [
{ key: 'sync', value: translate('WriteTagsSync') },
{ key: 'allFiles', value: translate('WriteTagsAll') },
{ key: 'newFiles', value: translate('WriteTagsNew') }
{
key: 'sync',
get value() {
return translate('WriteTagsSync');
}
},
{
key: 'allFiles',
get value() {
return translate('WriteTagsAll');
}
},
{
key: 'newFiles',
get value() {
return translate('WriteTagsNew');
}
}
];
function MetadataProvider(props) {
+1 -1
View File
@@ -139,7 +139,7 @@ function Settings() {
className={styles.link}
to="/settings/ui"
>
{translate('UI')}
{translate('Ui')}
</Link>
<div className={styles.summary}>
+20 -1
View File
@@ -4,6 +4,7 @@ import { createThunk, handleThunks } from 'Store/thunks';
import createAjaxRequest from 'Utilities/createAjaxRequest';
import getSectionState from 'Utilities/State/getSectionState';
import updateSectionState from 'Utilities/State/updateSectionState';
import { fetchTranslations as fetchAppTranslations } from 'Utilities/String/translate';
import createHandleActions from './Creators/createHandleActions';
function getDimensions(width, height) {
@@ -41,7 +42,12 @@ export const defaultState = {
isReconnecting: false,
isDisconnected: false,
isRestarting: false,
isSidebarVisible: !getDimensions(window.innerWidth, window.innerHeight).isSmallScreen
isSidebarVisible: !getDimensions(window.innerWidth, window.innerHeight).isSmallScreen,
translations: {
isFetching: true,
isPopulated: false,
error: null
}
};
//
@@ -53,6 +59,7 @@ export const SAVE_DIMENSIONS = 'app/saveDimensions';
export const SET_VERSION = 'app/setVersion';
export const SET_APP_VALUE = 'app/setAppValue';
export const SET_IS_SIDEBAR_VISIBLE = 'app/setIsSidebarVisible';
export const FETCH_TRANSLATIONS = 'app/fetchTranslations';
export const PING_SERVER = 'app/pingServer';
@@ -66,6 +73,7 @@ export const setAppValue = createAction(SET_APP_VALUE);
export const showMessage = createAction(SHOW_MESSAGE);
export const hideMessage = createAction(HIDE_MESSAGE);
export const pingServer = createThunk(PING_SERVER);
export const fetchTranslations = createThunk(FETCH_TRANSLATIONS);
//
// Helpers
@@ -127,6 +135,17 @@ function pingServerAfterTimeout(getState, dispatch) {
export const actionHandlers = handleThunks({
[PING_SERVER]: function(getState, payload, dispatch) {
pingServerAfterTimeout(getState, dispatch);
},
[FETCH_TRANSLATIONS]: async function(getState, payload, dispatch) {
const isFetchingComplete = await fetchAppTranslations();
dispatch(setAppValue({
translations: {
isFetching: false,
isPopulated: isFetchingComplete,
error: isFetchingComplete ? null : 'Failed to load translations from API'
}
}));
}
});
+5 -5
View File
@@ -24,12 +24,12 @@ export const section = 'books';
export const filters = [
{
key: 'all',
label: translate('All'),
label: () => translate('All'),
filters: []
},
{
key: 'monitored',
label: translate('Monitored'),
label: () => translate('Monitored'),
filters: [
{
key: 'monitored',
@@ -40,7 +40,7 @@ export const filters = [
},
{
key: 'unmonitored',
label: translate('Unmonitored'),
label: () => translate('Unmonitored'),
filters: [
{
key: 'monitored',
@@ -51,7 +51,7 @@ export const filters = [
},
{
key: 'missing',
label: translate('Missing'),
label: () => translate('Missing'),
filters: [
{
key: 'monitored',
@@ -67,7 +67,7 @@ export const filters = [
},
{
key: 'wanted',
label: translate('Wanted'),
label: () => translate('Wanted'),
filters: [
{
key: 'monitored',
+20 -20
View File
@@ -60,32 +60,32 @@ export const defaultState = {
columns: [
{
name: 'status',
columnLabel: translate('Status'),
columnLabel: () => translate('Status'),
isSortable: true,
isVisible: true,
isModifiable: false
},
{
name: 'authorMetadata.sortName',
label: translate('Author'),
label: () => translate('Author'),
isSortable: true,
isVisible: true
},
{
name: 'books.title',
label: translate('BookTitle'),
label: () => translate('BookTitle'),
isSortable: true,
isVisible: true
},
{
name: 'books.releaseDate',
label: translate('ReleaseDate'),
label: () => translate('ReleaseDate'),
isSortable: true,
isVisible: false
},
{
name: 'quality',
label: translate('Quality'),
label: () => translate('Quality'),
isSortable: true,
isVisible: true
},
@@ -97,64 +97,64 @@ export const defaultState = {
},
{
name: 'customFormatScore',
columnLabel: translate('CustomFormatScore'),
columnLabel: () => translate('CustomFormatScore'),
label: React.createElement(Icon, {
name: icons.SCORE,
title: translate('CustomFormatScore')
title: () => translate('CustomFormatScore')
}),
isVisible: false
},
{
name: 'protocol',
label: translate('Protocol'),
label: () => translate('Protocol'),
isSortable: true,
isVisible: false
},
{
name: 'indexer',
label: translate('Indexer'),
label: () => translate('Indexer'),
isSortable: true,
isVisible: false
},
{
name: 'downloadClient',
label: translate('DownloadClient'),
label: () => translate('DownloadClient'),
isSortable: true,
isVisible: false
},
{
name: 'title',
label: translate('ReleaseTitle'),
label: () => translate('ReleaseTitle'),
isSortable: true,
isVisible: false
},
{
name: 'size',
label: translate('Size'),
label: () => translate('Size'),
isSortable: true,
isVisible: false
},
{
name: 'outputPath',
label: translate('OutputPath'),
label: () => translate('OutputPath'),
isSortable: false,
isVisible: false
},
{
name: 'estimatedCompletionTime',
label: translate('TimeLeft'),
label: () => translate('TimeLeft'),
isSortable: true,
isVisible: true
},
{
name: 'progress',
label: translate('Progress'),
label: () => translate('Progress'),
isSortable: true,
isVisible: true
},
{
name: 'actions',
columnLabel: translate('Actions'),
columnLabel: () => translate('Actions'),
isVisible: true,
isModifiable: false
}
@@ -371,13 +371,13 @@ export const actionHandlers = handleThunks({
id,
remove,
blocklist,
skipredownload
skipRedownload
} = payload;
dispatch(updateItem({ section: paged, id, isRemoving: true }));
const promise = createAjaxRequest({
url: `/queue/${id}?removeFromClient=${remove}&blocklist=${blocklist}&skipredownload=${skipredownload}`,
url: `/queue/${id}?removeFromClient=${remove}&blocklist=${blocklist}&skipRedownload=${skipRedownload}`,
method: 'DELETE'
}).request;
@@ -395,7 +395,7 @@ export const actionHandlers = handleThunks({
ids,
remove,
blocklist,
skipredownload
skipRedownload
} = payload;
dispatch(batchActions([
@@ -411,7 +411,7 @@ export const actionHandlers = handleThunks({
]));
const promise = createAjaxRequest({
url: `/queue/bulk?removeFromClient=${remove}&blocklist=${blocklist}&skipredownload=${skipredownload}`,
url: `/queue/bulk?removeFromClient=${remove}&blocklist=${blocklist}&skipRedownload=${skipRedownload}`,
method: 'DELETE',
dataType: 'json',
data: JSON.stringify({ ids })
+1 -1
View File
@@ -199,7 +199,7 @@ export const defaultState = {
},
{
name: 'customFormatScore',
label: translate('CustomFormatScore'),
label: () => translate('CustomFormatScore'),
type: filterBuilderTypes.NUMBER
},
{
+5 -5
View File
@@ -82,34 +82,34 @@ export const defaultState = {
columns: [
{
name: 'level',
columnLabel: translate('Level'),
columnLabel: () => translate('Level'),
isSortable: false,
isVisible: true,
isModifiable: false
},
{
name: 'time',
label: translate('Time'),
label: () => translate('Time'),
isSortable: true,
isVisible: true,
isModifiable: false
},
{
name: 'logger',
label: translate('Component'),
label: () => translate('Component'),
isSortable: false,
isVisible: true,
isModifiable: false
},
{
name: 'message',
label: translate('Message'),
label: () => translate('Message'),
isVisible: true,
isModifiable: false
},
{
name: 'actions',
columnLabel: translate('Actions'),
columnLabel: () => translate('Actions'),
isSortable: true,
isVisible: true,
isModifiable: false
@@ -36,10 +36,17 @@ function mergeColumns(path, initialState, persistedState, computedState) {
const column = initialColumns.find((i) => i.name === persistedColumn.name);
if (column) {
columns.push({
...column,
isVisible: persistedColumn.isVisible
});
const newColumn = {};
// We can't use a spread operator or Object.assign to clone the column
// or any accessors are lost and can break translations.
for (const prop of Object.keys(column)) {
Object.defineProperty(newColumn, prop, Object.getOwnPropertyDescriptor(column, prop));
}
newColumn.isVisible = persistedColumn.isVisible;
columns.push(newColumn);
}
});
+3 -3
View File
@@ -21,17 +21,17 @@ const columns = [
},
{
name: 'name',
label: 'Name',
label: () => translate('Name'),
isVisible: true
},
{
name: 'size',
label: 'Size',
label: () => translate('Size'),
isVisible: true
},
{
name: 'time',
label: 'Time',
label: () => translate('Time'),
isVisible: true
},
{
+2 -2
View File
@@ -19,12 +19,12 @@ import LogFilesTableRow from './LogFilesTableRow';
const columns = [
{
name: 'filename',
label: 'Filename',
label: () => translate('Filename'),
isVisible: true
},
{
name: 'lastWriteTime',
label: 'Last Write Time',
label: () => translate('LastWriteTime'),
isVisible: true
},
{
@@ -15,17 +15,17 @@ import styles from './DiskSpace.css';
const columns = [
{
name: 'path',
label: 'Location',
label: () => translate('Location'),
isVisible: true
},
{
name: 'freeSpace',
label: 'Free Space',
label: () => translate('FreeSpace'),
isVisible: true
},
{
name: 'totalSpace',
label: 'Total Space',
label: () => translate('TotalSpace'),
isVisible: true
},
{
+2 -2
View File
@@ -95,12 +95,12 @@ const columns = [
},
{
name: 'message',
label: 'Message',
label: () => translate('Message'),
isVisible: true
},
{
name: 'actions',
label: 'Actions',
label: () => translate('Actions'),
isVisible: true
}
];
@@ -15,27 +15,27 @@ const columns = [
},
{
name: 'commandName',
label: translate('Name'),
label: () => translate('Name'),
isVisible: true
},
{
name: 'queued',
label: translate('Queued'),
label: () => translate('Queued'),
isVisible: true
},
{
name: 'started',
label: translate('Started'),
label: () => translate('Started'),
isVisible: true
},
{
name: 'ended',
label: translate('Ended'),
label: () => translate('Ended'),
isVisible: true
},
{
name: 'duration',
label: translate('Duration'),
label: () => translate('Duration'),
isVisible: true
},
{
@@ -10,27 +10,27 @@ import ScheduledTaskRowConnector from './ScheduledTaskRowConnector';
const columns = [
{
name: 'name',
label: 'Name',
label: () => translate('Name'),
isVisible: true
},
{
name: 'interval',
label: 'Interval',
label: () => translate('Interval'),
isVisible: true
},
{
name: 'lastExecution',
label: 'Last Execution',
label: () => translate('LastExecution'),
isVisible: true
},
{
name: 'lastDuration',
label: 'Last Duration',
label: () => translate('LastDuration'),
isVisible: true
},
{
name: 'nextExecution',
label: 'Next Execution',
label: () => translate('NextExecution'),
isVisible: true
},
{
@@ -1,5 +1,7 @@
function formatPreferredWordScore(input, customFormatsLength = 0) {
function formatCustomFormatScore(
input?: number,
customFormatsLength = 0
): string {
const score = Number(input);
if (score > 0) {
@@ -7,10 +9,10 @@ function formatPreferredWordScore(input, customFormatsLength = 0) {
}
if (score < 0) {
return score;
return `${score}`;
}
return customFormatsLength > 0 ? '+0' : '';
}
export default formatPreferredWordScore;
export default formatCustomFormatScore;
@@ -4,13 +4,13 @@ function getTranslations() {
return createAjaxRequest({
global: false,
dataType: 'json',
url: '/localization'
url: '/localization',
}).request;
}
let translations = {};
let translations: Record<string, string> = {};
export function fetchTranslations() {
export async function fetchTranslations(): Promise<boolean> {
return new Promise(async (resolve) => {
try {
const data = await getTranslations();
@@ -23,12 +23,19 @@ export function fetchTranslations() {
});
}
export default function translate(key, args = []) {
export default function translate(
key: string,
args?: (string | number | boolean)[]
) {
if (!(key in translations)) {
console.debug(key);
}
const translation = translations[key] || key;
if (args) {
return translation.replace(/\{(\d+)\}/g, (match, index) => {
return args[index];
return String(args[index]) ?? match;
});
}
+15
View File
@@ -0,0 +1,15 @@
import { createBrowserHistory } from 'history';
import React from 'react';
import { render } from 'react-dom';
import createAppStore from 'Store/createAppStore';
import App from './App/App';
export async function bootstrap() {
const history = createBrowserHistory();
const store = createAppStore(history);
render(
<App store={store} history={history} />,
document.getElementById('root')
);
}
+9 -4
View File
@@ -48,7 +48,15 @@
/>
<link rel="stylesheet" type="text/css" href="/Content/Fonts/fonts.css">
<!-- webpack bundles head -->
<script>
window.Readarr = {
urlBase: '__URL_BASE__'
};
</script>
<% for (key in htmlWebpackPlugin.files.js) { %><script type="text/javascript" src="<%= htmlWebpackPlugin.files.js[key] %>" data-no-hash></script><% } %>
<% for (key in htmlWebpackPlugin.files.css) { %><link href="<%= htmlWebpackPlugin.files.css[key] %>" rel="stylesheet"></link><% } %>
<title>Readarr</title>
@@ -77,7 +85,4 @@
<div id="portal-root"></div>
<div id="root" class="root"></div>
</body>
<script src="/initialize.js" data-no-hash></script>
<!-- webpack bundles body -->
</html>
-26
View File
@@ -1,26 +0,0 @@
import { createBrowserHistory } from 'history';
import React from 'react';
import { render } from 'react-dom';
import { fetchTranslations } from 'Utilities/String/translate';
import './preload';
import './polyfills';
import 'Styles/globals.css';
import './index.css';
const history = createBrowserHistory();
const hasTranslationsError = !await fetchTranslations();
const { default: createAppStore } = await import('Store/createAppStore');
const { default: App } = await import('./App/App');
const store = createAppStore(history);
render(
<App
store={store}
history={history}
hasTranslationsError={hasTranslationsError}
/>,
document.getElementById('root')
);
+19
View File
@@ -0,0 +1,19 @@
import './polyfills';
import 'Styles/globals.css';
import './index.css';
const initializeUrl = `${
window.Readarr.urlBase
}/initialize.json?t=${Date.now()}`;
const response = await fetch(initializeUrl);
window.Readarr = await response.json();
/* eslint-disable no-undef, @typescript-eslint/ban-ts-comment */
// @ts-ignore 2304
__webpack_public_path__ = `${window.Readarr.urlBase}/`;
/* eslint-enable no-undef, @typescript-eslint/ban-ts-comment */
const { bootstrap } = await import('./bootstrap');
await bootstrap();
+2 -2
View File
@@ -1,11 +1,11 @@
{
"compilerOptions": {
"target": "es6",
"target": "esnext",
"allowJs": true,
"checkJs": false,
"baseUrl": "src",
"jsx": "react",
"module": "commonjs",
"module": "esnext",
"moduleResolution": "node",
"noEmit": true,
"esModuleInterop": true,
+3 -1
View File
@@ -20,7 +20,7 @@
"author": "Team Readarr",
"license": "GPL-3.0",
"readmeFilename": "readme.md",
"main": "index.js",
"main": "index.ts",
"browserslist": [
"defaults"
],
@@ -97,6 +97,8 @@
"@babel/preset-env": "7.22.9",
"@babel/preset-react": "7.22.5",
"@babel/preset-typescript": "7.22.5",
"@types/lodash": "4.14.197",
"@types/redux-actions": "2.6.2",
"@typescript-eslint/eslint-plugin": "6.0.0",
"@typescript-eslint/parser": "6.0.0",
"autoprefixer": "10.4.14",
@@ -77,7 +77,9 @@ namespace NzbDrone.Common.EnvironmentInfo
FullName = Name;
}
if (IsLinux && File.Exists("/proc/1/cgroup") && File.ReadAllText("/proc/1/cgroup").Contains("/docker/"))
if (IsLinux &&
((File.Exists("/proc/1/cgroup") && File.ReadAllText("/proc/1/cgroup").Contains("/docker/")) ||
(File.Exists("/proc/1/mountinfo") && File.ReadAllText("/proc/1/mountinfo").Contains("/docker/"))))
{
IsDocker = true;
}
@@ -43,7 +43,13 @@ namespace NzbDrone.Common.Instrumentation.Sentry
"OutOfMemoryException",
// Filter out people stuck in boot loops
"CorruptDatabaseException"
"CorruptDatabaseException",
// Filter SingleInstance Termination Exceptions
"TerminateApplicationException",
// User config issue, root folder missing, etc.
"DirectoryNotFoundException"
};
public static readonly List<string> FilteredExceptionMessages = new List<string>
@@ -33,7 +33,7 @@ namespace NzbDrone.Core.Test.Download
{
AuthorId = 1,
BookIds = new List<int> { 1 },
SkipReDownload = true
SkipRedownload = true
};
Subject.Handle(failedEvent);
@@ -38,6 +38,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
_definition = new IndexerDefinition
{
Name = "Indexer",
EnableRss = true,
ConfigContract = "TorznabSettings",
Settings = torznabSettings
};
@@ -65,12 +65,21 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
}
[Test]
public void should_use_pagesize_reported_by_caps()
public void should_use_best_pagesize_reported_by_caps()
{
_caps.MaxPageSize = 30;
_caps.DefaultPageSize = 25;
Subject.PageSize.Should().Be(25);
Subject.PageSize.Should().Be(30);
}
[Test]
public void should_not_use_pagesize_over_100_even_if_reported_in_caps()
{
_caps.MaxPageSize = 250;
_caps.DefaultPageSize = 25;
Subject.PageSize.Should().Be(100);
}
[Test]
@@ -103,12 +103,21 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests
}
[Test]
public void should_use_pagesize_reported_by_caps()
public void should_use_best_pagesize_reported_by_caps()
{
_caps.MaxPageSize = 30;
_caps.DefaultPageSize = 25;
Subject.PageSize.Should().Be(25);
Subject.PageSize.Should().Be(30);
}
[Test]
public void should_not_use_pagesize_over_100_even_if_reported_in_caps()
{
_caps.MaxPageSize = 250;
_caps.DefaultPageSize = 25;
Subject.PageSize.Should().Be(100);
}
[TestCase("http://localhost:9117/", "/api")]
@@ -41,8 +41,8 @@ namespace NzbDrone.Core.Test.MetadataSource.Goodreads
}
[TestCase("Robert Harris", "Robert Harris")]
[TestCase("James Patterson", "James Patterson")]
[TestCase("Antoine de Saint-Exupéry", "Antoine de Saint-Exupéry")]
[TestCase("Lyndsay Ely", "Lyndsay Ely")]
[TestCase("Elisa Puricelli Guerra", "Elisa Puricelli Guerra")]
public void successful_author_search(string title, string expected)
{
var result = Subject.SearchForNewAuthor(title);
@@ -54,7 +54,7 @@ namespace NzbDrone.Core.Test.MetadataSource.Goodreads
ExceptionVerification.IgnoreWarns();
}
[TestCase("Harry Potter and the sorcerer's stone", null, "Harry Potter and the Sorcerer's Stone")]
[TestCase("Harry Potter and the sorcerer's stone a summary of the novel", null, "Harry Potter and the Sorcerer's Stone (Book 1): A Summary Of The Novel")]
[TestCase("edition:3", null, "Harry Potter and the Sorcerer's Stone")]
[TestCase("edition: 3", null, "Harry Potter and the Sorcerer's Stone")]
[TestCase("asin:B0192CTMYG", null, "Harry Potter and the Sorcerer's Stone")]
@@ -85,8 +85,8 @@ namespace NzbDrone.Core.Test.MetadataSource.Goodreads
ExceptionVerification.IgnoreWarns();
}
[TestCase("Philip Pullman", 0, typeof(Author), new[] { "Philip Pullman" }, TestName = "author")]
[TestCase("Philip Pullman", 1, typeof(Book), new[] { "Northern Lights", "The Amber Spyglass" }, TestName = "book")]
[TestCase("Catherine Butler", 0, typeof(Author), new[] { "Catherine Butler" }, TestName = "author")]
[TestCase("Catherine Butler", 1, typeof(Book), new[] { "Twisted Winter", "Shattered Dreams" }, TestName = "book")]
public void successful_combined_search(string query, int position, Type resultType, string[] expected)
{
var result = Subject.SearchForNewEntity(query);
@@ -97,7 +97,7 @@ namespace NzbDrone.Core.Test.MetadataSource.Goodreads
{
var cast = result[position] as Author;
cast.Should().NotBeNull();
cast.Name.Should().ContainAll(expected);
cast.Name.Should().ContainAny(expected);
}
else
{
@@ -25,8 +25,8 @@ namespace NzbDrone.Core.Test.MetadataSource.Goodreads
}
[TestCase("Robert Harris", 575)]
[TestCase("James Patterson", 3780)]
[TestCase("Antoine de Saint-Exupéry", 1020792)]
[TestCase("Lyndsay Ely", 8056539)]
[TestCase("Elisa Puricelli Guerra", 4481805)]
public void successful_author_search(string title, int expected)
{
var result = Subject.Search(title);
@@ -38,7 +38,7 @@ namespace NzbDrone.Core.Test.MetadataSource.Goodreads
ExceptionVerification.IgnoreWarns();
}
[TestCase("Harry Potter and the sorcerer's stone", 3)]
[TestCase("Harry Potter and the sorcerer's stone a summary of the novel", 23314781)]
[TestCase("B0192CTMYG", 61209488)]
[TestCase("9780439554930", 48517161)]
public void successful_book_search(string title, int expected)
@@ -0,0 +1,14 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(036)]
public class add_result_to_commands : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Alter.Table("Commands").AddColumn("Result").AsInt32().WithDefaultValue(1);
}
}
}
@@ -74,10 +74,19 @@ namespace NzbDrone.Core.Download
{
var result = base.Test(definition);
if ((result == null || result.IsValid) && definition.Id != 0)
if (definition.Id == 0)
{
return result;
}
if (result == null || result.IsValid)
{
_downloadClientStatusService.RecordSuccess(definition.Id);
}
else
{
_downloadClientStatusService.RecordFailure(definition.Id);
}
return result;
}
@@ -21,6 +21,6 @@ namespace NzbDrone.Core.Download
public string Message { get; set; }
public Dictionary<string, string> Data { get; set; }
public TrackedDownload TrackedDownload { get; set; }
public bool SkipReDownload { get; set; }
public bool SkipRedownload { get; set; }
}
}
@@ -9,8 +9,8 @@ namespace NzbDrone.Core.Download
{
public interface IFailedDownloadService
{
void MarkAsFailed(int historyId, bool skipReDownload = false);
void MarkAsFailed(string downloadId, bool skipReDownload = false);
void MarkAsFailed(int historyId, bool skipRedownload = false);
void MarkAsFailed(string downloadId, bool skipRedownload = false);
void Check(TrackedDownload trackedDownload);
void ProcessFailed(TrackedDownload trackedDownload);
}
@@ -30,14 +30,14 @@ namespace NzbDrone.Core.Download
_eventAggregator = eventAggregator;
}
public void MarkAsFailed(int historyId, bool skipReDownload = false)
public void MarkAsFailed(int historyId, bool skipRedownload = false)
{
var history = _historyService.Get(historyId);
var downloadId = history.DownloadId;
if (downloadId.IsNullOrWhiteSpace())
{
PublishDownloadFailedEvent(new List<EntityHistory> { history }, "Manually marked as failed", skipReDownload: skipReDownload);
PublishDownloadFailedEvent(new List<EntityHistory> { history }, "Manually marked as failed", skipRedownload: skipRedownload);
}
else
{
@@ -46,7 +46,7 @@ namespace NzbDrone.Core.Download
}
}
public void MarkAsFailed(string downloadId, bool skipReDownload = false)
public void MarkAsFailed(string downloadId, bool skipRedownload = false)
{
var history = _historyService.Find(downloadId, EntityHistoryEventType.Grabbed);
@@ -54,7 +54,7 @@ namespace NzbDrone.Core.Download
{
var trackedDownload = _trackedDownloadService.Find(downloadId);
PublishDownloadFailedEvent(history, "Manually marked as failed", trackedDownload, skipReDownload);
PublishDownloadFailedEvent(history, "Manually marked as failed", trackedDownload, skipRedownload);
}
}
@@ -114,7 +114,7 @@ namespace NzbDrone.Core.Download
PublishDownloadFailedEvent(grabbedItems, failure, trackedDownload);
}
private void PublishDownloadFailedEvent(List<EntityHistory> historyItems, string message, TrackedDownload trackedDownload = null, bool skipReDownload = false)
private void PublishDownloadFailedEvent(List<EntityHistory> historyItems, string message, TrackedDownload trackedDownload = null, bool skipRedownload = false)
{
var historyItem = historyItems.First();
@@ -129,7 +129,7 @@ namespace NzbDrone.Core.Download
Message = message,
Data = historyItem.Data,
TrackedDownload = trackedDownload,
SkipReDownload = skipReDownload
SkipRedownload = skipRedownload
};
_eventAggregator.PublishEvent(downloadFailedEvent);
@@ -29,7 +29,7 @@ namespace NzbDrone.Core.Download
[EventHandleOrder(EventHandleOrder.Last)]
public void Handle(DownloadFailedEvent message)
{
if (message.SkipReDownload)
if (message.SkipRedownload)
{
_logger.Debug("Skip redownloading requested by user");
return;
@@ -1,4 +1,5 @@
using System.Net;
using System;
using System.Net;
using NzbDrone.Common.Exceptions;
namespace NzbDrone.Core.Exceptions
@@ -13,6 +14,12 @@ namespace NzbDrone.Core.Exceptions
StatusCode = statusCode;
}
public NzbDroneClientException(HttpStatusCode statusCode, string message, Exception innerException, params object[] args)
: base(message, innerException, args)
{
StatusCode = statusCode;
}
public NzbDroneClientException(HttpStatusCode statusCode, string message)
: base(message)
{
@@ -12,5 +12,10 @@ namespace NzbDrone.Core.Extras.Files
public DateTime Added { get; set; }
public DateTime LastUpdated { get; set; }
public string Extension { get; set; }
public override string ToString()
{
return $"[{Id}] {RelativePath}";
}
}
}
@@ -81,6 +81,8 @@ namespace NzbDrone.Core.Extras.Files
protected TExtraFile MoveFile(Author author, BookFile bookFile, TExtraFile extraFile, string fileNameSuffix = null)
{
_logger.Trace("Renaming extra file: {0}", extraFile);
var newFolder = Path.GetDirectoryName(bookFile.Path);
var filenameBuilder = new StringBuilder(Path.GetFileNameWithoutExtension(bookFile.Path));
@@ -98,9 +100,13 @@ namespace NzbDrone.Core.Extras.Files
{
try
{
_logger.Trace("Renaming extra file: {0} to {1}", extraFile, newFileName);
_diskProvider.MoveFile(existingFileName, newFileName);
extraFile.RelativePath = author.Path.GetRelativePath(newFileName);
_logger.Trace("Renamed extra file from: {0}", extraFile);
return extraFile;
}
catch (Exception ex)
@@ -0,0 +1,45 @@
using System.Linq;
using NzbDrone.Core.Download;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Localization;
using NzbDrone.Core.ThingiProvider.Events;
namespace NzbDrone.Core.HealthCheck.Checks
{
[CheckOn(typeof(ProviderUpdatedEvent<IIndexer>))]
[CheckOn(typeof(ProviderDeletedEvent<IIndexer>))]
[CheckOn(typeof(ProviderUpdatedEvent<IDownloadClient>))]
[CheckOn(typeof(ProviderDeletedEvent<IDownloadClient>))]
public class IndexerDownloadClientCheck : HealthCheckBase
{
private readonly IIndexerFactory _indexerFactory;
private readonly IDownloadClientFactory _downloadClientFactory;
public IndexerDownloadClientCheck(IIndexerFactory indexerFactory,
IDownloadClientFactory downloadClientFactory,
ILocalizationService localizationService)
: base(localizationService)
{
_indexerFactory = indexerFactory;
_downloadClientFactory = downloadClientFactory;
}
public override HealthCheck Check()
{
var downloadClientsIds = _downloadClientFactory.All().Where(v => v.Enable).Select(v => v.Id).ToList();
var invalidIndexers = _indexerFactory.All()
.Where(v => v.Enable && v.DownloadClientId > 0 && !downloadClientsIds.Contains(v.DownloadClientId))
.ToList();
if (invalidIndexers.Any())
{
return new HealthCheck(GetType(),
HealthCheckResult.Warning,
string.Format(_localizationService.GetLocalizedString("IndexerDownloadClientHealthCheckMessage"), string.Join(", ", invalidIndexers.Select(v => v.Name).ToArray())),
"#invalid-indexer-download-client-setting");
}
return new HealthCheck(GetType());
}
}
}
@@ -8,6 +8,7 @@ using NzbDrone.Core.ThingiProvider.Events;
namespace NzbDrone.Core.HealthCheck.Checks
{
[CheckOn(typeof(ProviderAddedEvent<IIndexer>))]
[CheckOn(typeof(ProviderUpdatedEvent<IIndexer>))]
[CheckOn(typeof(ProviderDeletedEvent<IIndexer>))]
[CheckOn(typeof(ProviderStatusChangedEvent<IIndexer>))]
@@ -23,12 +24,15 @@ namespace NzbDrone.Core.HealthCheck.Checks
public override HealthCheck Check()
{
var jackettAllProviders = _providerFactory.All().Where(
i => i.ConfigContract.Equals("TorznabSettings") &&
((i.Settings as TorznabSettings).BaseUrl.Contains("/torznab/all/api", StringComparison.InvariantCultureIgnoreCase) ||
(i.Settings as TorznabSettings).BaseUrl.Contains("/api/v2.0/indexers/all/results/torznab", StringComparison.InvariantCultureIgnoreCase) ||
(i.Settings as TorznabSettings).ApiPath.Contains("/torznab/all/api", StringComparison.InvariantCultureIgnoreCase) ||
(i.Settings as TorznabSettings).ApiPath.Contains("/api/v2.0/indexers/all/results/torznab", StringComparison.InvariantCultureIgnoreCase)));
var jackettAllProviders = _providerFactory.All()
.Where(
i => i.Enable &&
i.ConfigContract.Equals("TorznabSettings") &&
(((TorznabSettings)i.Settings).BaseUrl.Contains("/torznab/all/api", StringComparison.InvariantCultureIgnoreCase) ||
((TorznabSettings)i.Settings).BaseUrl.Contains("/api/v2.0/indexers/all/results/torznab", StringComparison.InvariantCultureIgnoreCase) ||
((TorznabSettings)i.Settings).ApiPath.Contains("/torznab/all/api", StringComparison.InvariantCultureIgnoreCase) ||
((TorznabSettings)i.Settings).ApiPath.Contains("/api/v2.0/indexers/all/results/torznab", StringComparison.InvariantCultureIgnoreCase)))
.ToArray();
if (jackettAllProviders.Empty())
{
@@ -37,8 +41,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
return new HealthCheck(GetType(),
HealthCheckResult.Warning,
string.Format(_localizationService.GetLocalizedString("IndexerJackettAll"),
string.Join(", ", jackettAllProviders.Select(i => i.Name))),
string.Format(_localizationService.GetLocalizedString("IndexerJackettAll"), string.Join(", ", jackettAllProviders.Select(i => i.Name))),
"#jackett-all-endpoint-used");
}
}
@@ -75,10 +75,19 @@ namespace NzbDrone.Core.ImportLists
{
var result = base.Test(definition);
if ((result == null || result.IsValid) && definition.Id != 0)
if (definition.Id == 0)
{
return result;
}
if (result == null || result.IsValid)
{
_importListStatusService.RecordSuccess(definition.Id);
}
else
{
_importListStatusService.RecordFailure(definition.Id);
}
return result;
}
@@ -43,14 +43,26 @@ namespace NzbDrone.Core.IndexerSearch
public List<DownloadDecision> BookSearch(int bookId, bool missingOnly, bool userInvokedSearch, bool interactiveSearch)
{
var downloadDecisions = new List<DownloadDecision>();
var book = _bookService.GetBook(bookId);
return BookSearch(book, missingOnly, userInvokedSearch, interactiveSearch);
var decisions = BookSearch(book, missingOnly, userInvokedSearch, interactiveSearch);
downloadDecisions.AddRange(decisions);
return DeDupeDecisions(downloadDecisions);
}
public List<DownloadDecision> AuthorSearch(int authorId, bool missingOnly, bool userInvokedSearch, bool interactiveSearch)
{
var downloadDecisions = new List<DownloadDecision>();
var author = _authorService.GetAuthor(authorId);
return AuthorSearch(author, missingOnly, userInvokedSearch, interactiveSearch);
var decisions = AuthorSearch(author, missingOnly, userInvokedSearch, interactiveSearch);
downloadDecisions.AddRange(decisions);
return DeDupeDecisions(downloadDecisions);
}
public List<DownloadDecision> AuthorSearch(Author author, bool missingOnly, bool userInvokedSearch, bool interactiveSearch)
@@ -150,5 +162,13 @@ namespace NzbDrone.Core.IndexerSearch
return _makeDownloadDecision.GetSearchDecision(reports, criteriaBase).ToList();
}
private List<DownloadDecision> DeDupeDecisions(List<DownloadDecision> decisions)
{
// De-dupe reports by guid so duplicate results aren't returned. Pick the one with the least rejections and higher indexer priority.
return decisions.GroupBy(d => d.RemoteBook.Release.Guid)
.Select(d => d.OrderBy(v => v.Rejections.Count()).ThenBy(v => v.RemoteBook?.Release?.IndexerPriority ?? IndexerDefinition.DefaultPriority).First())
.ToList();
}
}
}
@@ -79,7 +79,6 @@ namespace NzbDrone.Core.Indexers
result.ForEach(c =>
{
c.Guid = string.Concat(Definition.Id, "_", c.Guid);
c.IndexerId = Definition.Id;
c.Indexer = Definition.Name;
c.DownloadProtocol = Protocol;
@@ -4,6 +4,13 @@ namespace NzbDrone.Core.Indexers
{
public class IndexerDefinition : ProviderDefinition
{
public const int DefaultPriority = 25;
public IndexerDefinition()
{
Priority = DefaultPriority;
}
public bool EnableRss { get; set; }
public bool EnableAutomaticSearch { get; set; }
public bool EnableInteractiveSearch { get; set; }
@@ -11,7 +18,7 @@ namespace NzbDrone.Core.Indexers
public DownloadProtocol Protocol { get; set; }
public bool SupportsRss { get; set; }
public bool SupportsSearch { get; set; }
public int Priority { get; set; } = 25;
public int Priority { get; set; }
public override bool Enable => EnableRss || EnableAutomaticSearch || EnableInteractiveSearch;
+10 -1
View File
@@ -102,10 +102,19 @@ namespace NzbDrone.Core.Indexers
{
var result = base.Test(definition);
if ((result == null || result.IsValid) && definition.Id != 0)
if (definition.Id == 0)
{
return result;
}
if (result == null || result.IsValid)
{
_indexerStatusService.RecordSuccess(definition.Id);
}
else
{
_indexerStatusService.RecordFailure(definition.Id);
}
return result;
}
+18 -7
View File
@@ -19,8 +19,13 @@ namespace NzbDrone.Core.Indexers.Newznab
public override string Name => "Newznab";
public override DownloadProtocol Protocol => DownloadProtocol.Usenet;
public override int PageSize => GetProviderPageSize();
public override int PageSize => _capabilitiesProvider.GetCapabilities(Settings).DefaultPageSize;
public Newznab(INewznabCapabilitiesProvider capabilitiesProvider, IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger)
: base(httpClient, indexerStatusService, configService, parsingService, logger)
{
_capabilitiesProvider = capabilitiesProvider;
}
public override IIndexerRequestGenerator GetRequestGenerator()
{
@@ -54,12 +59,6 @@ namespace NzbDrone.Core.Indexers.Newznab
}
}
public Newznab(INewznabCapabilitiesProvider capabilitiesProvider, IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger)
: base(httpClient, indexerStatusService, configService, parsingService, logger)
{
_capabilitiesProvider = capabilitiesProvider;
}
private IndexerDefinition GetDefinition(string name, NewznabSettings settings)
{
return new IndexerDefinition
@@ -163,5 +162,17 @@ namespace NzbDrone.Core.Indexers.Newznab
return base.RequestAction(action, query);
}
private int GetProviderPageSize()
{
try
{
return Math.Min(100, Math.Max(_capabilitiesProvider.GetCapabilities(Settings).DefaultPageSize, _capabilitiesProvider.GetCapabilities(Settings).MaxPageSize));
}
catch
{
return 100;
}
}
}
}
+19 -7
View File
@@ -19,7 +19,13 @@ namespace NzbDrone.Core.Indexers.Torznab
public override string Name => "Torznab";
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
public override int PageSize => _capabilitiesProvider.GetCapabilities(Settings).DefaultPageSize;
public override int PageSize => GetProviderPageSize();
public Torznab(INewznabCapabilitiesProvider capabilitiesProvider, IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger)
: base(httpClient, indexerStatusService, configService, parsingService, logger)
{
_capabilitiesProvider = capabilitiesProvider;
}
public override IIndexerRequestGenerator GetRequestGenerator()
{
@@ -35,12 +41,6 @@ namespace NzbDrone.Core.Indexers.Torznab
return new TorznabRssParser();
}
public Torznab(INewznabCapabilitiesProvider capabilitiesProvider, IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger)
: base(httpClient, indexerStatusService, configService, parsingService, logger)
{
_capabilitiesProvider = capabilitiesProvider;
}
private IndexerDefinition GetDefinition(string name, TorznabSettings settings)
{
return new IndexerDefinition
@@ -147,5 +147,17 @@ namespace NzbDrone.Core.Indexers.Torznab
return base.RequestAction(action, query);
}
private int GetProviderPageSize()
{
try
{
return Math.Min(100, Math.Max(_capabilitiesProvider.GetCapabilities(Settings).DefaultPageSize, _capabilitiesProvider.GetCapabilities(Settings).MaxPageSize));
}
catch
{
return 100;
}
}
}
}
@@ -441,7 +441,6 @@
"SslPortHelpTextWarning": "يتطلب إعادة التشغيل ليصبح ساري المفعول",
"DownloadClientCheckDownloadingToRoot": "يقوم برنامج التنزيل {0} بوضع التنزيلات في المجلد الجذر {1}. يجب ألا تقوم بالتنزيل إلى مجلد جذر.",
"Progress": "التقدم",
"UI": "واجهة المستخدم",
"ReplaceIllegalCharactersHelpText": "استبدل الأحرف غير القانونية. إذا لم يتم تحديده ، فسوف يقوم Radarr بإزالتها بدلاً من ذلك",
"ReleaseTitle": "عنوان الإصدار",
"Actions": "أجراءات",
@@ -441,7 +441,6 @@
"SslCertPathHelpTextWarning": "Изисква рестартиране, за да влезе в сила",
"DownloadClientCheckDownloadingToRoot": "Клиентът за изтегляне {0} поставя изтеглянията в основната папка {1}. Не трябва да изтегляте в основна папка.",
"ReplaceIllegalCharactersHelpText": "Заменете незаконните символи. Ако не е отметнато, Radarr ще ги премахне вместо това",
"UI": "Потребителски интерфейс",
"Actions": "Действия",
"Tomorrow": "Утре",
"Today": "Днес",
@@ -299,7 +299,6 @@
"StartupDirectory": "Directori d'inici",
"Test": "Prova",
"ThisCannotBeCancelled": "No es pot cancel·lar un cop iniciat sense desactivar tots els vostres indexadors.",
"UI": "Interfície",
"UISettings": "Configuració de la interfície",
"UISettingsSummary": "Opcions de calendari, data i color alternats",
"UnableToAddANewDownloadClientPleaseTryAgain": "No es pot afegir un nou client de descàrrega, torneu-ho a provar.",
@@ -440,7 +440,6 @@
"Year": "Rok",
"YesCancel": "Ano, zrušit",
"DownloadClientCheckDownloadingToRoot": "Stahovací klient {0} umístí stažené soubory do kořenové složky {1}. Neměli byste stahovat do kořenové složky.",
"UI": "UI",
"ReplaceIllegalCharactersHelpText": "Nahraďte nepovolené znaky. Pokud není zaškrtnuto, radarr je místo toho odstraní",
"OutputPath": "Výstupní cesta",
"Actions": "Akce",
@@ -443,7 +443,6 @@
"ReplaceIllegalCharactersHelpText": "Udskift ulovlige tegn. Hvis det ikke er markeret, fjerner Radarr dem i stedet",
"ReleaseTitle": "Udgiv titel",
"Actions": "Handlinger",
"UI": "UI",
"Tomorrow": "I morgen",
"Today": "I dag",
"Progress": "Fremskridt",
@@ -489,7 +489,6 @@
"PortHelpText": "Calibre-Content-Server",
"Progress": "Fortschritt",
"ReleaseTitle": "Release Titel",
"UI": "Oberfläche",
"Actions": "Aktionen",
"Today": "Heute",
"Tomorrow": "Morgen",
@@ -756,7 +755,6 @@
"ReadarrSupportsMultipleListsForImportingBooksAndAuthorsIntoTheDatabase": "Lidarr unterstützt mehrere Listen für den Import von Alben und Künstlern in die Datenbank.",
"TotalBookCountBooksTotalBookFileCountBooksWithFilesInterp": "{0} Titel insgesamt. {1} Titel mit Dateien.",
"SearchFiltered": "Suche gefilterte",
"SkipredownloadHelpText": "Verhindert, dass Lidarr versucht alternative Veröffentlichungen für die entfernten Objekte herunterzuladen",
"AddList": "Liste hinzufügen",
"InstanceName": "Instanzname",
"InstanceNameHelpText": "Instanzname im Browser-Tab und für Syslog-Anwendungsname",

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