mirror of
https://github.com/Readarr/Readarr.git
synced 2026-04-18 21:34:28 -04:00
Compare commits
120 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2faef704b4 | |||
| a566c3e21f | |||
| cc0d2a84ae | |||
| 1c3d2ce4e5 | |||
| 57f614f4cd | |||
| 9d2efe0944 | |||
| e032be48e0 | |||
| cd66de1992 | |||
| 3066dd92d7 | |||
| 467a87baec | |||
| 80fb077c94 | |||
| 07433d69ca | |||
| 3b3ebe463c | |||
| 03392ca635 | |||
| d23ce9ecc2 | |||
| e968fcaff6 | |||
| 31da559f89 | |||
| a093290792 | |||
| 9e3dfc510d | |||
| 9d27c172ac | |||
| 518dbe53eb | |||
| f9ba00c9e7 | |||
| 4aec7a0ea7 | |||
| fc4cf8e81e | |||
| 143de3b220 | |||
| e1a07d01b2 | |||
| 27e498bb14 | |||
| b9f1882a57 | |||
| 2392573c39 | |||
| 2351efd013 | |||
| 526429bde4 | |||
| abd44b59bc | |||
| 9942457ffc | |||
| 073342ef39 | |||
| b455708f2e | |||
| 622b02c478 | |||
| 8effba383d | |||
| 2749479283 | |||
| 4cbafa76d8 | |||
| 73782cc233 | |||
| de396fe9be | |||
| 71cb9e1dd7 | |||
| ee5ed57fcc | |||
| d20a049a5a | |||
| a9f77ace37 | |||
| 0341a2ec26 | |||
| d6796bbe1a | |||
| 9066f8558c | |||
| c4e37528ee | |||
| 5937c952af | |||
| 0f4bd3c472 | |||
| cf415e61de | |||
| 9865e92cea | |||
| 1cf956a9d9 | |||
| 8989c55c8c | |||
| dc83e0127e | |||
| 34eb312426 | |||
| 9d5cdebdb2 | |||
| a0ab224acd | |||
| 05aa35a54d | |||
| ca7f8775f5 | |||
| 2a01e9b445 | |||
| 7d30c7d1ea | |||
| 50be87e5a4 | |||
| 0572d1ac80 | |||
| d2240514d7 | |||
| ad47dc032d | |||
| 6c6df7d7d9 | |||
| 2121204064 | |||
| 61004ea33f | |||
| 54c1c7862e | |||
| 43dfdc8bf5 | |||
| 0d1ae0ca4e | |||
| 9902889a30 | |||
| 04d7061030 | |||
| fd201912a9 | |||
| c412701a3d | |||
| 7451a66365 | |||
| a6431fdb0b | |||
| 060b133f6d | |||
| 5ed13b942b | |||
| 89f3d8167b | |||
| 77b027374f | |||
| 650490abb2 | |||
| 7d2e215d61 | |||
| 65ff890c74 | |||
| 50c0b0dbaa | |||
| d5f36d0144 | |||
| fab7558bd4 | |||
| 3dc86b3a01 | |||
| 24ad6134e3 | |||
| 033f8c40af | |||
| 4c73a619eb | |||
| 3ca798e983 | |||
| d9827fd6a6 | |||
| f4f03a853f | |||
| 4f4e4bf2ca | |||
| 413a70a312 | |||
| a8f2b91010 | |||
| 68a4ee6000 | |||
| 5196ce311b | |||
| ae92b22727 | |||
| 0bccffef01 | |||
| bca899b9c0 | |||
| 2bb576a94b | |||
| bb49949853 | |||
| a093061b29 | |||
| df876707c4 | |||
| 2af33143ba | |||
| c3c5a47776 | |||
| a21abe0838 | |||
| a32f5f6639 | |||
| 4cd45ecc21 | |||
| 2c8e0b1ca4 | |||
| bd25c9e3e0 | |||
| ee64b8788b | |||
| 7aeada2089 | |||
| e188c9aac0 | |||
| a3ae2359f5 | |||
| 5b92905dd4 |
+2
-2
@@ -9,13 +9,13 @@ 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.3.8'
|
majorVersion: '0.3.16'
|
||||||
minorVersion: $[counter('minorVersion', 1)]
|
minorVersion: $[counter('minorVersion', 1)]
|
||||||
readarrVersion: '$(majorVersion).$(minorVersion)'
|
readarrVersion: '$(majorVersion).$(minorVersion)'
|
||||||
buildName: '$(Build.SourceBranchName).$(readarrVersion)'
|
buildName: '$(Build.SourceBranchName).$(readarrVersion)'
|
||||||
sentryOrg: 'servarr'
|
sentryOrg: 'servarr'
|
||||||
sentryUrl: 'https://sentry.servarr.com'
|
sentryUrl: 'https://sentry.servarr.com'
|
||||||
dotnetVersion: '6.0.413'
|
dotnetVersion: '6.0.417'
|
||||||
nodeVersion: '16.X'
|
nodeVersion: '16.X'
|
||||||
innoVersion: '6.2.0'
|
innoVersion: '6.2.0'
|
||||||
windowsImage: 'windows-2022'
|
windowsImage: 'windows-2022'
|
||||||
|
|||||||
@@ -338,4 +338,8 @@ Queue.propTypes = {
|
|||||||
onRemoveSelectedPress: PropTypes.func.isRequired
|
onRemoveSelectedPress: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Queue.defaultProps = {
|
||||||
|
count: 0
|
||||||
|
};
|
||||||
|
|
||||||
export default Queue;
|
export default Queue;
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ class RemoveQueueItemsModal extends Component {
|
|||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<div className={styles.message}>
|
<div className={styles.message}>
|
||||||
{selectedCount > 1 ? translate('RemoveSelectedItemsQueueMessageText', selectedCount) : translate('RemoveSelectedItemQueueMessageText')}
|
{selectedCount > 1 ? translate('RemoveSelectedItemsQueueMessageText', { selectedCount }) : translate('RemoveSelectedItemQueueMessageText')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -44,6 +44,10 @@
|
|||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.filterIcon {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
.authorNavigationButtons {
|
.authorNavigationButtons {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ interface CssExports {
|
|||||||
'authorUpButton': string;
|
'authorUpButton': string;
|
||||||
'contentContainer': string;
|
'contentContainer': string;
|
||||||
'errorMessage': string;
|
'errorMessage': string;
|
||||||
|
'filterIcon': string;
|
||||||
'innerContentBody': string;
|
'innerContentBody': string;
|
||||||
'metadataMessage': string;
|
'metadataMessage': string;
|
||||||
'selectedTab': string;
|
'selectedTab': string;
|
||||||
|
|||||||
@@ -239,9 +239,14 @@ class AuthorDetails extends Component {
|
|||||||
saveError,
|
saveError,
|
||||||
isDeleting,
|
isDeleting,
|
||||||
deleteError,
|
deleteError,
|
||||||
statistics
|
statistics = {}
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
bookFileCount = 0,
|
||||||
|
totalBookCount = 0
|
||||||
|
} = statistics;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isOrganizeModalOpen,
|
isOrganizeModalOpen,
|
||||||
isRetagModalOpen,
|
isRetagModalOpen,
|
||||||
@@ -435,7 +440,7 @@ class AuthorDetails extends Component {
|
|||||||
className={styles.tab}
|
className={styles.tab}
|
||||||
selectedClassName={styles.selectedTab}
|
selectedClassName={styles.selectedTab}
|
||||||
>
|
>
|
||||||
{translate('BooksTotal', [statistics.totalBookCount])}
|
{translate('BooksTotal', [totalBookCount])}
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
<Tab
|
<Tab
|
||||||
@@ -463,7 +468,7 @@ class AuthorDetails extends Component {
|
|||||||
className={styles.tab}
|
className={styles.tab}
|
||||||
selectedClassName={styles.selectedTab}
|
selectedClassName={styles.selectedTab}
|
||||||
>
|
>
|
||||||
{translate('FilesTotal', [statistics.bookFileCount])}
|
{translate('FilesTotal', [bookFileCount])}
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -155,7 +155,6 @@ function createMapStateToProps() {
|
|||||||
const isRefreshing = isAuthorRefreshing || allAuthorRefreshing;
|
const isRefreshing = isAuthorRefreshing || allAuthorRefreshing;
|
||||||
const isSearching = isCommandExecuting(findCommand(commands, { name: commandNames.AUTHOR_SEARCH, authorId: author.id }));
|
const isSearching = isCommandExecuting(findCommand(commands, { name: commandNames.AUTHOR_SEARCH, authorId: author.id }));
|
||||||
const isRenamingFiles = isCommandExecuting(findCommand(commands, { name: commandNames.RENAME_FILES, authorId: author.id }));
|
const isRenamingFiles = isCommandExecuting(findCommand(commands, { name: commandNames.RENAME_FILES, authorId: author.id }));
|
||||||
|
|
||||||
const isRenamingAuthorCommand = findCommand(commands, { name: commandNames.RENAME_AUTHOR });
|
const isRenamingAuthorCommand = findCommand(commands, { name: commandNames.RENAME_AUTHOR });
|
||||||
const isRenamingAuthor = (
|
const isRenamingAuthor = (
|
||||||
isCommandExecuting(isRenamingAuthorCommand) &&
|
isCommandExecuting(isRenamingAuthorCommand) &&
|
||||||
|
|||||||
@@ -136,8 +136,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
|
font-weight: 300;
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
line-height: 50px;
|
line-height: 30px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Modal from 'Components/Modal/Modal';
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import { sizes } from 'Helpers/Props';
|
||||||
import AuthorHistoryContentConnector from './AuthorHistoryContentConnector';
|
import AuthorHistoryContentConnector from './AuthorHistoryContentConnector';
|
||||||
import AuthorHistoryModalContent from './AuthorHistoryModalContent';
|
import AuthorHistoryModalContent from './AuthorHistoryModalContent';
|
||||||
|
|
||||||
@@ -14,6 +15,7 @@ function AuthorHistoryModal(props) {
|
|||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
|
size={sizes.EXTRA_LARGE}
|
||||||
onModalClose={onModalClose}
|
onModalClose={onModalClose}
|
||||||
>
|
>
|
||||||
<AuthorHistoryContentConnector
|
<AuthorHistoryContentConnector
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ 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 translate from 'Utilities/String/translate';
|
||||||
import AuthorHistoryTableContent from './AuthorHistoryTableContent';
|
import AuthorHistoryTableContent from './AuthorHistoryTableContent';
|
||||||
|
|
||||||
class AuthorHistoryModalContent extends Component {
|
class AuthorHistoryModalContent extends Component {
|
||||||
@@ -20,7 +21,7 @@ class AuthorHistoryModalContent extends Component {
|
|||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
History
|
{translate('History')}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
@@ -31,7 +32,7 @@ class AuthorHistoryModalContent extends Component {
|
|||||||
|
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button onPress={onModalClose}>
|
<Button onPress={onModalClose}>
|
||||||
Close
|
{translate('Close')}
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.details,
|
|
||||||
.actions {
|
.actions {
|
||||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'actions': string;
|
'actions': string;
|
||||||
'details': string;
|
|
||||||
'sourceTitle': string;
|
'sourceTitle': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import HistoryDetailsConnector from 'Activity/History/Details/HistoryDetailsConnector';
|
import HistoryDetailsConnector from 'Activity/History/Details/HistoryDetailsConnector';
|
||||||
import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell';
|
import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell';
|
||||||
|
import BookFormats from 'Book/BookFormats';
|
||||||
import BookQuality from 'Book/BookQuality';
|
import BookQuality from 'Book/BookQuality';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import IconButton from 'Components/Link/IconButton';
|
import IconButton from 'Components/Link/IconButton';
|
||||||
@@ -11,6 +12,7 @@ import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
|||||||
import TableRow from 'Components/Table/TableRow';
|
import TableRow from 'Components/Table/TableRow';
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
|
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './AuthorHistoryRow.css';
|
import styles from './AuthorHistoryRow.css';
|
||||||
|
|
||||||
@@ -75,6 +77,8 @@ class AuthorHistoryRow extends Component {
|
|||||||
sourceTitle,
|
sourceTitle,
|
||||||
quality,
|
quality,
|
||||||
qualityCutoffNotMet,
|
qualityCutoffNotMet,
|
||||||
|
customFormats,
|
||||||
|
customFormatScore,
|
||||||
date,
|
date,
|
||||||
data,
|
data,
|
||||||
book
|
book
|
||||||
@@ -106,11 +110,19 @@ class AuthorHistoryRow extends Component {
|
|||||||
/>
|
/>
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell>
|
||||||
|
<BookFormats formats={customFormats} />
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell>
|
||||||
|
{formatCustomFormatScore(customFormatScore, customFormats.length)}
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
<RelativeDateCellConnector
|
<RelativeDateCellConnector
|
||||||
date={date}
|
date={date}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TableRowCell className={styles.details}>
|
<TableRowCell className={styles.actions}>
|
||||||
<Popover
|
<Popover
|
||||||
anchor={
|
anchor={
|
||||||
<Icon
|
<Icon
|
||||||
@@ -127,14 +139,13 @@ class AuthorHistoryRow extends Component {
|
|||||||
}
|
}
|
||||||
position={tooltipPositions.LEFT}
|
position={tooltipPositions.LEFT}
|
||||||
/>
|
/>
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell className={styles.actions}>
|
|
||||||
{
|
{
|
||||||
eventType === 'grabbed' &&
|
eventType === 'grabbed' &&
|
||||||
<IconButton
|
<IconButton
|
||||||
title={translate('MarkAsFailed')}
|
title={translate('MarkAsFailed')}
|
||||||
name={icons.REMOVE}
|
name={icons.REMOVE}
|
||||||
|
size={14}
|
||||||
onPress={this.onMarkAsFailedPress}
|
onPress={this.onMarkAsFailedPress}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -160,6 +171,8 @@ AuthorHistoryRow.propTypes = {
|
|||||||
sourceTitle: PropTypes.string.isRequired,
|
sourceTitle: PropTypes.string.isRequired,
|
||||||
quality: PropTypes.object.isRequired,
|
quality: PropTypes.object.isRequired,
|
||||||
qualityCutoffNotMet: PropTypes.bool.isRequired,
|
qualityCutoffNotMet: PropTypes.bool.isRequired,
|
||||||
|
customFormats: PropTypes.arrayOf(PropTypes.object),
|
||||||
|
customFormatScore: PropTypes.number.isRequired,
|
||||||
date: PropTypes.string.isRequired,
|
date: PropTypes.string.isRequired,
|
||||||
data: PropTypes.object.isRequired,
|
data: PropTypes.object.isRequired,
|
||||||
fullAuthor: PropTypes.bool.isRequired,
|
fullAuthor: PropTypes.bool.isRequired,
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
.container {
|
||||||
|
border: 1px solid var(--borderColor);
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--inputBackgroundColor);
|
||||||
|
|
||||||
|
&:last-of-type {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'container': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import AuthorHistoryContentConnector from 'Author/History/AuthorHistoryContentConnector';
|
import AuthorHistoryContentConnector from 'Author/History/AuthorHistoryContentConnector';
|
||||||
import AuthorHistoryTableContent from 'Author/History/AuthorHistoryTableContent';
|
import AuthorHistoryTableContent from 'Author/History/AuthorHistoryTableContent';
|
||||||
|
import styles from './AuthorHistoryTable.css';
|
||||||
|
|
||||||
function AuthorHistoryTable(props) {
|
function AuthorHistoryTable(props) {
|
||||||
const {
|
const {
|
||||||
@@ -8,10 +9,12 @@ function AuthorHistoryTable(props) {
|
|||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthorHistoryContentConnector
|
<div className={styles.container}>
|
||||||
component={AuthorHistoryTableContent}
|
<AuthorHistoryContentConnector
|
||||||
{...otherProps}
|
component={AuthorHistoryTableContent}
|
||||||
/>
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
.blankpad {
|
||||||
|
padding-top: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
padding-left: 2em;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'blankpad': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import Alert from 'Components/Alert';
|
import Alert from 'Components/Alert';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
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 { kinds } from 'Helpers/Props';
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import AuthorHistoryRowConnector from './AuthorHistoryRowConnector';
|
import AuthorHistoryRowConnector from './AuthorHistoryRowConnector';
|
||||||
|
import styles from './AuthorHistoryTableContent.css';
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
@@ -15,32 +17,41 @@ const columns = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'book',
|
name: 'book',
|
||||||
label: 'Book',
|
label: () => translate('Book'),
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'sourceTitle',
|
name: 'sourceTitle',
|
||||||
label: 'Source Title',
|
label: () => translate( 'SourceTitle'),
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'quality',
|
name: 'quality',
|
||||||
label: 'Quality',
|
label: () => translate('Quality'),
|
||||||
|
isVisible: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'customFormats',
|
||||||
|
label: () => translate('CustomFormats'),
|
||||||
|
isSortable: false,
|
||||||
|
isVisible: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'customFormatScore',
|
||||||
|
label: React.createElement(Icon, {
|
||||||
|
name: icons.SCORE,
|
||||||
|
title: () => translate('CustomFormatScore')
|
||||||
|
}),
|
||||||
|
isSortable: true,
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'date',
|
name: 'date',
|
||||||
label: 'Date',
|
label: () => translate('Date'),
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'details',
|
|
||||||
label: 'Details',
|
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'actions',
|
name: 'actions',
|
||||||
label: 'Actions',
|
|
||||||
isVisible: true
|
isVisible: true
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@@ -64,7 +75,7 @@ class AuthorHistoryTableContent extends Component {
|
|||||||
const hasItems = !!items.length;
|
const hasItems = !!items.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div>
|
||||||
{
|
{
|
||||||
isFetching &&
|
isFetching &&
|
||||||
<LoadingIndicator />
|
<LoadingIndicator />
|
||||||
@@ -79,7 +90,7 @@ class AuthorHistoryTableContent extends Component {
|
|||||||
|
|
||||||
{
|
{
|
||||||
isPopulated && !hasItems && !error &&
|
isPopulated && !hasItems && !error &&
|
||||||
<div>
|
<div className={styles.blankpad}>
|
||||||
{translate('NoHistory')}
|
{translate('NoHistory')}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -103,7 +114,7 @@ class AuthorHistoryTableContent extends Component {
|
|||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
}
|
}
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import React from 'react';
|
|||||||
import Label from 'Components/Label';
|
import Label from 'Components/Label';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
function getTooltip(title, quality, size, isMonitored, isCutoffNotMet) {
|
function getTooltip(title, quality, size, isMonitored, isCutoffNotMet) {
|
||||||
const revision = quality.revision;
|
const revision = quality.revision;
|
||||||
@@ -28,6 +29,36 @@ function getTooltip(title, quality, size, isMonitored, isCutoffNotMet) {
|
|||||||
return title;
|
return title;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function revisionLabel(className, quality, showRevision) {
|
||||||
|
if (!showRevision) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (quality.revision.isRepack) {
|
||||||
|
return (
|
||||||
|
<Label
|
||||||
|
className={className}
|
||||||
|
kind={kinds.PRIMARY}
|
||||||
|
title={translate('Repack')}
|
||||||
|
>
|
||||||
|
R
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (quality.revision.version && quality.revision.version > 1) {
|
||||||
|
return (
|
||||||
|
<Label
|
||||||
|
className={className}
|
||||||
|
kind={kinds.PRIMARY}
|
||||||
|
title={translate('Proper')}
|
||||||
|
>
|
||||||
|
P
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function BookQuality(props) {
|
function BookQuality(props) {
|
||||||
const {
|
const {
|
||||||
className,
|
className,
|
||||||
@@ -35,7 +66,8 @@ function BookQuality(props) {
|
|||||||
quality,
|
quality,
|
||||||
size,
|
size,
|
||||||
isMonitored,
|
isMonitored,
|
||||||
isCutoffNotMet
|
isCutoffNotMet,
|
||||||
|
showRevision
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
let kind = kinds.DEFAULT;
|
let kind = kinds.DEFAULT;
|
||||||
@@ -50,13 +82,15 @@ function BookQuality(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Label
|
<span>
|
||||||
className={className}
|
<Label
|
||||||
kind={kind}
|
className={className}
|
||||||
title={getTooltip(title, quality, size, isMonitored, isCutoffNotMet)}
|
kind={kind}
|
||||||
>
|
title={getTooltip(title, quality, size, isMonitored, isCutoffNotMet)}
|
||||||
{quality.quality.name}
|
>
|
||||||
</Label>
|
{quality.quality.name}
|
||||||
|
</Label>{revisionLabel(className, quality, showRevision)}
|
||||||
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,12 +100,14 @@ BookQuality.propTypes = {
|
|||||||
quality: PropTypes.object.isRequired,
|
quality: PropTypes.object.isRequired,
|
||||||
size: PropTypes.number,
|
size: PropTypes.number,
|
||||||
isMonitored: PropTypes.bool,
|
isMonitored: PropTypes.bool,
|
||||||
isCutoffNotMet: PropTypes.bool
|
isCutoffNotMet: PropTypes.bool,
|
||||||
|
showRevision: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
BookQuality.defaultProps = {
|
BookQuality.defaultProps = {
|
||||||
title: '',
|
title: '',
|
||||||
isMonitored: true
|
isMonitored: true,
|
||||||
|
showRevision: false
|
||||||
};
|
};
|
||||||
|
|
||||||
export default BookQuality;
|
export default BookQuality;
|
||||||
|
|||||||
@@ -99,9 +99,14 @@ class BookDetails extends Component {
|
|||||||
nextBook,
|
nextBook,
|
||||||
isSearching,
|
isSearching,
|
||||||
onRefreshPress,
|
onRefreshPress,
|
||||||
onSearchPress
|
onSearchPress,
|
||||||
|
statistics = {}
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
bookFileCount = 0
|
||||||
|
} = statistics;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isOrganizeModalOpen,
|
isOrganizeModalOpen,
|
||||||
isRetagModalOpen,
|
isRetagModalOpen,
|
||||||
@@ -238,21 +243,21 @@ class BookDetails extends Component {
|
|||||||
className={styles.tab}
|
className={styles.tab}
|
||||||
selectedClassName={styles.selectedTab}
|
selectedClassName={styles.selectedTab}
|
||||||
>
|
>
|
||||||
History
|
{translate('History')}
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
<Tab
|
<Tab
|
||||||
className={styles.tab}
|
className={styles.tab}
|
||||||
selectedClassName={styles.selectedTab}
|
selectedClassName={styles.selectedTab}
|
||||||
>
|
>
|
||||||
Search
|
{translate('Search')}
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
<Tab
|
<Tab
|
||||||
className={styles.tab}
|
className={styles.tab}
|
||||||
selectedClassName={styles.selectedTab}
|
selectedClassName={styles.selectedTab}
|
||||||
>
|
>
|
||||||
Files
|
{translate('FilesTotal', [bookFileCount])}
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -335,6 +340,7 @@ BookDetails.propTypes = {
|
|||||||
ratings: PropTypes.object.isRequired,
|
ratings: PropTypes.object.isRequired,
|
||||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
links: PropTypes.arrayOf(PropTypes.object).isRequired,
|
links: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
statistics: PropTypes.object.isRequired,
|
||||||
monitored: PropTypes.bool.isRequired,
|
monitored: PropTypes.bool.isRequired,
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
shortDateFormat: PropTypes.string.isRequired,
|
||||||
isSaving: PropTypes.bool.isRequired,
|
isSaving: PropTypes.bool.isRequired,
|
||||||
|
|||||||
@@ -69,16 +69,21 @@ function createMapStateToProps() {
|
|||||||
|
|
||||||
const previousBook = sortedBooks[bookIndex - 1] || _.last(sortedBooks);
|
const previousBook = sortedBooks[bookIndex - 1] || _.last(sortedBooks);
|
||||||
const nextBook = sortedBooks[bookIndex + 1] || _.first(sortedBooks);
|
const nextBook = sortedBooks[bookIndex + 1] || _.first(sortedBooks);
|
||||||
|
const isRefreshingCommand = findCommand(commands, { name: commandNames.REFRESH_BOOK });
|
||||||
|
const isRefreshing = (
|
||||||
|
isCommandExecuting(isRefreshingCommand) &&
|
||||||
|
isRefreshingCommand.body.bookId === book.id
|
||||||
|
);
|
||||||
const isSearchingCommand = findCommand(commands, { name: commandNames.BOOK_SEARCH });
|
const isSearchingCommand = findCommand(commands, { name: commandNames.BOOK_SEARCH });
|
||||||
const isSearching = (
|
const isSearching = (
|
||||||
isCommandExecuting(isSearchingCommand) &&
|
isCommandExecuting(isSearchingCommand) &&
|
||||||
isSearchingCommand.body.bookIds.indexOf(book.id) > -1
|
isSearchingCommand.body.bookIds.indexOf(book.id) > -1
|
||||||
);
|
);
|
||||||
|
const isRenamingFiles = isCommandExecuting(findCommand(commands, { name: commandNames.RENAME_FILES, authorId: author.id }));
|
||||||
const isRefreshingCommand = findCommand(commands, { name: commandNames.REFRESH_BOOK });
|
const isRenamingAuthorCommand = findCommand(commands, { name: commandNames.RENAME_AUTHOR });
|
||||||
const isRefreshing = (
|
const isRenamingAuthor = (
|
||||||
isCommandExecuting(isRefreshingCommand) &&
|
isCommandExecuting(isRenamingAuthorCommand) &&
|
||||||
isRefreshingCommand.body.bookId === book.id
|
isRenamingAuthorCommand.body.authorIds.indexOf(author.id) > -1
|
||||||
);
|
);
|
||||||
|
|
||||||
const isFetching = isBookFilesFetching || editions.isFetching;
|
const isFetching = isBookFilesFetching || editions.isFetching;
|
||||||
@@ -90,6 +95,8 @@ function createMapStateToProps() {
|
|||||||
author,
|
author,
|
||||||
isRefreshing,
|
isRefreshing,
|
||||||
isSearching,
|
isSearching,
|
||||||
|
isRenamingFiles,
|
||||||
|
isRenamingAuthor,
|
||||||
isFetching,
|
isFetching,
|
||||||
isPopulated,
|
isPopulated,
|
||||||
bookFilesError,
|
bookFilesError,
|
||||||
@@ -125,9 +132,27 @@ class BookDetailsConnector extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
if (prevProps.id !== this.props.id ||
|
const {
|
||||||
|
id,
|
||||||
|
anyReleaseOk,
|
||||||
|
isRenamingFiles,
|
||||||
|
isRenamingAuthor
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (
|
||||||
|
(prevProps.isRenamingFiles && !isRenamingFiles) ||
|
||||||
|
(prevProps.isRenamingAuthor && !isRenamingAuthor) ||
|
||||||
!_.isEqual(getMonitoredEditions(prevProps), getMonitoredEditions(this.props)) ||
|
!_.isEqual(getMonitoredEditions(prevProps), getMonitoredEditions(this.props)) ||
|
||||||
(prevProps.anyReleaseOk === false && this.props.anyReleaseOk === true)) {
|
(prevProps.anyReleaseOk === false && anyReleaseOk === true)
|
||||||
|
) {
|
||||||
|
this.unpopulate();
|
||||||
|
this.populate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the id has changed we need to clear the book
|
||||||
|
// files and fetch from the server.
|
||||||
|
|
||||||
|
if (prevProps.id !== id) {
|
||||||
this.unpopulate();
|
this.unpopulate();
|
||||||
this.populate();
|
this.populate();
|
||||||
}
|
}
|
||||||
@@ -197,6 +222,8 @@ class BookDetailsConnector extends Component {
|
|||||||
BookDetailsConnector.propTypes = {
|
BookDetailsConnector.propTypes = {
|
||||||
id: PropTypes.number,
|
id: PropTypes.number,
|
||||||
anyReleaseOk: PropTypes.bool,
|
anyReleaseOk: PropTypes.bool,
|
||||||
|
isRenamingFiles: PropTypes.bool.isRequired,
|
||||||
|
isRenamingAuthor: PropTypes.bool.isRequired,
|
||||||
isBookFetching: PropTypes.bool,
|
isBookFetching: PropTypes.bool,
|
||||||
isBookPopulated: PropTypes.bool,
|
isBookPopulated: PropTypes.bool,
|
||||||
titleSlug: PropTypes.string.isRequired,
|
titleSlug: PropTypes.string.isRequired,
|
||||||
|
|||||||
@@ -117,8 +117,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
|
font-weight: 300;
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
line-height: 50px;
|
line-height: 30px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -229,7 +229,6 @@ class BookIndexRow extends Component {
|
|||||||
className={styles[name]}
|
className={styles[name]}
|
||||||
>
|
>
|
||||||
{bookFileCount}
|
{bookFileCount}
|
||||||
|
|
||||||
</VirtualTableRowCell>
|
</VirtualTableRowCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
.container {
|
||||||
|
border: 1px solid var(--borderColor);
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--inputBackgroundColor);
|
||||||
|
|
||||||
|
&:last-of-type {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'container': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import BookFileEditorTableContentConnector from './BookFileEditorTableContentConnector';
|
import BookFileEditorTableContentConnector from './BookFileEditorTableContentConnector';
|
||||||
|
import styles from './BookFileEditorTable.css';
|
||||||
|
|
||||||
function BookFileEditorTable(props) {
|
function BookFileEditorTable(props) {
|
||||||
const {
|
const {
|
||||||
@@ -7,9 +8,11 @@ function BookFileEditorTable(props) {
|
|||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BookFileEditorTableContentConnector
|
<div className={styles.container}>
|
||||||
{...otherProps}
|
<BookFileEditorTableContentConnector
|
||||||
/>
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
.filesTable {
|
.filesTable {
|
||||||
margin-bottom: 20px;
|
margin: 10px;
|
||||||
padding-top: 15px;
|
padding-top: 5px;
|
||||||
border: 1px solid var(--borderColor);
|
border: 1px solid var(--borderColor);
|
||||||
border-top: 1px solid var(--borderColor);
|
border-top: 1px solid var(--borderColor);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@@ -13,9 +13,15 @@
|
|||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-right: auto;
|
margin: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selectInput {
|
.selectInput {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.blankpad {
|
||||||
|
padding-top: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
padding-left: 2em;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'actions': string;
|
'actions': string;
|
||||||
|
'blankpad': string;
|
||||||
'filesTable': string;
|
'filesTable': string;
|
||||||
'selectInput': string;
|
'selectInput': string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
import SelectInput from 'Components/Form/SelectInput';
|
import SelectInput from 'Components/Form/SelectInput';
|
||||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
@@ -120,7 +121,7 @@ class BookFileEditorTableContent extends Component {
|
|||||||
const hasSelectedFiles = this.getSelectedIds().length > 0;
|
const hasSelectedFiles = this.getSelectedIds().length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div>
|
||||||
{
|
{
|
||||||
isFetching && !isPopulated ?
|
isFetching && !isPopulated ?
|
||||||
<LoadingIndicator /> :
|
<LoadingIndicator /> :
|
||||||
@@ -129,13 +130,13 @@ class BookFileEditorTableContent extends Component {
|
|||||||
|
|
||||||
{
|
{
|
||||||
!isFetching && error ?
|
!isFetching && error ?
|
||||||
<div>{error}</div> :
|
<Alert kind={kinds.DANGER}>{error}</Alert> :
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
isPopulated && !items.length ?
|
isPopulated && !items.length ?
|
||||||
<div>
|
<div className={styles.blankpad}>
|
||||||
No book files to manage.
|
No book files to manage.
|
||||||
</div> :
|
</div> :
|
||||||
null
|
null
|
||||||
@@ -173,26 +174,30 @@ class BookFileEditorTableContent extends Component {
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
<div className={styles.actions}>
|
{
|
||||||
<SpinnerButton
|
isPopulated && items.length ? (
|
||||||
kind={kinds.DANGER}
|
<div className={styles.actions}>
|
||||||
isSpinning={isDeleting}
|
<SpinnerButton
|
||||||
isDisabled={!hasSelectedFiles}
|
kind={kinds.DANGER}
|
||||||
onPress={this.onDeletePress}
|
isSpinning={isDeleting}
|
||||||
>
|
isDisabled={!hasSelectedFiles}
|
||||||
Delete
|
onPress={this.onDeletePress}
|
||||||
</SpinnerButton>
|
>
|
||||||
|
{translate('Delete')}
|
||||||
|
</SpinnerButton>
|
||||||
|
|
||||||
<div className={styles.selectInput}>
|
<div className={styles.selectInput}>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
name="quality"
|
name="quality"
|
||||||
value="selectQuality"
|
value="selectQuality"
|
||||||
values={qualityOptions}
|
values={qualityOptions}
|
||||||
isDisabled={!hasSelectedFiles}
|
isDisabled={!hasSelectedFiles}
|
||||||
onChange={this.onQualityChange}
|
onChange={this.onQualityChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
isOpen={isConfirmDeleteModalOpen}
|
isOpen={isConfirmDeleteModalOpen}
|
||||||
@@ -203,7 +208,7 @@ class BookFileEditorTableContent extends Component {
|
|||||||
onConfirm={this.onConfirmDelete}
|
onConfirm={this.onConfirmDelete}
|
||||||
onCancel={this.onConfirmDeleteModalClose}
|
onCancel={this.onConfirmDeleteModalClose}
|
||||||
/>
|
/>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ class CalendarConnector extends Component {
|
|||||||
gotoCalendarToday
|
gotoCalendarToday
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
registerPagePopulator(this.repopulate);
|
registerPagePopulator(this.repopulate, ['bookFileUpdated', 'bookFileDeleted']);
|
||||||
|
|
||||||
if (useCurrentPage) {
|
if (useCurrentPage) {
|
||||||
fetchCalendar();
|
fetchCalendar();
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
.description {
|
|
||||||
line-height: $lineHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
.description {
|
.description {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
|
line-height: $lineHeight;
|
||||||
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
|
|||||||
@@ -29,22 +29,24 @@ function CustomFiltersModalContent(props) {
|
|||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
{
|
{
|
||||||
customFilters.map((customFilter) => {
|
customFilters
|
||||||
return (
|
.sort((a, b) => a.label.localeCompare(b.label))
|
||||||
<CustomFilter
|
.map((customFilter) => {
|
||||||
key={customFilter.id}
|
return (
|
||||||
id={customFilter.id}
|
<CustomFilter
|
||||||
label={customFilter.label}
|
key={customFilter.id}
|
||||||
filters={customFilter.filters}
|
id={customFilter.id}
|
||||||
selectedFilterKey={selectedFilterKey}
|
label={customFilter.label}
|
||||||
isDeleting={isDeleting}
|
filters={customFilter.filters}
|
||||||
deleteError={deleteError}
|
selectedFilterKey={selectedFilterKey}
|
||||||
dispatchSetFilter={dispatchSetFilter}
|
isDeleting={isDeleting}
|
||||||
dispatchDeleteCustomFilter={dispatchDeleteCustomFilter}
|
deleteError={deleteError}
|
||||||
onEditPress={onEditCustomFilter}
|
dispatchSetFilter={dispatchSetFilter}
|
||||||
/>
|
dispatchDeleteCustomFilter={dispatchDeleteCustomFilter}
|
||||||
);
|
onEditPress={onEditCustomFilter}
|
||||||
})
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
<div className={styles.addButtonContainer}>
|
<div className={styles.addButtonContainer}>
|
||||||
|
|||||||
@@ -2,8 +2,10 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
margin-right: $formLabelRightMarginWidth;
|
margin-right: $formLabelRightMarginWidth;
|
||||||
|
padding-top: 8px;
|
||||||
|
min-height: 35px;
|
||||||
|
text-align: end;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
line-height: 35px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.hasError {
|
.hasError {
|
||||||
|
|||||||
@@ -39,18 +39,26 @@ class FilterMenuContent extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
customFilters.map((filter) => {
|
customFilters.length > 0 ?
|
||||||
return (
|
<MenuItemSeparator /> :
|
||||||
<FilterMenuItem
|
null
|
||||||
key={filter.id}
|
}
|
||||||
filterKey={filter.id}
|
|
||||||
selectedFilterKey={selectedFilterKey}
|
{
|
||||||
onPress={onFilterSelect}
|
customFilters
|
||||||
>
|
.sort((a, b) => a.label.localeCompare(b.label))
|
||||||
{filter.label}
|
.map((filter) => {
|
||||||
</FilterMenuItem>
|
return (
|
||||||
);
|
<FilterMenuItem
|
||||||
})
|
key={filter.id}
|
||||||
|
filterKey={filter.id}
|
||||||
|
selectedFilterKey={selectedFilterKey}
|
||||||
|
onPress={onFilterSelect}
|
||||||
|
>
|
||||||
|
{filter.label}
|
||||||
|
</FilterMenuItem>
|
||||||
|
);
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -202,6 +202,8 @@ class SignalRConnector extends Component {
|
|||||||
this.props.dispatchUpdateItem({ section, ...body.resource });
|
this.props.dispatchUpdateItem({ section, ...body.resource });
|
||||||
} else if (body.action === 'deleted') {
|
} else if (body.action === 'deleted') {
|
||||||
this.props.dispatchRemoveItem({ section, id: body.resource.id });
|
this.props.dispatchRemoveItem({ section, id: body.resource.id });
|
||||||
|
|
||||||
|
repopulatePage('bookFileDeleted');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Repopulate the page to handle recently imported file
|
// Repopulate the page to handle recently imported file
|
||||||
|
|||||||
@@ -15,5 +15,5 @@
|
|||||||
"start_url": "../../../../",
|
"start_url": "../../../../",
|
||||||
"theme_color": "#3a3f51",
|
"theme_color": "#3a3f51",
|
||||||
"background_color": "#3a3f51",
|
"background_color": "#3a3f51",
|
||||||
"display": "standalone"
|
"display": "minimal-ui"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||||
|
|
||||||
|
// This file contains some helpers for power users in a browser console
|
||||||
|
|
||||||
|
let hasWarned = false;
|
||||||
|
|
||||||
|
function checkActivationWarning() {
|
||||||
|
if (!hasWarned) {
|
||||||
|
console.log('Activated ReadarrApi console helpers.');
|
||||||
|
console.warn('Be warned: There will be no further confirmation checks.');
|
||||||
|
hasWarned = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachAsyncActions(promise) {
|
||||||
|
promise.filter = function() {
|
||||||
|
const args = arguments;
|
||||||
|
const res = this.then((d) => d.filter(...args));
|
||||||
|
attachAsyncActions(res);
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
promise.map = function() {
|
||||||
|
const args = arguments;
|
||||||
|
const res = this.then((d) => d.map(...args));
|
||||||
|
attachAsyncActions(res);
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
promise.all = function() {
|
||||||
|
const res = this.then((d) => Promise.all(d));
|
||||||
|
attachAsyncActions(res);
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
promise.forEach = function(action) {
|
||||||
|
const res = this.then((d) => Promise.all(d.map(action)));
|
||||||
|
attachAsyncActions(res);
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class ResourceApi {
|
||||||
|
constructor(api, url) {
|
||||||
|
this.api = api;
|
||||||
|
this.url = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
single(id) {
|
||||||
|
return this.api.fetch(`${this.url}/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
all() {
|
||||||
|
return this.api.fetch(this.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
filter(pred) {
|
||||||
|
return this.all().filter(pred);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(resource) {
|
||||||
|
return this.api.fetch(`${this.url}/${resource.id}`, { method: 'PUT', data: resource });
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(resource) {
|
||||||
|
if (typeof resource === 'object' && resource !== null && resource.id) {
|
||||||
|
resource = resource.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!resource || !Number.isInteger(resource)) {
|
||||||
|
throw Error('Invalid resource', resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.api.fetch(`${this.url}/${resource}`, { method: 'DELETE' });
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(url, options) {
|
||||||
|
return this.api.fetch(`${this.url}${url}`, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConsoleApi {
|
||||||
|
constructor() {
|
||||||
|
this.author = new ResourceApi(this, '/author');
|
||||||
|
}
|
||||||
|
|
||||||
|
resource(url) {
|
||||||
|
return new ResourceApi(this, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(url, options) {
|
||||||
|
checkActivationWarning();
|
||||||
|
|
||||||
|
options = options || {};
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
url,
|
||||||
|
method: options.method || 'GET'
|
||||||
|
};
|
||||||
|
|
||||||
|
if (options.data) {
|
||||||
|
req.dataType = 'json';
|
||||||
|
req.data = JSON.stringify(options.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
const promise = createAjaxRequest(req).request;
|
||||||
|
|
||||||
|
promise.fail((xhr) => {
|
||||||
|
console.error(`Failed to fetch ${url}`, xhr);
|
||||||
|
});
|
||||||
|
|
||||||
|
attachAsyncActions(promise);
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.ReadarrApi = new ConsoleApi();
|
||||||
|
|
||||||
|
export default ConsoleApi;
|
||||||
@@ -28,6 +28,10 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.quality {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.customFormatScore {
|
.customFormatScore {
|
||||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ class InteractiveSearchRow extends Component {
|
|||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
<TableRowCell className={styles.quality}>
|
<TableRowCell className={styles.quality}>
|
||||||
<BookQuality quality={quality} />
|
<BookQuality quality={quality} showRevision={true} />
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
<TableRowCell className={styles.customFormatScore}>
|
<TableRowCell className={styles.customFormatScore}>
|
||||||
|
|||||||
+16
@@ -14,9 +14,11 @@ 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,
|
||||||
|
setManageDownloadClientsSort,
|
||||||
} from 'Store/Actions/settingsActions';
|
} from 'Store/Actions/settingsActions';
|
||||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
import { SelectStateInputProps } from 'typings/props';
|
import { SelectStateInputProps } from 'typings/props';
|
||||||
@@ -80,6 +82,8 @@ const COLUMNS = [
|
|||||||
|
|
||||||
interface ManageDownloadClientsModalContentProps {
|
interface ManageDownloadClientsModalContentProps {
|
||||||
onModalClose(): void;
|
onModalClose(): void;
|
||||||
|
sortKey?: string;
|
||||||
|
sortDirection?: SortDirection;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ManageDownloadClientsModalContent(
|
function ManageDownloadClientsModalContent(
|
||||||
@@ -94,6 +98,8 @@ function ManageDownloadClientsModalContent(
|
|||||||
isSaving,
|
isSaving,
|
||||||
error,
|
error,
|
||||||
items,
|
items,
|
||||||
|
sortKey,
|
||||||
|
sortDirection,
|
||||||
}: DownloadClientAppState = useSelector(
|
}: DownloadClientAppState = useSelector(
|
||||||
createClientSideCollectionSelector('settings.downloadClients')
|
createClientSideCollectionSelector('settings.downloadClients')
|
||||||
);
|
);
|
||||||
@@ -114,6 +120,13 @@ function ManageDownloadClientsModalContent(
|
|||||||
|
|
||||||
const selectedCount = selectedIds.length;
|
const selectedCount = selectedIds.length;
|
||||||
|
|
||||||
|
const onSortPress = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
dispatch(setManageDownloadClientsSort({ sortKey: value }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
const onDeletePress = useCallback(() => {
|
const onDeletePress = useCallback(() => {
|
||||||
setIsDeleteModalOpen(true);
|
setIsDeleteModalOpen(true);
|
||||||
}, [setIsDeleteModalOpen]);
|
}, [setIsDeleteModalOpen]);
|
||||||
@@ -219,6 +232,9 @@ function ManageDownloadClientsModalContent(
|
|||||||
allSelected={allSelected}
|
allSelected={allSelected}
|
||||||
allUnselected={allUnselected}
|
allUnselected={allUnselected}
|
||||||
onSelectAllChange={onSelectAllChange}
|
onSelectAllChange={onSelectAllChange}
|
||||||
|
sortKey={sortKey}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
onSortPress={onSortPress}
|
||||||
>
|
>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{items.map((item) => {
|
{items.map((item) => {
|
||||||
|
|||||||
@@ -61,8 +61,12 @@ function DownloadClientOptions(props) {
|
|||||||
legend={translate('FailedDownloadHandling')}
|
legend={translate('FailedDownloadHandling')}
|
||||||
>
|
>
|
||||||
<Form>
|
<Form>
|
||||||
<FormGroup size={sizes.MEDIUM}>
|
<FormGroup
|
||||||
<FormLabel>{translate('RedownloadFailed')}</FormLabel>
|
advancedSettings={advancedSettings}
|
||||||
|
isAdvanced={true}
|
||||||
|
size={sizes.MEDIUM}
|
||||||
|
>
|
||||||
|
<FormLabel>{translate('AutoRedownloadFailed')}</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
@@ -72,7 +76,28 @@ function DownloadClientOptions(props) {
|
|||||||
{...settings.autoRedownloadFailed}
|
{...settings.autoRedownloadFailed}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
{
|
||||||
|
settings.autoRedownloadFailed.value ?
|
||||||
|
<FormGroup
|
||||||
|
advancedSettings={advancedSettings}
|
||||||
|
isAdvanced={true}
|
||||||
|
size={sizes.MEDIUM}
|
||||||
|
>
|
||||||
|
<FormLabel>{translate('AutoRedownloadFailedFromInteractiveSearch')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="autoRedownloadFailedFromInteractiveSearch"
|
||||||
|
helpText={translate('AutoRedownloadFailedFromInteractiveSearchHelpText')}
|
||||||
|
onChange={onInputChange}
|
||||||
|
{...settings.autoRedownloadFailedFromInteractiveSearch}
|
||||||
|
/>
|
||||||
|
</FormGroup> :
|
||||||
|
null
|
||||||
|
}
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
<Alert kind={kinds.INFO}>
|
<Alert kind={kinds.INFO}>
|
||||||
{translate('RemoveDownloadsAlert')}
|
{translate('RemoveDownloadsAlert')}
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|||||||
@@ -8,11 +8,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.name {
|
.name {
|
||||||
flex: 1 0 300px;
|
@add-mixin truncate;
|
||||||
|
|
||||||
|
flex: 0 1 600px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.foreignId {
|
.foreignId {
|
||||||
flex: 0 0 200px;
|
flex: 0 0 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
|
|||||||
@@ -4,12 +4,12 @@
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.foreignId {
|
.name {
|
||||||
flex: 0 0 200px;
|
flex: 0 1 600px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.name {
|
.foreignId {
|
||||||
flex: 1 0 300px;
|
flex: 0 0 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.addImportListExclusion {
|
.addImportListExclusion {
|
||||||
|
|||||||
@@ -14,9 +14,11 @@ 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,
|
||||||
|
setManageIndexersSort,
|
||||||
} from 'Store/Actions/settingsActions';
|
} from 'Store/Actions/settingsActions';
|
||||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
import { SelectStateInputProps } from 'typings/props';
|
import { SelectStateInputProps } from 'typings/props';
|
||||||
@@ -80,6 +82,8 @@ const COLUMNS = [
|
|||||||
|
|
||||||
interface ManageIndexersModalContentProps {
|
interface ManageIndexersModalContentProps {
|
||||||
onModalClose(): void;
|
onModalClose(): void;
|
||||||
|
sortKey?: string;
|
||||||
|
sortDirection?: SortDirection;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
||||||
@@ -92,6 +96,8 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
|||||||
isSaving,
|
isSaving,
|
||||||
error,
|
error,
|
||||||
items,
|
items,
|
||||||
|
sortKey,
|
||||||
|
sortDirection,
|
||||||
}: IndexerAppState = useSelector(
|
}: IndexerAppState = useSelector(
|
||||||
createClientSideCollectionSelector('settings.indexers')
|
createClientSideCollectionSelector('settings.indexers')
|
||||||
);
|
);
|
||||||
@@ -112,6 +118,13 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
|||||||
|
|
||||||
const selectedCount = selectedIds.length;
|
const selectedCount = selectedIds.length;
|
||||||
|
|
||||||
|
const onSortPress = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
dispatch(setManageIndexersSort({ sortKey: value }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
const onDeletePress = useCallback(() => {
|
const onDeletePress = useCallback(() => {
|
||||||
setIsDeleteModalOpen(true);
|
setIsDeleteModalOpen(true);
|
||||||
}, [setIsDeleteModalOpen]);
|
}, [setIsDeleteModalOpen]);
|
||||||
@@ -214,6 +227,9 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
|||||||
allSelected={allSelected}
|
allSelected={allSelected}
|
||||||
allUnselected={allUnselected}
|
allUnselected={allUnselected}
|
||||||
onSelectAllChange={onSelectAllChange}
|
onSelectAllChange={onSelectAllChange}
|
||||||
|
sortKey={sortKey}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
onSortPress={onSortPress}
|
||||||
>
|
>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{items.map((item) => {
|
{items.map((item) => {
|
||||||
|
|||||||
@@ -212,26 +212,24 @@ class MediaManagement extends Component {
|
|||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
{
|
{
|
||||||
settings.importExtraFiles.value &&
|
settings.importExtraFiles.value ?
|
||||||
<FormGroup
|
<FormGroup
|
||||||
advancedSettings={advancedSettings}
|
advancedSettings={advancedSettings}
|
||||||
isAdvanced={true}
|
isAdvanced={true}
|
||||||
>
|
>
|
||||||
<FormLabel>
|
<FormLabel>{translate('ImportExtraFiles')}</FormLabel>
|
||||||
{translate('ImportExtraFiles')}
|
|
||||||
</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.TEXT}
|
type={inputTypes.TEXT}
|
||||||
name="extraFileExtensions"
|
name="extraFileExtensions"
|
||||||
helpTexts={[
|
helpTexts={[
|
||||||
translate('ExtraFileExtensionsHelpTexts1'),
|
translate('ExtraFileExtensionsHelpText'),
|
||||||
translate('ExtraFileExtensionsHelpTexts2')
|
translate('ExtraFileExtensionsHelpTextsExamples')
|
||||||
]}
|
]}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...settings.extraFileExtensions}
|
{...settings.extraFileExtensions}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup> : null
|
||||||
}
|
}
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -223,6 +223,13 @@ class UISettings extends Component {
|
|||||||
helpTextWarning={translate('UILanguageHelpTextWarning')}
|
helpTextWarning={translate('UILanguageHelpTextWarning')}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...settings.uiLanguage}
|
{...settings.uiLanguage}
|
||||||
|
errors={
|
||||||
|
languages.some((language) => language.key === settings.uiLanguage.value) ?
|
||||||
|
settings.uiLanguage.errors :
|
||||||
|
[
|
||||||
|
...settings.uiLanguage.errors,
|
||||||
|
{ message: translate('InvalidUILanguage') }
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { createAction } from 'redux-actions';
|
import { createAction } from 'redux-actions';
|
||||||
|
import { sortDirections } from 'Helpers/Props';
|
||||||
import createBulkEditItemHandler from 'Store/Actions/Creators/createBulkEditItemHandler';
|
import createBulkEditItemHandler from 'Store/Actions/Creators/createBulkEditItemHandler';
|
||||||
import createBulkRemoveItemHandler from 'Store/Actions/Creators/createBulkRemoveItemHandler';
|
import createBulkRemoveItemHandler from 'Store/Actions/Creators/createBulkRemoveItemHandler';
|
||||||
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
|
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
|
||||||
@@ -7,6 +8,7 @@ import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHand
|
|||||||
import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler';
|
import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler';
|
||||||
import createTestAllProvidersHandler from 'Store/Actions/Creators/createTestAllProvidersHandler';
|
import createTestAllProvidersHandler from 'Store/Actions/Creators/createTestAllProvidersHandler';
|
||||||
import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler';
|
import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler';
|
||||||
|
import createSetClientSideCollectionSortReducer from 'Store/Actions/Creators/Reducers/createSetClientSideCollectionSortReducer';
|
||||||
import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
|
import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
|
||||||
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
||||||
import { createThunk } from 'Store/thunks';
|
import { createThunk } from 'Store/thunks';
|
||||||
@@ -33,6 +35,7 @@ export const CANCEL_TEST_DOWNLOAD_CLIENT = 'settings/downloadClients/cancelTestD
|
|||||||
export const TEST_ALL_DOWNLOAD_CLIENTS = 'settings/downloadClients/testAllDownloadClients';
|
export const TEST_ALL_DOWNLOAD_CLIENTS = 'settings/downloadClients/testAllDownloadClients';
|
||||||
export const BULK_EDIT_DOWNLOAD_CLIENTS = 'settings/downloadClients/bulkEditDownloadClients';
|
export const BULK_EDIT_DOWNLOAD_CLIENTS = 'settings/downloadClients/bulkEditDownloadClients';
|
||||||
export const BULK_DELETE_DOWNLOAD_CLIENTS = 'settings/downloadClients/bulkDeleteDownloadClients';
|
export const BULK_DELETE_DOWNLOAD_CLIENTS = 'settings/downloadClients/bulkDeleteDownloadClients';
|
||||||
|
export const SET_MANAGE_DOWNLOAD_CLIENTS_SORT = 'settings/downloadClients/setManageDownloadClientsSort';
|
||||||
|
|
||||||
//
|
//
|
||||||
// Action Creators
|
// Action Creators
|
||||||
@@ -49,6 +52,7 @@ export const cancelTestDownloadClient = createThunk(CANCEL_TEST_DOWNLOAD_CLIENT)
|
|||||||
export const testAllDownloadClients = createThunk(TEST_ALL_DOWNLOAD_CLIENTS);
|
export const testAllDownloadClients = createThunk(TEST_ALL_DOWNLOAD_CLIENTS);
|
||||||
export const bulkEditDownloadClients = createThunk(BULK_EDIT_DOWNLOAD_CLIENTS);
|
export const bulkEditDownloadClients = createThunk(BULK_EDIT_DOWNLOAD_CLIENTS);
|
||||||
export const bulkDeleteDownloadClients = createThunk(BULK_DELETE_DOWNLOAD_CLIENTS);
|
export const bulkDeleteDownloadClients = createThunk(BULK_DELETE_DOWNLOAD_CLIENTS);
|
||||||
|
export const setManageDownloadClientsSort = createAction(SET_MANAGE_DOWNLOAD_CLIENTS_SORT);
|
||||||
|
|
||||||
export const setDownloadClientValue = createAction(SET_DOWNLOAD_CLIENT_VALUE, (payload) => {
|
export const setDownloadClientValue = createAction(SET_DOWNLOAD_CLIENT_VALUE, (payload) => {
|
||||||
return {
|
return {
|
||||||
@@ -88,7 +92,9 @@ export default {
|
|||||||
isTesting: false,
|
isTesting: false,
|
||||||
isTestingAll: false,
|
isTestingAll: false,
|
||||||
items: [],
|
items: [],
|
||||||
pendingChanges: {}
|
pendingChanges: {},
|
||||||
|
sortKey: 'name',
|
||||||
|
sortDirection: sortDirections.DESCENDING
|
||||||
},
|
},
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -121,7 +127,10 @@ export default {
|
|||||||
|
|
||||||
return selectedSchema;
|
return selectedSchema;
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
|
[SET_MANAGE_DOWNLOAD_CLIENTS_SORT]: createSetClientSideCollectionSortReducer(section)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { createAction } from 'redux-actions';
|
import { createAction } from 'redux-actions';
|
||||||
|
import { sortDirections } from 'Helpers/Props';
|
||||||
import createBulkEditItemHandler from 'Store/Actions/Creators/createBulkEditItemHandler';
|
import createBulkEditItemHandler from 'Store/Actions/Creators/createBulkEditItemHandler';
|
||||||
import createBulkRemoveItemHandler from 'Store/Actions/Creators/createBulkRemoveItemHandler';
|
import createBulkRemoveItemHandler from 'Store/Actions/Creators/createBulkRemoveItemHandler';
|
||||||
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
|
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
|
||||||
@@ -7,6 +8,7 @@ import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHand
|
|||||||
import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler';
|
import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler';
|
||||||
import createTestAllProvidersHandler from 'Store/Actions/Creators/createTestAllProvidersHandler';
|
import createTestAllProvidersHandler from 'Store/Actions/Creators/createTestAllProvidersHandler';
|
||||||
import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler';
|
import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler';
|
||||||
|
import createSetClientSideCollectionSortReducer from 'Store/Actions/Creators/Reducers/createSetClientSideCollectionSortReducer';
|
||||||
import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
|
import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
|
||||||
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
||||||
import { createThunk } from 'Store/thunks';
|
import { createThunk } from 'Store/thunks';
|
||||||
@@ -36,6 +38,7 @@ export const CANCEL_TEST_INDEXER = 'settings/indexers/cancelTestIndexer';
|
|||||||
export const TEST_ALL_INDEXERS = 'settings/indexers/testAllIndexers';
|
export const TEST_ALL_INDEXERS = 'settings/indexers/testAllIndexers';
|
||||||
export const BULK_EDIT_INDEXERS = 'settings/indexers/bulkEditIndexers';
|
export const BULK_EDIT_INDEXERS = 'settings/indexers/bulkEditIndexers';
|
||||||
export const BULK_DELETE_INDEXERS = 'settings/indexers/bulkDeleteIndexers';
|
export const BULK_DELETE_INDEXERS = 'settings/indexers/bulkDeleteIndexers';
|
||||||
|
export const SET_MANAGE_INDEXERS_SORT = 'settings/indexers/setManageIndexersSort';
|
||||||
|
|
||||||
//
|
//
|
||||||
// Action Creators
|
// Action Creators
|
||||||
@@ -53,6 +56,7 @@ export const cancelTestIndexer = createThunk(CANCEL_TEST_INDEXER);
|
|||||||
export const testAllIndexers = createThunk(TEST_ALL_INDEXERS);
|
export const testAllIndexers = createThunk(TEST_ALL_INDEXERS);
|
||||||
export const bulkEditIndexers = createThunk(BULK_EDIT_INDEXERS);
|
export const bulkEditIndexers = createThunk(BULK_EDIT_INDEXERS);
|
||||||
export const bulkDeleteIndexers = createThunk(BULK_DELETE_INDEXERS);
|
export const bulkDeleteIndexers = createThunk(BULK_DELETE_INDEXERS);
|
||||||
|
export const setManageIndexersSort = createAction(SET_MANAGE_INDEXERS_SORT);
|
||||||
|
|
||||||
export const setIndexerValue = createAction(SET_INDEXER_VALUE, (payload) => {
|
export const setIndexerValue = createAction(SET_INDEXER_VALUE, (payload) => {
|
||||||
return {
|
return {
|
||||||
@@ -92,7 +96,9 @@ export default {
|
|||||||
isTesting: false,
|
isTesting: false,
|
||||||
isTestingAll: false,
|
isTestingAll: false,
|
||||||
items: [],
|
items: [],
|
||||||
pendingChanges: {}
|
pendingChanges: {},
|
||||||
|
sortKey: 'name',
|
||||||
|
sortDirection: sortDirections.DESCENDING
|
||||||
},
|
},
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -151,7 +157,10 @@ export default {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return updateSectionState(state, section, newState);
|
return updateSectionState(state, section, newState);
|
||||||
}
|
},
|
||||||
|
|
||||||
|
[SET_MANAGE_INDEXERS_SORT]: createSetClientSideCollectionSortReducer(section)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -41,6 +41,14 @@ export const defaultState = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
columns: [
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'select',
|
||||||
|
columnLabel: 'Select',
|
||||||
|
isSortable: false,
|
||||||
|
isVisible: true,
|
||||||
|
isModifiable: false,
|
||||||
|
isHidden: true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'path',
|
name: 'path',
|
||||||
label: 'Path',
|
label: 'Path',
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ export const defaultState = {
|
|||||||
bookFileCount: function(item) {
|
bookFileCount: function(item) {
|
||||||
const { statistics = {} } = item;
|
const { statistics = {} } = item;
|
||||||
|
|
||||||
return statistics.bookCount || 0;
|
return statistics.bookFileCount || 0;
|
||||||
},
|
},
|
||||||
|
|
||||||
ratings: function(item) {
|
ratings: function(item) {
|
||||||
|
|||||||
@@ -84,11 +84,6 @@ export const defaultState = {
|
|||||||
label: 'Source Title',
|
label: 'Source Title',
|
||||||
isVisible: false
|
isVisible: false
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'sourceTitle',
|
|
||||||
label: 'Source Title',
|
|
||||||
isVisible: false
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'customFormatScore',
|
name: 'customFormatScore',
|
||||||
columnLabel: 'Custom Format Score',
|
columnLabel: 'Custom Format Score',
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { sortDirections } from 'Helpers/Props';
|
|||||||
import { createThunk, handleThunks } from 'Store/thunks';
|
import { createThunk, handleThunks } from 'Store/thunks';
|
||||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||||
import updateSectionState from 'Utilities/State/updateSectionState';
|
import updateSectionState from 'Utilities/State/updateSectionState';
|
||||||
|
import naturalExpansion from 'Utilities/String/naturalExpansion';
|
||||||
import { set, update, updateItem } from './baseActions';
|
import { set, update, updateItem } from './baseActions';
|
||||||
import createFetchHandler from './Creators/createFetchHandler';
|
import createFetchHandler from './Creators/createFetchHandler';
|
||||||
import createHandleActions from './Creators/createHandleActions';
|
import createHandleActions from './Creators/createHandleActions';
|
||||||
@@ -17,6 +18,7 @@ export const section = 'interactiveImport';
|
|||||||
|
|
||||||
const booksSection = `${section}.books`;
|
const booksSection = `${section}.books`;
|
||||||
const bookFilesSection = `${section}.bookFiles`;
|
const bookFilesSection = `${section}.bookFiles`;
|
||||||
|
let abortCurrentFetchRequest = null;
|
||||||
let abortCurrentRequest = null;
|
let abortCurrentRequest = null;
|
||||||
let currentIds = [];
|
let currentIds = [];
|
||||||
|
|
||||||
@@ -32,15 +34,17 @@ export const defaultState = {
|
|||||||
error: null,
|
error: null,
|
||||||
items: [],
|
items: [],
|
||||||
pendingChanges: {},
|
pendingChanges: {},
|
||||||
sortKey: 'quality',
|
sortKey: 'path',
|
||||||
sortDirection: sortDirections.DESCENDING,
|
sortDirection: sortDirections.ASCENDING,
|
||||||
|
secondarySortKey: 'path',
|
||||||
|
secondarySortDirection: sortDirections.ASCENDING,
|
||||||
recentFolders: [],
|
recentFolders: [],
|
||||||
importMode: 'chooseImportMode',
|
importMode: 'chooseImportMode',
|
||||||
sortPredicates: {
|
sortPredicates: {
|
||||||
path: function(item, direction) {
|
path: function(item, direction) {
|
||||||
const path = item.path;
|
const path = item.path;
|
||||||
|
|
||||||
return path.toLowerCase();
|
return naturalExpansion(path.toLowerCase());
|
||||||
},
|
},
|
||||||
|
|
||||||
author: function(item, direction) {
|
author: function(item, direction) {
|
||||||
@@ -74,6 +78,8 @@ export const defaultState = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const persistState = [
|
export const persistState = [
|
||||||
|
'interactiveImport.sortKey',
|
||||||
|
'interactiveImport.sortDirection',
|
||||||
'interactiveImport.recentFolders',
|
'interactiveImport.recentFolders',
|
||||||
'interactiveImport.importMode'
|
'interactiveImport.importMode'
|
||||||
];
|
];
|
||||||
@@ -122,6 +128,11 @@ export const clearInteractiveImportBookFiles = createAction(CLEAR_INTERACTIVE_IM
|
|||||||
// Action Handlers
|
// Action Handlers
|
||||||
export const actionHandlers = handleThunks({
|
export const actionHandlers = handleThunks({
|
||||||
[FETCH_INTERACTIVE_IMPORT_ITEMS]: function(getState, payload, dispatch) {
|
[FETCH_INTERACTIVE_IMPORT_ITEMS]: function(getState, payload, dispatch) {
|
||||||
|
if (abortCurrentFetchRequest) {
|
||||||
|
abortCurrentFetchRequest();
|
||||||
|
abortCurrentFetchRequest = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (!payload.downloadId && !payload.folder) {
|
if (!payload.downloadId && !payload.folder) {
|
||||||
dispatch(set({ section, error: { message: '`downloadId` or `folder` is required.' } }));
|
dispatch(set({ section, error: { message: '`downloadId` or `folder` is required.' } }));
|
||||||
return;
|
return;
|
||||||
@@ -129,12 +140,14 @@ export const actionHandlers = handleThunks({
|
|||||||
|
|
||||||
dispatch(set({ section, isFetching: true }));
|
dispatch(set({ section, isFetching: true }));
|
||||||
|
|
||||||
const promise = createAjaxRequest({
|
const { request, abortRequest } = createAjaxRequest({
|
||||||
url: '/manualimport',
|
url: '/manualimport',
|
||||||
data: payload
|
data: payload
|
||||||
}).request;
|
});
|
||||||
|
|
||||||
promise.done((data) => {
|
abortCurrentFetchRequest = abortRequest;
|
||||||
|
|
||||||
|
request.done((data) => {
|
||||||
dispatch(batchActions([
|
dispatch(batchActions([
|
||||||
update({ section, data }),
|
update({ section, data }),
|
||||||
|
|
||||||
@@ -147,7 +160,11 @@ export const actionHandlers = handleThunks({
|
|||||||
]));
|
]));
|
||||||
});
|
});
|
||||||
|
|
||||||
promise.fail((xhr) => {
|
request.fail((xhr) => {
|
||||||
|
if (xhr.aborted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
dispatch(set({
|
dispatch(set({
|
||||||
section,
|
section,
|
||||||
isFetching: false,
|
isFetching: false,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
import PageContent from 'Components/Page/PageContent';
|
import PageContent from 'Components/Page/PageContent';
|
||||||
import PageContentBody from 'Components/Page/PageContentBody';
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
@@ -9,8 +10,12 @@ import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
|||||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||||
import VirtualTable from 'Components/Table/VirtualTable';
|
import VirtualTable from 'Components/Table/VirtualTable';
|
||||||
import VirtualTableRow from 'Components/Table/VirtualTableRow';
|
import VirtualTableRow from 'Components/Table/VirtualTableRow';
|
||||||
import { align, icons, sortDirections } from 'Helpers/Props';
|
import { align, icons, kinds, sortDirections } from 'Helpers/Props';
|
||||||
|
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
|
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||||
|
import selectAll from 'Utilities/Table/selectAll';
|
||||||
|
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||||
import UnmappedFilesTableHeader from './UnmappedFilesTableHeader';
|
import UnmappedFilesTableHeader from './UnmappedFilesTableHeader';
|
||||||
import UnmappedFilesTableRow from './UnmappedFilesTableRow';
|
import UnmappedFilesTableRow from './UnmappedFilesTableRow';
|
||||||
|
|
||||||
@@ -23,10 +28,43 @@ class UnmappedFilesTable extends Component {
|
|||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
scroller: null
|
scroller: null,
|
||||||
|
allSelected: false,
|
||||||
|
allUnselected: false,
|
||||||
|
lastToggled: null,
|
||||||
|
selectedState: {}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.setSelectedState();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
const {
|
||||||
|
items,
|
||||||
|
sortKey,
|
||||||
|
sortDirection,
|
||||||
|
isDeleting,
|
||||||
|
deleteError
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (sortKey !== prevProps.sortKey ||
|
||||||
|
sortDirection !== prevProps.sortDirection ||
|
||||||
|
hasDifferentItemsOrOrder(prevProps.items, items)
|
||||||
|
) {
|
||||||
|
this.setSelectedState();
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasFinishedDeleting = prevProps.isDeleting &&
|
||||||
|
!isDeleting &&
|
||||||
|
!deleteError;
|
||||||
|
|
||||||
|
if (hasFinishedDeleting) {
|
||||||
|
this.onSelectAllChange({ value: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Control
|
// Control
|
||||||
|
|
||||||
@@ -34,6 +72,68 @@ class UnmappedFilesTable extends Component {
|
|||||||
this.setState({ scroller: ref });
|
this.setState({ scroller: ref });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
getSelectedIds = () => {
|
||||||
|
if (this.state.allUnselected) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return getSelectedIds(this.state.selectedState);
|
||||||
|
};
|
||||||
|
|
||||||
|
setSelectedState() {
|
||||||
|
const {
|
||||||
|
items
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
selectedState
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const newSelectedState = {};
|
||||||
|
|
||||||
|
items.forEach((file) => {
|
||||||
|
const isItemSelected = selectedState[file.id];
|
||||||
|
|
||||||
|
if (isItemSelected) {
|
||||||
|
newSelectedState[file.id] = isItemSelected;
|
||||||
|
} else {
|
||||||
|
newSelectedState[file.id] = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedCount = getSelectedIds(newSelectedState).length;
|
||||||
|
const newStateCount = Object.keys(newSelectedState).length;
|
||||||
|
let isAllSelected = false;
|
||||||
|
let isAllUnselected = false;
|
||||||
|
|
||||||
|
if (selectedCount === 0) {
|
||||||
|
isAllUnselected = true;
|
||||||
|
} else if (selectedCount === newStateCount) {
|
||||||
|
isAllSelected = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ selectedState: newSelectedState, allSelected: isAllSelected, allUnselected: isAllUnselected });
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelectAllChange = ({ value }) => {
|
||||||
|
this.setState(selectAll(this.state.selectedState, value));
|
||||||
|
};
|
||||||
|
|
||||||
|
onSelectAllPress = () => {
|
||||||
|
this.onSelectAllChange({ value: !this.state.allSelected });
|
||||||
|
};
|
||||||
|
|
||||||
|
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
||||||
|
this.setState((state) => {
|
||||||
|
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onDeleteUnmappedFilesPress = () => {
|
||||||
|
const selectedIds = this.getSelectedIds();
|
||||||
|
|
||||||
|
this.props.deleteUnmappedFiles(selectedIds);
|
||||||
|
};
|
||||||
|
|
||||||
rowRenderer = ({ key, rowIndex, style }) => {
|
rowRenderer = ({ key, rowIndex, style }) => {
|
||||||
const {
|
const {
|
||||||
items,
|
items,
|
||||||
@@ -41,6 +141,10 @@ class UnmappedFilesTable extends Component {
|
|||||||
deleteUnmappedFile
|
deleteUnmappedFile
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
selectedState
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
const item = items[rowIndex];
|
const item = items[rowIndex];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -51,6 +155,8 @@ class UnmappedFilesTable extends Component {
|
|||||||
<UnmappedFilesTableRow
|
<UnmappedFilesTableRow
|
||||||
key={item.id}
|
key={item.id}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
isSelected={selectedState[item.id]}
|
||||||
|
onSelectedChange={this.onSelectedChange}
|
||||||
deleteUnmappedFile={deleteUnmappedFile}
|
deleteUnmappedFile={deleteUnmappedFile}
|
||||||
{...item}
|
{...item}
|
||||||
/>
|
/>
|
||||||
@@ -63,6 +169,7 @@ class UnmappedFilesTable extends Component {
|
|||||||
const {
|
const {
|
||||||
isFetching,
|
isFetching,
|
||||||
isPopulated,
|
isPopulated,
|
||||||
|
isDeleting,
|
||||||
error,
|
error,
|
||||||
items,
|
items,
|
||||||
columns,
|
columns,
|
||||||
@@ -72,13 +179,19 @@ class UnmappedFilesTable extends Component {
|
|||||||
onSortPress,
|
onSortPress,
|
||||||
isScanningFolders,
|
isScanningFolders,
|
||||||
onAddMissingAuthorsPress,
|
onAddMissingAuthorsPress,
|
||||||
|
deleteUnmappedFiles,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
scroller
|
scroller,
|
||||||
|
allSelected,
|
||||||
|
allUnselected,
|
||||||
|
selectedState
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
|
const selectedTrackFileIds = this.getSelectedIds();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContent title={translate('UnmappedFiles')}>
|
<PageContent title={translate('UnmappedFiles')}>
|
||||||
<PageToolbar>
|
<PageToolbar>
|
||||||
@@ -90,6 +203,13 @@ class UnmappedFilesTable extends Component {
|
|||||||
isSpinning={isScanningFolders}
|
isSpinning={isScanningFolders}
|
||||||
onPress={onAddMissingAuthorsPress}
|
onPress={onAddMissingAuthorsPress}
|
||||||
/>
|
/>
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('DeleteSelected')}
|
||||||
|
iconName={icons.DELETE}
|
||||||
|
isDisabled={selectedTrackFileIds.length === 0}
|
||||||
|
isSpinning={isDeleting}
|
||||||
|
onPress={this.onDeleteUnmappedFilesPress}
|
||||||
|
/>
|
||||||
</PageToolbarSection>
|
</PageToolbarSection>
|
||||||
|
|
||||||
<PageToolbarSection alignContent={align.RIGHT}>
|
<PageToolbarSection alignContent={align.RIGHT}>
|
||||||
@@ -117,9 +237,9 @@ class UnmappedFilesTable extends Component {
|
|||||||
|
|
||||||
{
|
{
|
||||||
isPopulated && !error && !items.length &&
|
isPopulated && !error && !items.length &&
|
||||||
<div>
|
<Alert kind={kinds.INFO}>
|
||||||
Success! My work is done, all files on disk are matched to known books.
|
Success! My work is done, all files on disk are matched to known books.
|
||||||
</div>
|
</Alert>
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -138,8 +258,12 @@ class UnmappedFilesTable extends Component {
|
|||||||
sortDirection={sortDirection}
|
sortDirection={sortDirection}
|
||||||
onTableOptionChange={onTableOptionChange}
|
onTableOptionChange={onTableOptionChange}
|
||||||
onSortPress={onSortPress}
|
onSortPress={onSortPress}
|
||||||
|
allSelected={allSelected}
|
||||||
|
allUnselected={allUnselected}
|
||||||
|
onSelectAllChange={this.onSelectAllChange}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
selectedState={selectedState}
|
||||||
sortKey={sortKey}
|
sortKey={sortKey}
|
||||||
sortDirection={sortDirection}
|
sortDirection={sortDirection}
|
||||||
/>
|
/>
|
||||||
@@ -153,6 +277,8 @@ class UnmappedFilesTable extends Component {
|
|||||||
UnmappedFilesTable.propTypes = {
|
UnmappedFilesTable.propTypes = {
|
||||||
isFetching: PropTypes.bool.isRequired,
|
isFetching: PropTypes.bool.isRequired,
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
|
isDeleting: PropTypes.bool.isRequired,
|
||||||
|
deleteError: PropTypes.object,
|
||||||
error: PropTypes.object,
|
error: PropTypes.object,
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
@@ -161,6 +287,7 @@ UnmappedFilesTable.propTypes = {
|
|||||||
onTableOptionChange: PropTypes.func.isRequired,
|
onTableOptionChange: PropTypes.func.isRequired,
|
||||||
onSortPress: PropTypes.func.isRequired,
|
onSortPress: PropTypes.func.isRequired,
|
||||||
deleteUnmappedFile: PropTypes.func.isRequired,
|
deleteUnmappedFile: PropTypes.func.isRequired,
|
||||||
|
deleteUnmappedFiles: PropTypes.func.isRequired,
|
||||||
isScanningFolders: PropTypes.bool.isRequired,
|
isScanningFolders: PropTypes.bool.isRequired,
|
||||||
onAddMissingAuthorsPress: PropTypes.func.isRequired
|
onAddMissingAuthorsPress: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { connect } from 'react-redux';
|
|||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import * as commandNames from 'Commands/commandNames';
|
import * as commandNames from 'Commands/commandNames';
|
||||||
import withCurrentPage from 'Components/withCurrentPage';
|
import withCurrentPage from 'Components/withCurrentPage';
|
||||||
import { deleteBookFile, fetchBookFiles, setBookFilesSort, setBookFilesTableOption } from 'Store/Actions/bookFileActions';
|
import { deleteBookFile, deleteBookFiles, fetchBookFiles, setBookFilesSort, setBookFilesTableOption } from 'Store/Actions/bookFileActions';
|
||||||
import { executeCommand } from 'Store/Actions/commandActions';
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
@@ -28,7 +28,9 @@ function createMapStateToProps() {
|
|||||||
items,
|
items,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = bookFiles;
|
} = bookFiles;
|
||||||
|
|
||||||
const unmappedFiles = _.filter(items, { bookId: 0 });
|
const unmappedFiles = _.filter(items, { bookId: 0 });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items: unmappedFiles,
|
items: unmappedFiles,
|
||||||
...otherProps,
|
...otherProps,
|
||||||
@@ -57,6 +59,10 @@ function createMapDispatchToProps(dispatch, props) {
|
|||||||
dispatch(deleteBookFile({ id }));
|
dispatch(deleteBookFile({ id }));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
deleteUnmappedFiles(bookFileIds) {
|
||||||
|
dispatch(deleteBookFiles({ bookFileIds }));
|
||||||
|
},
|
||||||
|
|
||||||
onAddMissingAuthorsPress() {
|
onAddMissingAuthorsPress() {
|
||||||
dispatch(executeCommand({
|
dispatch(executeCommand({
|
||||||
name: commandNames.RESCAN_FOLDERS,
|
name: commandNames.RESCAN_FOLDERS,
|
||||||
@@ -106,7 +112,8 @@ UnmappedFilesTableConnector.propTypes = {
|
|||||||
onSortPress: PropTypes.func.isRequired,
|
onSortPress: PropTypes.func.isRequired,
|
||||||
onTableOptionChange: PropTypes.func.isRequired,
|
onTableOptionChange: PropTypes.func.isRequired,
|
||||||
fetchUnmappedFiles: PropTypes.func.isRequired,
|
fetchUnmappedFiles: PropTypes.func.isRequired,
|
||||||
deleteUnmappedFile: PropTypes.func.isRequired
|
deleteUnmappedFile: PropTypes.func.isRequired,
|
||||||
|
deleteUnmappedFiles: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withCurrentPage(
|
export default withCurrentPage(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import IconButton from 'Components/Link/IconButton';
|
|||||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||||
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
|
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
|
||||||
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
|
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
|
||||||
|
import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
// import hasGrowableColumns from './hasGrowableColumns';
|
// import hasGrowableColumns from './hasGrowableColumns';
|
||||||
import styles from './UnmappedFilesTableHeader.css';
|
import styles from './UnmappedFilesTableHeader.css';
|
||||||
@@ -12,6 +13,9 @@ function UnmappedFilesTableHeader(props) {
|
|||||||
const {
|
const {
|
||||||
columns,
|
columns,
|
||||||
onTableOptionChange,
|
onTableOptionChange,
|
||||||
|
allSelected,
|
||||||
|
allUnselected,
|
||||||
|
onSelectAllChange,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
@@ -30,6 +34,17 @@ function UnmappedFilesTableHeader(props) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (name === 'select') {
|
||||||
|
return (
|
||||||
|
<VirtualTableSelectAllHeaderCell
|
||||||
|
key={name}
|
||||||
|
allSelected={allSelected}
|
||||||
|
allUnselected={allUnselected}
|
||||||
|
onSelectAllChange={onSelectAllChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (name === 'actions') {
|
if (name === 'actions') {
|
||||||
return (
|
return (
|
||||||
<VirtualTableHeaderCell
|
<VirtualTableHeaderCell
|
||||||
@@ -71,6 +86,9 @@ function UnmappedFilesTableHeader(props) {
|
|||||||
|
|
||||||
UnmappedFilesTableHeader.propTypes = {
|
UnmappedFilesTableHeader.propTypes = {
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
allSelected: PropTypes.bool.isRequired,
|
||||||
|
allUnselected: PropTypes.bool.isRequired,
|
||||||
|
onSelectAllChange: PropTypes.func.isRequired,
|
||||||
onTableOptionChange: PropTypes.func.isRequired
|
onTableOptionChange: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -20,3 +20,9 @@
|
|||||||
|
|
||||||
flex: 0 0 100px;
|
flex: 0 0 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.checkInput {
|
||||||
|
composes: input from '~Components/Form/CheckInput.css';
|
||||||
|
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'actions': string;
|
'actions': string;
|
||||||
|
'checkInput': string;
|
||||||
'dateAdded': string;
|
'dateAdded': string;
|
||||||
'path': string;
|
'path': string;
|
||||||
'quality': string;
|
'quality': string;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import IconButton from 'Components/Link/IconButton';
|
|||||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||||
|
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
@@ -69,7 +70,9 @@ class UnmappedFilesTableRow extends Component {
|
|||||||
size,
|
size,
|
||||||
dateAdded,
|
dateAdded,
|
||||||
quality,
|
quality,
|
||||||
columns
|
columns,
|
||||||
|
isSelected,
|
||||||
|
onSelectedChange
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const folder = path.substring(0, Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')));
|
const folder = path.substring(0, Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')));
|
||||||
@@ -93,6 +96,19 @@ class UnmappedFilesTableRow extends Component {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (name === 'select') {
|
||||||
|
return (
|
||||||
|
<VirtualTableSelectCell
|
||||||
|
inputClassName={styles.checkInput}
|
||||||
|
id={id}
|
||||||
|
key={name}
|
||||||
|
isSelected={isSelected}
|
||||||
|
isDisabled={false}
|
||||||
|
onSelectedChange={onSelectedChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (name === 'path') {
|
if (name === 'path') {
|
||||||
return (
|
return (
|
||||||
<VirtualTableRowCell
|
<VirtualTableRowCell
|
||||||
@@ -208,6 +224,8 @@ UnmappedFilesTableRow.propTypes = {
|
|||||||
quality: PropTypes.object.isRequired,
|
quality: PropTypes.object.isRequired,
|
||||||
dateAdded: PropTypes.string.isRequired,
|
dateAdded: PropTypes.string.isRequired,
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
isSelected: PropTypes.bool,
|
||||||
|
onSelectedChange: PropTypes.func.isRequired,
|
||||||
deleteUnmappedFile: PropTypes.func.isRequired
|
deleteUnmappedFile: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
const regex = /\d+/g;
|
||||||
|
|
||||||
|
function naturalExpansion(input) {
|
||||||
|
if (!input) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return input.replace(regex, (n) => n.padStart(8, '0'));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default naturalExpansion;
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
|
const regex = /\b\w+/g;
|
||||||
|
|
||||||
function titleCase(input) {
|
function titleCase(input) {
|
||||||
if (!input) {
|
if (!input) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
return input.replace(/\b\w+/g, (match) => {
|
return input.replace(regex, (match) => {
|
||||||
return match.charAt(0).toUpperCase() + match.substr(1).toLowerCase();
|
return match.charAt(0).toUpperCase() + match.substr(1).toLowerCase();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,20 +25,18 @@ export async function fetchTranslations(): Promise<boolean> {
|
|||||||
|
|
||||||
export default function translate(
|
export default function translate(
|
||||||
key: string,
|
key: string,
|
||||||
tokens?: Record<string, string | number | boolean>
|
tokens: Record<string, string | number | boolean> = {}
|
||||||
) {
|
) {
|
||||||
const translation = translations[key] || key;
|
const translation = translations[key] || key;
|
||||||
|
|
||||||
if (tokens) {
|
tokens.appName = 'Readarr';
|
||||||
// Fallback to the old behaviour for translations not yet updated to use named tokens
|
|
||||||
Object.values(tokens).forEach((value, index) => {
|
|
||||||
tokens[index] = value;
|
|
||||||
});
|
|
||||||
|
|
||||||
return translation.replace(/\{([a-z0-9]+?)\}/gi, (match, tokenMatch) =>
|
// Fallback to the old behaviour for translations not yet updated to use named tokens
|
||||||
String(tokens[tokenMatch] ?? match)
|
Object.values(tokens).forEach((value, index) => {
|
||||||
);
|
tokens[index] = value;
|
||||||
}
|
});
|
||||||
|
|
||||||
return translation;
|
return translation.replace(/\{([a-z0-9]+?)\}/gi, (match, tokenMatch) =>
|
||||||
|
String(tokens[tokenMatch] ?? match)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ class CutoffUnmetConnector extends Component {
|
|||||||
gotoCutoffUnmetFirstPage
|
gotoCutoffUnmetFirstPage
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
registerPagePopulator(this.repopulate, ['bookFileUpdated']);
|
registerPagePopulator(this.repopulate, ['bookFileUpdated', 'bookFileDeleted']);
|
||||||
|
|
||||||
if (useCurrentPage) {
|
if (useCurrentPage) {
|
||||||
fetchCutoffUnmet();
|
fetchCutoffUnmet();
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ class MissingConnector extends Component {
|
|||||||
gotoMissingFirstPage
|
gotoMissingFirstPage
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
registerPagePopulator(this.repopulate, ['bookFileUpdated']);
|
registerPagePopulator(this.repopulate, ['bookFileUpdated', 'bookFileDeleted']);
|
||||||
|
|
||||||
if (useCurrentPage) {
|
if (useCurrentPage) {
|
||||||
fetchMissing();
|
fetchMissing();
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { render } from 'react-dom';
|
|||||||
import createAppStore from 'Store/createAppStore';
|
import createAppStore from 'Store/createAppStore';
|
||||||
import App from './App/App';
|
import App from './App/App';
|
||||||
|
|
||||||
|
import 'Diag/ConsoleApi';
|
||||||
|
|
||||||
export async function bootstrap() {
|
export async function bootstrap() {
|
||||||
const history = createBrowserHistory();
|
const history = createBrowserHistory();
|
||||||
const store = createAppStore(history);
|
const store = createAppStore(history);
|
||||||
|
|||||||
+1
-1
@@ -30,7 +30,7 @@
|
|||||||
"@fortawesome/free-regular-svg-icons": "6.4.0",
|
"@fortawesome/free-regular-svg-icons": "6.4.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "6.4.0",
|
"@fortawesome/free-solid-svg-icons": "6.4.0",
|
||||||
"@fortawesome/react-fontawesome": "0.2.0",
|
"@fortawesome/react-fontawesome": "0.2.0",
|
||||||
"@microsoft/signalr": "6.0.21",
|
"@microsoft/signalr": "6.0.25",
|
||||||
"@sentry/browser": "7.51.2",
|
"@sentry/browser": "7.51.2",
|
||||||
"@sentry/integrations": "7.51.2",
|
"@sentry/integrations": "7.51.2",
|
||||||
"@types/node": "18.16.16",
|
"@types/node": "18.16.16",
|
||||||
|
|||||||
@@ -4,10 +4,11 @@
|
|||||||
<PackageVersion Include="AutoFixture" Version="4.17.0" />
|
<PackageVersion Include="AutoFixture" Version="4.17.0" />
|
||||||
<PackageVersion Include="coverlet.collector" Version="3.0.4-preview.27.ge7cb7c3b40" PrivateAssets="all" />
|
<PackageVersion Include="coverlet.collector" Version="3.0.4-preview.27.ge7cb7c3b40" PrivateAssets="all" />
|
||||||
<PackageVersion Include="Dapper" Version="2.0.123" />
|
<PackageVersion Include="Dapper" Version="2.0.123" />
|
||||||
<PackageVersion Include="DryIoc.dll" Version="5.4.1" />
|
<PackageVersion Include="DryIoc.dll" Version="5.4.3" />
|
||||||
<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="Polly" Version="8.2.0" />
|
||||||
<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" />
|
||||||
@@ -16,11 +17,11 @@
|
|||||||
<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="3.6.0" />
|
<PackageVersion Include="Mailkit" Version="3.6.0" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.21" />
|
<PackageVersion Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.25" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" />
|
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" />
|
||||||
<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.1" />
|
<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.4.1" />
|
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
|
||||||
<PackageVersion Include="Microsoft.Win32.Registry" Version="5.0.0" />
|
<PackageVersion Include="Microsoft.Win32.Registry" Version="5.0.0" />
|
||||||
@@ -32,7 +33,7 @@
|
|||||||
<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.4" />
|
<PackageVersion Include="Npgsql" Version="7.0.6" />
|
||||||
<PackageVersion Include="NUnit3TestAdapter" Version="4.2.1" />
|
<PackageVersion Include="NUnit3TestAdapter" Version="4.2.1" />
|
||||||
<PackageVersion Include="NUnit" Version="3.13.3" />
|
<PackageVersion Include="NUnit" Version="3.13.3" />
|
||||||
<PackageVersion Include="NunitXml.TestLogger" Version="3.0.117" />
|
<PackageVersion Include="NunitXml.TestLogger" Version="3.0.117" />
|
||||||
@@ -59,7 +60,7 @@
|
|||||||
<PackageVersion Include="System.Security.Principal.Windows" Version="5.0.0" />
|
<PackageVersion Include="System.Security.Principal.Windows" Version="5.0.0" />
|
||||||
<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.8" />
|
<PackageVersion Include="System.Text.Json" Version="6.0.9" />
|
||||||
<PackageVersion Include="System.ValueTuple" Version="4.5.0" />
|
<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>
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
|
using Moq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NzbDrone.Core.Annotations;
|
using NzbDrone.Core.Annotations;
|
||||||
|
using NzbDrone.Core.Localization;
|
||||||
using NzbDrone.Test.Common;
|
using NzbDrone.Test.Common;
|
||||||
using Readarr.Http.ClientSchema;
|
using Readarr.Http.ClientSchema;
|
||||||
|
|
||||||
@@ -9,6 +12,16 @@ namespace NzbDrone.Api.Test.ClientSchemaTests
|
|||||||
[TestFixture]
|
[TestFixture]
|
||||||
public class SchemaBuilderFixture : TestBase
|
public class SchemaBuilderFixture : TestBase
|
||||||
{
|
{
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
Mocker.GetMock<ILocalizationService>()
|
||||||
|
.Setup(s => s.GetLocalizedString(It.IsAny<string>(), It.IsAny<Dictionary<string, object>>()))
|
||||||
|
.Returns<string, Dictionary<string, object>>((s, d) => s);
|
||||||
|
|
||||||
|
SchemaBuilder.Initialize(Mocker.Container);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void should_return_field_for_every_property()
|
public void should_return_field_for_every_property()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -124,6 +124,16 @@ namespace NzbDrone.Common.Test.Http
|
|||||||
response.Content.Should().NotBeNullOrWhiteSpace();
|
response.Content.Should().NotBeNullOrWhiteSpace();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_throw_timeout_request()
|
||||||
|
{
|
||||||
|
var request = new HttpRequest($"https://{_httpBinHost}/delay/10");
|
||||||
|
|
||||||
|
request.RequestTimeout = new TimeSpan(0, 0, 5);
|
||||||
|
|
||||||
|
Assert.ThrowsAsync<WebException>(async () => await Subject.ExecuteAsync(request));
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public async Task should_execute_https_get()
|
public async Task should_execute_https_get()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -70,15 +70,15 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
|||||||
[TestCase(@"[Info] MigrationController: *** Migrating Database=readarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;token=mySecret;Enlist=False&username=mySecret;mypassword=mySecret;mypass=shouldkeep1;test_token=mySecret;password=123%@%_@!#^#@;use_password=mySecret;get_token=shouldkeep2;usetoken=shouldkeep3;passwrd=mySecret;")]
|
[TestCase(@"[Info] MigrationController: *** Migrating Database=readarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;token=mySecret;Enlist=False&username=mySecret;mypassword=mySecret;mypass=shouldkeep1;test_token=mySecret;password=123%@%_@!#^#@;use_password=mySecret;get_token=shouldkeep2;usetoken=shouldkeep3;passwrd=mySecret;")]
|
||||||
|
|
||||||
// Announce URLs (passkeys) Magnet & Tracker
|
// Announce URLs (passkeys) Magnet & Tracker
|
||||||
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2f9pr04sg601233210imaveql2tyu8xyui%2fannounce""}")]
|
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2f9pr04sg601233210IMAveQL2tyu8xyui%2fannounce""}")]
|
||||||
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2ftracker.php%2f9pr04sg601233210imaveql2tyu8xyui%2fannounce""}")]
|
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2ftracker.php%2f9pr04sg601233210IMAveQL2tyu8xyui%2fannounce""}")]
|
||||||
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2fannounce%2f9pr04sg601233210imaveql2tyu8xyui""}")]
|
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2fannounce%2f9pr04sg601233210IMAveQL2tyu8xyui""}")]
|
||||||
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2fannounce.php%3fpasskey%3d9pr04sg601233210imaveql2tyu8xyui""}")]
|
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2fannounce.php%3fpasskey%3d9pr04sg601233210IMAveQL2tyu8xyui""}")]
|
||||||
[TestCase(@"tracker"":""https://xxx.yyy/9pr04sg601233210imaveql2tyu8xyui/announce""}")]
|
[TestCase(@"tracker"":""https://xxx.yyy/9pr04sg601233210IMAveQL2tyu8xyui/announce""}")]
|
||||||
[TestCase(@"tracker"":""https://xxx.yyy/tracker.php/9pr04sg601233210imaveql2tyu8xyui/announce""}")]
|
[TestCase(@"tracker"":""https://xxx.yyy/tracker.php/9pr04sg601233210IMAveQL2tyu8xyui/announce""}")]
|
||||||
[TestCase(@"tracker"":""https://xxx.yyy/announce/9pr04sg601233210imaveql2tyu8xyui""}")]
|
[TestCase(@"tracker"":""https://xxx.yyy/announce/9pr04sg601233210IMAveQL2tyu8xyui""}")]
|
||||||
[TestCase(@"tracker"":""https://xxx.yyy/announce.php?passkey=9pr04sg601233210imaveql2tyu8xyui""}")]
|
[TestCase(@"tracker"":""https://xxx.yyy/announce.php?passkey=9pr04sg601233210IMAveQL2tyu8xyui""}")]
|
||||||
[TestCase(@"tracker"":""http://xxx.yyy/announce.php?passkey=9pr04sg601233210imaveql2tyu8xyui"",""info"":""http://xxx.yyy/info?a=b""")]
|
[TestCase(@"tracker"":""http://xxx.yyy/announce.php?passkey=9pr04sg601233210IMAveQL2tyu8xyui"",""info"":""http://xxx.yyy/info?a=b""")]
|
||||||
|
|
||||||
// Notifiarr
|
// Notifiarr
|
||||||
[TestCase(@"https://xxx.yyy/api/v1/notification/readarr/9pr04sg6-0123-3210-imav-eql2tyu8xyui")]
|
[TestCase(@"https://xxx.yyy/api/v1/notification/readarr/9pr04sg6-0123-3210-imav-eql2tyu8xyui")]
|
||||||
|
|||||||
@@ -103,31 +103,38 @@ namespace NzbDrone.Common.Http.Dispatchers
|
|||||||
|
|
||||||
var httpClient = GetClient(request.Url);
|
var httpClient = GetClient(request.Url);
|
||||||
|
|
||||||
using var responseMessage = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cts.Token);
|
try
|
||||||
{
|
{
|
||||||
byte[] data = null;
|
using var responseMessage = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cts.Token);
|
||||||
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
if (request.ResponseStream != null && responseMessage.StatusCode == HttpStatusCode.OK)
|
byte[] data = null;
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
await responseMessage.Content.CopyToAsync(request.ResponseStream, null, cts.Token);
|
if (request.ResponseStream != null && responseMessage.StatusCode == HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
await responseMessage.Content.CopyToAsync(request.ResponseStream, null, cts.Token);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
data = await responseMessage.Content.ReadAsByteArrayAsync(cts.Token);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
data = await responseMessage.Content.ReadAsByteArrayAsync(cts.Token);
|
throw new WebException("Failed to read complete http response", ex, WebExceptionStatus.ReceiveFailure, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var headers = responseMessage.Headers.ToNameValueCollection();
|
||||||
|
|
||||||
|
headers.Add(responseMessage.Content.Headers.ToNameValueCollection());
|
||||||
|
|
||||||
|
return new HttpResponse(request, new HttpHeader(headers), data, responseMessage.StatusCode, responseMessage.Version);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
}
|
||||||
{
|
catch (OperationCanceledException ex) when (cts.IsCancellationRequested)
|
||||||
throw new WebException("Failed to read complete http response", ex, WebExceptionStatus.ReceiveFailure, null);
|
{
|
||||||
}
|
throw new WebException("Http request timed out", ex.InnerException, WebExceptionStatus.Timeout, null);
|
||||||
|
|
||||||
var headers = responseMessage.Headers.ToNameValueCollection();
|
|
||||||
|
|
||||||
headers.Add(responseMessage.Content.Headers.ToNameValueCollection());
|
|
||||||
|
|
||||||
return new HttpResponse(request, new HttpHeader(headers), data, responseMessage.StatusCode, responseMessage.Version);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ namespace NzbDrone.Common.Instrumentation
|
|||||||
new (@"\b(\w*)?(_?(?<!use|get_)token|username|passwo?rd)=(?<secret>[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
new (@"\b(\w*)?(_?(?<!use|get_)token|username|passwo?rd)=(?<secret>[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||||
|
|
||||||
// Trackers Announce Keys; Designed for Qbit Json; should work for all in theory
|
// Trackers Announce Keys; Designed for Qbit Json; should work for all in theory
|
||||||
new (@"announce(\.php)?(/|%2f|%3fpasskey%3d)(?<secret>[a-z0-9]{16,})|(?<secret>[a-z0-9]{16,})(/|%2f)announce"),
|
new (@"announce(\.php)?(/|%2f|%3fpasskey%3d)(?<secret>[a-z0-9]{16,})|(?<secret>[a-z0-9]{16,})(/|%2f)announce", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||||
|
|
||||||
// Path
|
// Path
|
||||||
new (@"C:\\Users\\(?<secret>[^\""]+?)(\\|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
new (@"C:\\Users\\(?<secret>[^\""]+?)(\\|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using FluentAssertions;
|
|||||||
using Moq;
|
using Moq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NzbDrone.Core.Books;
|
using NzbDrone.Core.Books;
|
||||||
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.CustomFormats;
|
using NzbDrone.Core.CustomFormats;
|
||||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||||
using NzbDrone.Core.Download.TrackedDownloads;
|
using NzbDrone.Core.Download.TrackedDownloads;
|
||||||
@@ -369,5 +370,31 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||||||
|
|
||||||
Subject.IsSatisfiedBy(_remoteBook, null).Accepted.Should().BeTrue();
|
Subject.IsSatisfiedBy(_remoteBook, null).Accepted.Should().BeTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_false_if_same_quality_non_proper_in_queue_and_download_propers_is_do_not_upgrade()
|
||||||
|
{
|
||||||
|
_remoteBook.ParsedBookInfo.Quality = new QualityModel(Quality.FLAC, new Revision(2));
|
||||||
|
_author.QualityProfile.Value.Cutoff = _remoteBook.ParsedBookInfo.Quality.Quality.Id;
|
||||||
|
|
||||||
|
Mocker.GetMock<IConfigService>()
|
||||||
|
.Setup(s => s.DownloadPropersAndRepacks)
|
||||||
|
.Returns(ProperDownloadTypes.DoNotUpgrade);
|
||||||
|
|
||||||
|
var remoteBook = Builder<RemoteBook>.CreateNew()
|
||||||
|
.With(r => r.Author = _author)
|
||||||
|
.With(r => r.Books = new List<Book> { _book })
|
||||||
|
.With(r => r.ParsedBookInfo = new ParsedBookInfo
|
||||||
|
{
|
||||||
|
Quality = new QualityModel(Quality.FLAC)
|
||||||
|
})
|
||||||
|
.With(r => r.Release = _releaseInfo)
|
||||||
|
.With(r => r.CustomFormats = new List<CustomFormat>())
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
GivenQueue(new List<RemoteBook> { remoteBook });
|
||||||
|
|
||||||
|
Subject.IsSatisfiedBy(_remoteBook, null).Accepted.Should().BeFalse();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-11
@@ -68,7 +68,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
|
|||||||
decisions.Add(new DownloadDecision(remoteBook));
|
decisions.Add(new DownloadDecision(remoteBook));
|
||||||
|
|
||||||
await Subject.ProcessDecisions(decisions);
|
await Subject.ProcessDecisions(decisions);
|
||||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteBook>()), Times.Once());
|
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteBook>(), null), Times.Once());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -82,7 +82,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
|
|||||||
decisions.Add(new DownloadDecision(remoteBook));
|
decisions.Add(new DownloadDecision(remoteBook));
|
||||||
|
|
||||||
await Subject.ProcessDecisions(decisions);
|
await Subject.ProcessDecisions(decisions);
|
||||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteBook>()), Times.Once());
|
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteBook>(), null), Times.Once());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -101,7 +101,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
|
|||||||
decisions.Add(new DownloadDecision(remoteBook2));
|
decisions.Add(new DownloadDecision(remoteBook2));
|
||||||
|
|
||||||
await Subject.ProcessDecisions(decisions);
|
await Subject.ProcessDecisions(decisions);
|
||||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteBook>()), Times.Once());
|
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteBook>(), null), Times.Once());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -172,7 +172,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
|
|||||||
var decisions = new List<DownloadDecision>();
|
var decisions = new List<DownloadDecision>();
|
||||||
decisions.Add(new DownloadDecision(remoteBook));
|
decisions.Add(new DownloadDecision(remoteBook));
|
||||||
|
|
||||||
Mocker.GetMock<IDownloadService>().Setup(s => s.DownloadReport(It.IsAny<RemoteBook>())).Throws(new Exception());
|
Mocker.GetMock<IDownloadService>().Setup(s => s.DownloadReport(It.IsAny<RemoteBook>(), null)).Throws(new Exception());
|
||||||
|
|
||||||
var result = await Subject.ProcessDecisions(decisions);
|
var result = await Subject.ProcessDecisions(decisions);
|
||||||
|
|
||||||
@@ -201,7 +201,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
|
|||||||
decisions.Add(new DownloadDecision(remoteBook, new Rejection("Failure!", RejectionType.Temporary)));
|
decisions.Add(new DownloadDecision(remoteBook, new Rejection("Failure!", RejectionType.Temporary)));
|
||||||
|
|
||||||
await Subject.ProcessDecisions(decisions);
|
await Subject.ProcessDecisions(decisions);
|
||||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteBook>()), Times.Never());
|
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteBook>(), null), Times.Never());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -242,11 +242,11 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
|
|||||||
decisions.Add(new DownloadDecision(remoteBook));
|
decisions.Add(new DownloadDecision(remoteBook));
|
||||||
decisions.Add(new DownloadDecision(remoteBook));
|
decisions.Add(new DownloadDecision(remoteBook));
|
||||||
|
|
||||||
Mocker.GetMock<IDownloadService>().Setup(s => s.DownloadReport(It.IsAny<RemoteBook>()))
|
Mocker.GetMock<IDownloadService>().Setup(s => s.DownloadReport(It.IsAny<RemoteBook>(), null))
|
||||||
.Throws(new DownloadClientUnavailableException("Download client failed"));
|
.Throws(new DownloadClientUnavailableException("Download client failed"));
|
||||||
|
|
||||||
await Subject.ProcessDecisions(decisions);
|
await Subject.ProcessDecisions(decisions);
|
||||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteBook>()), Times.Once());
|
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteBook>(), null), Times.Once());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -260,12 +260,12 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
|
|||||||
decisions.Add(new DownloadDecision(remoteBook));
|
decisions.Add(new DownloadDecision(remoteBook));
|
||||||
decisions.Add(new DownloadDecision(remoteBook2));
|
decisions.Add(new DownloadDecision(remoteBook2));
|
||||||
|
|
||||||
Mocker.GetMock<IDownloadService>().Setup(s => s.DownloadReport(It.Is<RemoteBook>(r => r.Release.DownloadProtocol == DownloadProtocol.Usenet)))
|
Mocker.GetMock<IDownloadService>().Setup(s => s.DownloadReport(It.Is<RemoteBook>(r => r.Release.DownloadProtocol == DownloadProtocol.Usenet), null))
|
||||||
.Throws(new DownloadClientUnavailableException("Download client failed"));
|
.Throws(new DownloadClientUnavailableException("Download client failed"));
|
||||||
|
|
||||||
await Subject.ProcessDecisions(decisions);
|
await Subject.ProcessDecisions(decisions);
|
||||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.Is<RemoteBook>(r => r.Release.DownloadProtocol == DownloadProtocol.Usenet)), Times.Once());
|
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.Is<RemoteBook>(r => r.Release.DownloadProtocol == DownloadProtocol.Usenet), null), Times.Once());
|
||||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.Is<RemoteBook>(r => r.Release.DownloadProtocol == DownloadProtocol.Torrent)), Times.Once());
|
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.Is<RemoteBook>(r => r.Release.DownloadProtocol == DownloadProtocol.Torrent), null), Times.Once());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -278,7 +278,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
|
|||||||
decisions.Add(new DownloadDecision(remoteBook));
|
decisions.Add(new DownloadDecision(remoteBook));
|
||||||
|
|
||||||
Mocker.GetMock<IDownloadService>()
|
Mocker.GetMock<IDownloadService>()
|
||||||
.Setup(s => s.DownloadReport(It.IsAny<RemoteBook>()))
|
.Setup(s => s.DownloadReport(It.IsAny<RemoteBook>(), null))
|
||||||
.Throws(new ReleaseUnavailableException(remoteBook.Release, "That 404 Error is not just a Quirk"));
|
.Throws(new ReleaseUnavailableException(remoteBook.Release, "That 404 Error is not just a Quirk"));
|
||||||
|
|
||||||
var result = await Subject.ProcessDecisions(decisions);
|
var result = await Subject.ProcessDecisions(decisions);
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ namespace NzbDrone.Core.Test.Download
|
|||||||
var mock = WithUsenetClient();
|
var mock = WithUsenetClient();
|
||||||
mock.Setup(s => s.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()));
|
mock.Setup(s => s.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()));
|
||||||
|
|
||||||
await Subject.DownloadReport(_parseResult);
|
await Subject.DownloadReport(_parseResult, null);
|
||||||
|
|
||||||
VerifyEventPublished<BookGrabbedEvent>();
|
VerifyEventPublished<BookGrabbedEvent>();
|
||||||
}
|
}
|
||||||
@@ -94,7 +94,7 @@ namespace NzbDrone.Core.Test.Download
|
|||||||
var mock = WithUsenetClient();
|
var mock = WithUsenetClient();
|
||||||
mock.Setup(s => s.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()));
|
mock.Setup(s => s.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()));
|
||||||
|
|
||||||
await Subject.DownloadReport(_parseResult);
|
await Subject.DownloadReport(_parseResult, null);
|
||||||
|
|
||||||
mock.Verify(s => s.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()), Times.Once());
|
mock.Verify(s => s.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()), Times.Once());
|
||||||
}
|
}
|
||||||
@@ -106,7 +106,7 @@ namespace NzbDrone.Core.Test.Download
|
|||||||
mock.Setup(s => s.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()))
|
mock.Setup(s => s.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()))
|
||||||
.Throws(new WebException());
|
.Throws(new WebException());
|
||||||
|
|
||||||
Assert.ThrowsAsync<WebException>(async () => await Subject.DownloadReport(_parseResult));
|
Assert.ThrowsAsync<WebException>(async () => await Subject.DownloadReport(_parseResult, null));
|
||||||
|
|
||||||
VerifyEventNotPublished<BookGrabbedEvent>();
|
VerifyEventNotPublished<BookGrabbedEvent>();
|
||||||
}
|
}
|
||||||
@@ -121,7 +121,7 @@ namespace NzbDrone.Core.Test.Download
|
|||||||
throw new ReleaseDownloadException(v.Release, "Error", new WebException());
|
throw new ReleaseDownloadException(v.Release, "Error", new WebException());
|
||||||
});
|
});
|
||||||
|
|
||||||
Assert.ThrowsAsync<ReleaseDownloadException>(async () => await Subject.DownloadReport(_parseResult));
|
Assert.ThrowsAsync<ReleaseDownloadException>(async () => await Subject.DownloadReport(_parseResult, null));
|
||||||
|
|
||||||
Mocker.GetMock<IIndexerStatusService>()
|
Mocker.GetMock<IIndexerStatusService>()
|
||||||
.Verify(v => v.RecordFailure(It.IsAny<int>(), It.IsAny<TimeSpan>()), Times.Once());
|
.Verify(v => v.RecordFailure(It.IsAny<int>(), It.IsAny<TimeSpan>()), Times.Once());
|
||||||
@@ -141,7 +141,7 @@ namespace NzbDrone.Core.Test.Download
|
|||||||
throw new ReleaseDownloadException(v.Release, "Error", new TooManyRequestsException(request, response));
|
throw new ReleaseDownloadException(v.Release, "Error", new TooManyRequestsException(request, response));
|
||||||
});
|
});
|
||||||
|
|
||||||
Assert.ThrowsAsync<ReleaseDownloadException>(async () => await Subject.DownloadReport(_parseResult));
|
Assert.ThrowsAsync<ReleaseDownloadException>(async () => await Subject.DownloadReport(_parseResult, null));
|
||||||
|
|
||||||
Mocker.GetMock<IIndexerStatusService>()
|
Mocker.GetMock<IIndexerStatusService>()
|
||||||
.Verify(v => v.RecordFailure(It.IsAny<int>(), TimeSpan.FromMinutes(5.0)), Times.Once());
|
.Verify(v => v.RecordFailure(It.IsAny<int>(), TimeSpan.FromMinutes(5.0)), Times.Once());
|
||||||
@@ -161,7 +161,7 @@ namespace NzbDrone.Core.Test.Download
|
|||||||
throw new ReleaseDownloadException(v.Release, "Error", new TooManyRequestsException(request, response));
|
throw new ReleaseDownloadException(v.Release, "Error", new TooManyRequestsException(request, response));
|
||||||
});
|
});
|
||||||
|
|
||||||
Assert.ThrowsAsync<ReleaseDownloadException>(async () => await Subject.DownloadReport(_parseResult));
|
Assert.ThrowsAsync<ReleaseDownloadException>(async () => await Subject.DownloadReport(_parseResult, null));
|
||||||
|
|
||||||
Mocker.GetMock<IIndexerStatusService>()
|
Mocker.GetMock<IIndexerStatusService>()
|
||||||
.Verify(v => v.RecordFailure(It.IsAny<int>(),
|
.Verify(v => v.RecordFailure(It.IsAny<int>(),
|
||||||
@@ -175,7 +175,7 @@ namespace NzbDrone.Core.Test.Download
|
|||||||
mock.Setup(s => s.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()))
|
mock.Setup(s => s.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()))
|
||||||
.Throws(new DownloadClientException("Some Error"));
|
.Throws(new DownloadClientException("Some Error"));
|
||||||
|
|
||||||
Assert.ThrowsAsync<DownloadClientException>(async () => await Subject.DownloadReport(_parseResult));
|
Assert.ThrowsAsync<DownloadClientException>(async () => await Subject.DownloadReport(_parseResult, null));
|
||||||
|
|
||||||
Mocker.GetMock<IIndexerStatusService>()
|
Mocker.GetMock<IIndexerStatusService>()
|
||||||
.Verify(v => v.RecordFailure(It.IsAny<int>(), It.IsAny<TimeSpan>()), Times.Never());
|
.Verify(v => v.RecordFailure(It.IsAny<int>(), It.IsAny<TimeSpan>()), Times.Never());
|
||||||
@@ -191,7 +191,7 @@ namespace NzbDrone.Core.Test.Download
|
|||||||
throw new ReleaseUnavailableException(v.Release, "Error", new WebException());
|
throw new ReleaseUnavailableException(v.Release, "Error", new WebException());
|
||||||
});
|
});
|
||||||
|
|
||||||
Assert.ThrowsAsync<ReleaseUnavailableException>(async () => await Subject.DownloadReport(_parseResult));
|
Assert.ThrowsAsync<ReleaseUnavailableException>(async () => await Subject.DownloadReport(_parseResult, null));
|
||||||
|
|
||||||
Mocker.GetMock<IIndexerStatusService>()
|
Mocker.GetMock<IIndexerStatusService>()
|
||||||
.Verify(v => v.RecordFailure(It.IsAny<int>(), It.IsAny<TimeSpan>()), Times.Never());
|
.Verify(v => v.RecordFailure(It.IsAny<int>(), It.IsAny<TimeSpan>()), Times.Never());
|
||||||
@@ -200,7 +200,7 @@ namespace NzbDrone.Core.Test.Download
|
|||||||
[Test]
|
[Test]
|
||||||
public void should_not_attempt_download_if_client_isnt_configured()
|
public void should_not_attempt_download_if_client_isnt_configured()
|
||||||
{
|
{
|
||||||
Assert.ThrowsAsync<DownloadClientUnavailableException>(async () => await Subject.DownloadReport(_parseResult));
|
Assert.ThrowsAsync<DownloadClientUnavailableException>(async () => await Subject.DownloadReport(_parseResult, null));
|
||||||
|
|
||||||
Mocker.GetMock<IDownloadClient>().Verify(c => c.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()), Times.Never());
|
Mocker.GetMock<IDownloadClient>().Verify(c => c.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()), Times.Never());
|
||||||
VerifyEventNotPublished<BookGrabbedEvent>();
|
VerifyEventNotPublished<BookGrabbedEvent>();
|
||||||
@@ -222,7 +222,7 @@ namespace NzbDrone.Core.Test.Download
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await Subject.DownloadReport(_parseResult);
|
await Subject.DownloadReport(_parseResult, null);
|
||||||
|
|
||||||
Mocker.GetMock<IDownloadClientStatusService>().Verify(c => c.GetBlockedProviders(), Times.Never());
|
Mocker.GetMock<IDownloadClientStatusService>().Verify(c => c.GetBlockedProviders(), Times.Never());
|
||||||
mockUsenet.Verify(c => c.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()), Times.Once());
|
mockUsenet.Verify(c => c.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()), Times.Once());
|
||||||
@@ -235,7 +235,7 @@ namespace NzbDrone.Core.Test.Download
|
|||||||
var mockTorrent = WithTorrentClient();
|
var mockTorrent = WithTorrentClient();
|
||||||
var mockUsenet = WithUsenetClient();
|
var mockUsenet = WithUsenetClient();
|
||||||
|
|
||||||
await Subject.DownloadReport(_parseResult);
|
await Subject.DownloadReport(_parseResult, null);
|
||||||
|
|
||||||
mockTorrent.Verify(c => c.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()), Times.Never());
|
mockTorrent.Verify(c => c.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()), Times.Never());
|
||||||
mockUsenet.Verify(c => c.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()), Times.Once());
|
mockUsenet.Verify(c => c.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()), Times.Once());
|
||||||
@@ -249,7 +249,7 @@ namespace NzbDrone.Core.Test.Download
|
|||||||
|
|
||||||
_parseResult.Release.DownloadProtocol = DownloadProtocol.Torrent;
|
_parseResult.Release.DownloadProtocol = DownloadProtocol.Torrent;
|
||||||
|
|
||||||
await Subject.DownloadReport(_parseResult);
|
await Subject.DownloadReport(_parseResult, null);
|
||||||
|
|
||||||
mockTorrent.Verify(c => c.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()), Times.Once());
|
mockTorrent.Verify(c => c.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()), Times.Once());
|
||||||
mockUsenet.Verify(c => c.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()), Times.Never());
|
mockUsenet.Verify(c => c.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()), Times.Never());
|
||||||
|
|||||||
+1
-1
@@ -37,7 +37,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
|
|||||||
|
|
||||||
results.Should().NotBeEmpty();
|
results.Should().NotBeEmpty();
|
||||||
Mocker.GetMock<IMakeDownloadDecision>()
|
Mocker.GetMock<IMakeDownloadDecision>()
|
||||||
.Verify(v => v.GetRssDecision(It.Is<List<ReleaseInfo>>(d => d.Count == 0)), Times.Never());
|
.Verify(v => v.GetRssDecision(It.Is<List<ReleaseInfo>>(d => d.Count == 0), It.IsAny<bool>()), Times.Never());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ namespace NzbDrone.Core.Test.ImportListTests
|
|||||||
|
|
||||||
_importListReports = new List<ImportListItemInfo> { importListItem1 };
|
_importListReports = new List<ImportListItemInfo> { importListItem1 };
|
||||||
|
|
||||||
|
var mockImportList = new Mock<IImportList>();
|
||||||
|
|
||||||
Mocker.GetMock<IFetchAndParseImportList>()
|
Mocker.GetMock<IFetchAndParseImportList>()
|
||||||
.Setup(v => v.Fetch())
|
.Setup(v => v.Fetch())
|
||||||
.Returns(_importListReports);
|
.Returns(_importListReports);
|
||||||
@@ -53,6 +55,10 @@ namespace NzbDrone.Core.Test.ImportListTests
|
|||||||
.Setup(v => v.Get(It.IsAny<int>()))
|
.Setup(v => v.Get(It.IsAny<int>()))
|
||||||
.Returns(new ImportListDefinition { ShouldMonitor = ImportListMonitorType.SpecificBook });
|
.Returns(new ImportListDefinition { ShouldMonitor = ImportListMonitorType.SpecificBook });
|
||||||
|
|
||||||
|
Mocker.GetMock<IImportListFactory>()
|
||||||
|
.Setup(v => v.AutomaticAddEnabled(It.IsAny<bool>()))
|
||||||
|
.Returns(new List<IImportList> { mockImportList.Object });
|
||||||
|
|
||||||
Mocker.GetMock<IFetchAndParseImportList>()
|
Mocker.GetMock<IFetchAndParseImportList>()
|
||||||
.Setup(v => v.Fetch())
|
.Setup(v => v.Fetch())
|
||||||
.Returns(_importListReports);
|
.Returns(_importListReports);
|
||||||
@@ -322,5 +328,31 @@ namespace NzbDrone.Core.Test.ImportListTests
|
|||||||
t.First().AddOptions.BooksToMonitor.Count == expectedBooksMonitored &&
|
t.First().AddOptions.BooksToMonitor.Count == expectedBooksMonitored &&
|
||||||
t.First().Monitored == expectedAuthorMonitored), false));
|
t.First().Monitored == expectedAuthorMonitored), false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_not_fetch_if_no_lists_are_enabled()
|
||||||
|
{
|
||||||
|
Mocker.GetMock<IImportListFactory>()
|
||||||
|
.Setup(v => v.AutomaticAddEnabled(It.IsAny<bool>()))
|
||||||
|
.Returns(new List<IImportList>());
|
||||||
|
|
||||||
|
Subject.Execute(new ImportListSyncCommand());
|
||||||
|
|
||||||
|
Mocker.GetMock<IFetchAndParseImportList>()
|
||||||
|
.Verify(v => v.Fetch(), Times.Never);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_not_process_if_no_items_are_returned()
|
||||||
|
{
|
||||||
|
Mocker.GetMock<IFetchAndParseImportList>()
|
||||||
|
.Setup(v => v.Fetch())
|
||||||
|
.Returns(new List<ImportListItemInfo>());
|
||||||
|
|
||||||
|
Subject.Execute(new ImportListSyncCommand());
|
||||||
|
|
||||||
|
Mocker.GetMock<IImportListExclusionService>()
|
||||||
|
.Verify(v => v.All(), Times.Never);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
|
|||||||
|
|
||||||
Mocker.GetMock<IHttpClient>()
|
Mocker.GetMock<IHttpClient>()
|
||||||
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
|
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
|
||||||
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), recentFeed)));
|
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, recentFeed)));
|
||||||
|
|
||||||
var releases = await Subject.FetchRecent();
|
var releases = await Subject.FetchRecent();
|
||||||
|
|
||||||
|
|||||||
@@ -68,5 +68,16 @@ namespace NzbDrone.Core.Test.IndexerTests
|
|||||||
|
|
||||||
VerifyNoUpdate();
|
VerifyNoUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_not_record_failure_for_unknown_provider()
|
||||||
|
{
|
||||||
|
Subject.RecordFailure(0);
|
||||||
|
|
||||||
|
Mocker.GetMock<IIndexerStatusRepository>()
|
||||||
|
.Verify(v => v.FindByProviderId(1), Times.Never);
|
||||||
|
|
||||||
|
VerifyNoUpdate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
using System;
|
||||||
|
using FluentAssertions;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
|
using NzbDrone.Core.Configuration;
|
||||||
|
using NzbDrone.Core.Languages;
|
||||||
|
using NzbDrone.Core.Localization;
|
||||||
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
using NzbDrone.Test.Common;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Test.Localization
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class LocalizationServiceFixture : CoreTest<LocalizationService>
|
||||||
|
{
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
Mocker.GetMock<IConfigService>().Setup(m => m.UILanguage).Returns((int)Language.English);
|
||||||
|
|
||||||
|
Mocker.GetMock<IAppFolderInfo>().Setup(m => m.StartUpFolder).Returns(TestContext.CurrentContext.TestDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_get_string_in_dictionary_if_lang_exists_and_string_exists()
|
||||||
|
{
|
||||||
|
var localizedString = Subject.GetLocalizedString("UiLanguage");
|
||||||
|
|
||||||
|
localizedString.Should().Be("UI Language");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_get_string_in_french()
|
||||||
|
{
|
||||||
|
Mocker.GetMock<IConfigService>().Setup(m => m.UILanguage).Returns((int)Language.French);
|
||||||
|
|
||||||
|
var localizedString = Subject.GetLocalizedString("UiLanguage");
|
||||||
|
|
||||||
|
localizedString.Should().Be("Langue de l'IU");
|
||||||
|
|
||||||
|
ExceptionVerification.ExpectedErrors(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_get_string_in_default_dictionary_if_unknown_language_and_string_exists()
|
||||||
|
{
|
||||||
|
Mocker.GetMock<IConfigService>().Setup(m => m.UILanguage).Returns(0);
|
||||||
|
var localizedString = Subject.GetLocalizedString("UiLanguage");
|
||||||
|
|
||||||
|
localizedString.Should().Be("UI Language");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_argument_if_string_doesnt_exists()
|
||||||
|
{
|
||||||
|
var localizedString = Subject.GetLocalizedString("badString");
|
||||||
|
|
||||||
|
localizedString.Should().Be("badString");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_argument_if_string_doesnt_exists_default_lang()
|
||||||
|
{
|
||||||
|
var localizedString = Subject.GetLocalizedString("badString");
|
||||||
|
|
||||||
|
localizedString.Should().Be("badString");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_throw_if_empty_string_passed()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => Subject.GetLocalizedString(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_throw_if_null_string_passed()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => Subject.GetLocalizedString(null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,12 +5,15 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using FizzWare.NBuilder;
|
using FizzWare.NBuilder;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
|
using Moq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Core.Books;
|
using NzbDrone.Core.Books;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.MediaFiles;
|
using NzbDrone.Core.MediaFiles;
|
||||||
|
using NzbDrone.Core.MediaFiles.Events;
|
||||||
|
using NzbDrone.Core.Messaging.Events;
|
||||||
using NzbDrone.Core.Test.Framework;
|
using NzbDrone.Core.Test.Framework;
|
||||||
using NzbDrone.Test.Common;
|
using NzbDrone.Test.Common;
|
||||||
using NzbDrone.Test.Common.AutoMoq;
|
using NzbDrone.Test.Common.AutoMoq;
|
||||||
@@ -166,7 +169,7 @@ namespace NzbDrone.Core.Test.MediaFiles.AudioTagServiceFixture
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
[TestCaseSource(typeof(TestCaseFactory), "TestCases")]
|
[TestCaseSource(typeof(TestCaseFactory), nameof(TestCaseFactory.TestCases))]
|
||||||
public void should_read_duration(string filename, string[] ignored)
|
public void should_read_duration(string filename, string[] ignored)
|
||||||
{
|
{
|
||||||
var path = Path.Combine(_testdir, filename);
|
var path = Path.Combine(_testdir, filename);
|
||||||
@@ -177,7 +180,7 @@ namespace NzbDrone.Core.Test.MediaFiles.AudioTagServiceFixture
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
[TestCaseSource(typeof(TestCaseFactory), "TestCases")]
|
[TestCaseSource(typeof(TestCaseFactory), nameof(TestCaseFactory.TestCases))]
|
||||||
public void should_read_write_tags(string filename, string[] skipProperties)
|
public void should_read_write_tags(string filename, string[] skipProperties)
|
||||||
{
|
{
|
||||||
GivenFileCopy(filename);
|
GivenFileCopy(filename);
|
||||||
@@ -198,7 +201,7 @@ namespace NzbDrone.Core.Test.MediaFiles.AudioTagServiceFixture
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
[TestCaseSource(typeof(TestCaseFactory), "TestCases")]
|
[TestCaseSource(typeof(TestCaseFactory), nameof(TestCaseFactory.TestCases))]
|
||||||
public void should_read_audiotag_from_file_with_no_tags(string filename, string[] skipProperties)
|
public void should_read_audiotag_from_file_with_no_tags(string filename, string[] skipProperties)
|
||||||
{
|
{
|
||||||
GivenFileCopy(filename);
|
GivenFileCopy(filename);
|
||||||
@@ -220,7 +223,7 @@ namespace NzbDrone.Core.Test.MediaFiles.AudioTagServiceFixture
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
[TestCaseSource(typeof(TestCaseFactory), "TestCases")]
|
[TestCaseSource(typeof(TestCaseFactory), nameof(TestCaseFactory.TestCases))]
|
||||||
public void should_read_parsedtrackinfo_from_file_with_no_tags(string filename, string[] skipProperties)
|
public void should_read_parsedtrackinfo_from_file_with_no_tags(string filename, string[] skipProperties)
|
||||||
{
|
{
|
||||||
GivenFileCopy(filename);
|
GivenFileCopy(filename);
|
||||||
@@ -235,7 +238,7 @@ namespace NzbDrone.Core.Test.MediaFiles.AudioTagServiceFixture
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
[TestCaseSource(typeof(TestCaseFactory), "TestCases")]
|
[TestCaseSource(typeof(TestCaseFactory), nameof(TestCaseFactory.TestCases))]
|
||||||
public void should_set_quality_and_mediainfo_for_corrupt_file(string filename, string[] skipProperties)
|
public void should_set_quality_and_mediainfo_for_corrupt_file(string filename, string[] skipProperties)
|
||||||
{
|
{
|
||||||
// use missing to simulate corrupt
|
// use missing to simulate corrupt
|
||||||
@@ -250,7 +253,7 @@ namespace NzbDrone.Core.Test.MediaFiles.AudioTagServiceFixture
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
[TestCaseSource(typeof(TestCaseFactory), "TestCases")]
|
[TestCaseSource(typeof(TestCaseFactory), nameof(TestCaseFactory.TestCases))]
|
||||||
public void should_read_file_with_only_title_tag(string filename, string[] ignored)
|
public void should_read_file_with_only_title_tag(string filename, string[] ignored)
|
||||||
{
|
{
|
||||||
GivenFileCopy(filename);
|
GivenFileCopy(filename);
|
||||||
@@ -270,7 +273,7 @@ namespace NzbDrone.Core.Test.MediaFiles.AudioTagServiceFixture
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
[TestCaseSource(typeof(TestCaseFactory), "TestCases")]
|
[TestCaseSource(typeof(TestCaseFactory), nameof(TestCaseFactory.TestCases))]
|
||||||
public void should_remove_date_from_tags_when_not_in_metadata(string filename, string[] ignored)
|
public void should_remove_date_from_tags_when_not_in_metadata(string filename, string[] ignored)
|
||||||
{
|
{
|
||||||
GivenFileCopy(filename);
|
GivenFileCopy(filename);
|
||||||
@@ -365,6 +368,29 @@ namespace NzbDrone.Core.Test.MediaFiles.AudioTagServiceFixture
|
|||||||
var fileInfo = _diskProvider.GetFileInfo(file.Path);
|
var fileInfo = _diskProvider.GetFileInfo(file.Path);
|
||||||
file.Modified.Should().Be(fileInfo.LastWriteTimeUtc);
|
file.Modified.Should().Be(fileInfo.LastWriteTimeUtc);
|
||||||
file.Size.Should().Be(fileInfo.Length);
|
file.Size.Should().Be(fileInfo.Length);
|
||||||
|
|
||||||
|
Mocker.GetMock<IEventAggregator>()
|
||||||
|
.Verify(v => v.PublishEvent(It.IsAny<BookFileRetaggedEvent>()), Times.Once());
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase("nin.mp3")]
|
||||||
|
public void write_tags_should_not_update_tags_if_already_updated(string filename)
|
||||||
|
{
|
||||||
|
Mocker.GetMock<IConfigService>()
|
||||||
|
.Setup(x => x.ScrubAudioTags)
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
GivenFileCopy(filename);
|
||||||
|
|
||||||
|
var file = GivenPopulatedTrackfile(0);
|
||||||
|
|
||||||
|
file.Path = _copiedFile;
|
||||||
|
Subject.WriteTags(file, false, true);
|
||||||
|
Subject.WriteTags(file, false, true);
|
||||||
|
Subject.WriteTags(file, false, true);
|
||||||
|
|
||||||
|
Mocker.GetMock<IEventAggregator>()
|
||||||
|
.Verify(v => v.PublishEvent(It.IsAny<BookFileRetaggedEvent>()), Times.Once());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|||||||
@@ -13,6 +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 = "2024-01-31 00:00:00Z")]
|
||||||
public class BookInfoProxyFixture : CoreTest<BookInfoProxy>
|
public class BookInfoProxyFixture : CoreTest<BookInfoProxy>
|
||||||
{
|
{
|
||||||
private MetadataProfile _metadataProfile;
|
private MetadataProfile _metadataProfile;
|
||||||
|
|||||||
@@ -15,6 +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 = "2024-01-31 00:00:00Z")]
|
||||||
public class BookInfoProxySearchFixture : CoreTest<BookInfoProxy>
|
public class BookInfoProxySearchFixture : CoreTest<BookInfoProxy>
|
||||||
{
|
{
|
||||||
[SetUp]
|
[SetUp]
|
||||||
|
|||||||
@@ -166,6 +166,7 @@ namespace NzbDrone.Core.Test.MusicTests.BookRepositoryTests
|
|||||||
|
|
||||||
private EquivalencyAssertionOptions<Book> BookComparerOptions(EquivalencyAssertionOptions<Book> opts) => opts.ComparingByMembers<Book>()
|
private EquivalencyAssertionOptions<Book> BookComparerOptions(EquivalencyAssertionOptions<Book> opts) => opts.ComparingByMembers<Book>()
|
||||||
.Excluding(ctx => ctx.SelectedMemberInfo.MemberType.IsGenericType && ctx.SelectedMemberInfo.MemberType.GetGenericTypeDefinition() == typeof(LazyLoaded<>))
|
.Excluding(ctx => ctx.SelectedMemberInfo.MemberType.IsGenericType && ctx.SelectedMemberInfo.MemberType.GetGenericTypeDefinition() == typeof(LazyLoaded<>))
|
||||||
.Excluding(x => x.AuthorId);
|
.Excluding(x => x.AuthorId)
|
||||||
|
.Excluding(x => x.ForeignEditionId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,16 +35,13 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
[TestCase("or")]
|
[TestCase("or")]
|
||||||
[TestCase("an")]
|
[TestCase("an")]
|
||||||
[TestCase("of")]
|
[TestCase("of")]
|
||||||
public void should_remove_common_words(string word)
|
public void should_remove_common_words_from_middle_of_title(string word)
|
||||||
{
|
{
|
||||||
var dirtyFormat = new[]
|
var dirtyFormat = new[]
|
||||||
{
|
{
|
||||||
"word.{0}.word",
|
"word.{0}.word",
|
||||||
"word {0} word",
|
"word {0} word",
|
||||||
"word-{0}-word",
|
"word-{0}-word"
|
||||||
"word.word.{0}",
|
|
||||||
"word-word-{0}",
|
|
||||||
"word-word {0}",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
foreach (var s in dirtyFormat)
|
foreach (var s in dirtyFormat)
|
||||||
@@ -54,6 +51,27 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestCase("the")]
|
||||||
|
[TestCase("and")]
|
||||||
|
[TestCase("or")]
|
||||||
|
[TestCase("an")]
|
||||||
|
[TestCase("of")]
|
||||||
|
public void should_not_remove_common_words_from_end_of_title(string word)
|
||||||
|
{
|
||||||
|
var dirtyFormat = new[]
|
||||||
|
{
|
||||||
|
"word.word.{0}",
|
||||||
|
"word-word-{0}",
|
||||||
|
"word-word {0}"
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var s in dirtyFormat)
|
||||||
|
{
|
||||||
|
var dirty = string.Format(s, word);
|
||||||
|
dirty.CleanAuthorName().Should().Be("wordword" + word.ToLower());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void should_remove_a_from_middle_of_title()
|
public void should_remove_a_from_middle_of_title()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -41,6 +41,23 @@ namespace NzbDrone.Core.Annotations
|
|||||||
public string Hint { get; set; }
|
public string Hint { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
|
||||||
|
public class FieldTokenAttribute : Attribute
|
||||||
|
{
|
||||||
|
public FieldTokenAttribute(TokenField field, string label = "", string token = "", object value = null)
|
||||||
|
{
|
||||||
|
Label = label;
|
||||||
|
Field = field;
|
||||||
|
Token = token;
|
||||||
|
Value = value?.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Label { get; set; }
|
||||||
|
public TokenField Field { get; set; }
|
||||||
|
public string Token { get; set; }
|
||||||
|
public string Value { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
public class FieldSelectOption
|
public class FieldSelectOption
|
||||||
{
|
{
|
||||||
public int Value { get; set; }
|
public int Value { get; set; }
|
||||||
@@ -83,4 +100,11 @@ namespace NzbDrone.Core.Annotations
|
|||||||
ApiKey,
|
ApiKey,
|
||||||
UserName
|
UserName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum TokenField
|
||||||
|
{
|
||||||
|
Label,
|
||||||
|
HelpText,
|
||||||
|
HelpTextWarning
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ namespace NzbDrone.Core.Blocklisting
|
|||||||
public interface IBlocklistService
|
public interface IBlocklistService
|
||||||
{
|
{
|
||||||
bool Blocklisted(int authorId, ReleaseInfo release);
|
bool Blocklisted(int authorId, ReleaseInfo release);
|
||||||
|
bool BlocklistedTorrentHash(int authorId, string hash);
|
||||||
PagingSpec<Blocklist> Paged(PagingSpec<Blocklist> pagingSpec);
|
PagingSpec<Blocklist> Paged(PagingSpec<Blocklist> pagingSpec);
|
||||||
void Block(RemoteBook remoteEpisode, string message);
|
void Block(RemoteBook remoteEpisode, string message);
|
||||||
void Delete(int id);
|
void Delete(int id);
|
||||||
@@ -36,30 +37,34 @@ namespace NzbDrone.Core.Blocklisting
|
|||||||
|
|
||||||
public bool Blocklisted(int authorId, ReleaseInfo release)
|
public bool Blocklisted(int authorId, ReleaseInfo release)
|
||||||
{
|
{
|
||||||
var blocklistedByTitle = _blocklistRepository.BlocklistedByTitle(authorId, release.Title);
|
|
||||||
|
|
||||||
if (release.DownloadProtocol == DownloadProtocol.Torrent)
|
if (release.DownloadProtocol == DownloadProtocol.Torrent)
|
||||||
{
|
{
|
||||||
var torrentInfo = release as TorrentInfo;
|
if (release is not TorrentInfo torrentInfo)
|
||||||
|
|
||||||
if (torrentInfo == null)
|
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (torrentInfo.InfoHash.IsNullOrWhiteSpace())
|
if (torrentInfo.InfoHash.IsNotNullOrWhiteSpace())
|
||||||
{
|
{
|
||||||
return blocklistedByTitle.Where(b => b.Protocol == DownloadProtocol.Torrent)
|
var blocklistedByTorrentInfohash = _blocklistRepository.BlocklistedByTorrentInfoHash(authorId, torrentInfo.InfoHash);
|
||||||
.Any(b => SameTorrent(b, torrentInfo));
|
|
||||||
|
return blocklistedByTorrentInfohash.Any(b => SameTorrent(b, torrentInfo));
|
||||||
}
|
}
|
||||||
|
|
||||||
var blocklistedByTorrentInfohash = _blocklistRepository.BlocklistedByTorrentInfoHash(authorId, torrentInfo.InfoHash);
|
return _blocklistRepository.BlocklistedByTitle(authorId, release.Title)
|
||||||
|
.Where(b => b.Protocol == DownloadProtocol.Torrent)
|
||||||
return blocklistedByTorrentInfohash.Any(b => SameTorrent(b, torrentInfo));
|
.Any(b => SameTorrent(b, torrentInfo));
|
||||||
}
|
}
|
||||||
|
|
||||||
return blocklistedByTitle.Where(b => b.Protocol == DownloadProtocol.Usenet)
|
return _blocklistRepository.BlocklistedByTitle(authorId, release.Title)
|
||||||
.Any(b => SameNzb(b, release));
|
.Where(b => b.Protocol == DownloadProtocol.Usenet)
|
||||||
|
.Any(b => SameNzb(b, release));
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool BlocklistedTorrentHash(int authorId, string hash)
|
||||||
|
{
|
||||||
|
return _blocklistRepository.BlocklistedByTorrentInfoHash(authorId, hash).Any(b =>
|
||||||
|
b.TorrentInfoHash.Equals(hash, StringComparison.InvariantCultureIgnoreCase));
|
||||||
}
|
}
|
||||||
|
|
||||||
public PagingSpec<Blocklist> Paged(PagingSpec<Blocklist> pagingSpec)
|
public PagingSpec<Blocklist> Paged(PagingSpec<Blocklist> pagingSpec)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
using FluentValidation.Results;
|
using FluentValidation.Results;
|
||||||
|
using Newtonsoft.Json;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common.Cache;
|
using NzbDrone.Common.Cache;
|
||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
@@ -218,11 +219,11 @@ namespace NzbDrone.Core.Books.Calibre
|
|||||||
double? seriesIndex = null;
|
double? seriesIndex = null;
|
||||||
if (double.TryParse(serieslink?.Position, out var index))
|
if (double.TryParse(serieslink?.Position, out var index))
|
||||||
{
|
{
|
||||||
_logger.Trace($"Parsed {serieslink?.Position} as {index}");
|
_logger.Trace("Parsed '{0}' as '{1}'", serieslink.Position, index);
|
||||||
seriesIndex = index;
|
seriesIndex = index;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.Trace($"Book: {book} Series: {series?.Title}, Position: {seriesIndex}");
|
_logger.Trace("Book: {0} Series: {1}, Position: {2}", book, series?.Title, seriesIndex);
|
||||||
|
|
||||||
var cover = edition.Images.FirstOrDefault(x => x.CoverType == MediaCoverTypes.Cover);
|
var cover = edition.Images.FirstOrDefault(x => x.CoverType == MediaCoverTypes.Cover);
|
||||||
string image = null;
|
string image = null;
|
||||||
@@ -275,7 +276,9 @@ namespace NzbDrone.Core.Books.Calibre
|
|||||||
|
|
||||||
var updatedPath = GetOriginalFormat(updated.Formats);
|
var updatedPath = GetOriginalFormat(updated.Formats);
|
||||||
|
|
||||||
if (updatedPath != file.Path)
|
_logger.Trace("File path from Calibre: '{0}'", updatedPath);
|
||||||
|
|
||||||
|
if (updatedPath.IsNotNullOrWhiteSpace() && updatedPath != file.Path)
|
||||||
{
|
{
|
||||||
_rootFolderWatchingService.ReportFileSystemChangeBeginning(updatedPath);
|
_rootFolderWatchingService.ReportFileSystemChangeBeginning(updatedPath);
|
||||||
file.Path = updatedPath;
|
file.Path = updatedPath;
|
||||||
@@ -304,6 +307,7 @@ namespace NzbDrone.Core.Books.Calibre
|
|||||||
|
|
||||||
var request = builder.Build();
|
var request = builder.Build();
|
||||||
request.SetContent(payload.ToJson());
|
request.SetContent(payload.ToJson());
|
||||||
|
request.ContentSummary = payload.ToJson(Formatting.None);
|
||||||
|
|
||||||
_httpClient.Execute(request);
|
_httpClient.Execute(request);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ namespace NzbDrone.Core.Books
|
|||||||
// These are metadata entries
|
// These are metadata entries
|
||||||
public int AuthorMetadataId { get; set; }
|
public int AuthorMetadataId { get; set; }
|
||||||
public string ForeignBookId { get; set; }
|
public string ForeignBookId { get; set; }
|
||||||
|
public string ForeignEditionId { get; set; }
|
||||||
public string TitleSlug { get; set; }
|
public string TitleSlug { get; set; }
|
||||||
public string Title { get; set; }
|
public string Title { get; set; }
|
||||||
public DateTime? ReleaseDate { get; set; }
|
public DateTime? ReleaseDate { get; set; }
|
||||||
@@ -71,6 +72,7 @@ namespace NzbDrone.Core.Books
|
|||||||
public override void UseMetadataFrom(Book other)
|
public override void UseMetadataFrom(Book other)
|
||||||
{
|
{
|
||||||
ForeignBookId = other.ForeignBookId;
|
ForeignBookId = other.ForeignBookId;
|
||||||
|
ForeignEditionId = other.ForeignEditionId;
|
||||||
TitleSlug = other.TitleSlug;
|
TitleSlug = other.TitleSlug;
|
||||||
Title = other.Title;
|
Title = other.Title;
|
||||||
ReleaseDate = other.ReleaseDate;
|
ReleaseDate = other.ReleaseDate;
|
||||||
@@ -95,6 +97,7 @@ namespace NzbDrone.Core.Books
|
|||||||
public override void ApplyChanges(Book other)
|
public override void ApplyChanges(Book other)
|
||||||
{
|
{
|
||||||
ForeignBookId = other.ForeignBookId;
|
ForeignBookId = other.ForeignBookId;
|
||||||
|
ForeignEditionId = other.ForeignEditionId;
|
||||||
AddOptions = other.AddOptions;
|
AddOptions = other.AddOptions;
|
||||||
Monitored = other.Monitored;
|
Monitored = other.Monitored;
|
||||||
AnyEditionOk = other.AnyEditionOk;
|
AnyEditionOk = other.AnyEditionOk;
|
||||||
|
|||||||
@@ -322,6 +322,20 @@ namespace NzbDrone.Core.Configuration
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void MigrateConfigFile()
|
||||||
|
{
|
||||||
|
if (!File.Exists(_configFile))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If SSL is enabled and a cert hash is still in the config file or cert path is empty disable SSL
|
||||||
|
if (EnableSsl && (GetValue("SslCertHash", null).IsNotNullOrWhiteSpace() || SslCertPath.IsNullOrWhiteSpace()))
|
||||||
|
{
|
||||||
|
SetValue("EnableSsl", false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void DeleteOldValues()
|
private void DeleteOldValues()
|
||||||
{
|
{
|
||||||
var xDoc = LoadConfigFile();
|
var xDoc = LoadConfigFile();
|
||||||
@@ -404,6 +418,7 @@ namespace NzbDrone.Core.Configuration
|
|||||||
|
|
||||||
public void HandleAsync(ApplicationStartedEvent message)
|
public void HandleAsync(ApplicationStartedEvent message)
|
||||||
{
|
{
|
||||||
|
MigrateConfigFile();
|
||||||
EnsureDefaultConfigFile();
|
EnsureDefaultConfigFile();
|
||||||
DeleteOldValues();
|
DeleteOldValues();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,6 +144,13 @@ namespace NzbDrone.Core.Configuration
|
|||||||
set { SetValue("AutoRedownloadFailed", value); }
|
set { SetValue("AutoRedownloadFailed", value); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool AutoRedownloadFailedFromInteractiveSearch
|
||||||
|
{
|
||||||
|
get { return GetValueBoolean("AutoRedownloadFailedFromInteractiveSearch", true); }
|
||||||
|
|
||||||
|
set { SetValue("AutoRedownloadFailedFromInteractiveSearch", value); }
|
||||||
|
}
|
||||||
|
|
||||||
public bool CreateEmptyAuthorFolders
|
public bool CreateEmptyAuthorFolders
|
||||||
{
|
{
|
||||||
get { return GetValueBoolean("CreateEmptyAuthorFolders", false); }
|
get { return GetValueBoolean("CreateEmptyAuthorFolders", false); }
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ namespace NzbDrone.Core.Configuration
|
|||||||
//Completed/Failed Download Handling (Download client)
|
//Completed/Failed Download Handling (Download client)
|
||||||
bool EnableCompletedDownloadHandling { get; set; }
|
bool EnableCompletedDownloadHandling { get; set; }
|
||||||
bool AutoRedownloadFailed { get; set; }
|
bool AutoRedownloadFailed { get; set; }
|
||||||
|
bool AutoRedownloadFailedFromInteractiveSearch { get; set; }
|
||||||
|
|
||||||
//Media Management
|
//Media Management
|
||||||
bool AutoUnmonitorPreviouslyDownloadedBooks { get; set; }
|
bool AutoUnmonitorPreviouslyDownloadedBooks { get; set; }
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ namespace NzbDrone.Core.CustomFormats
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return matches;
|
return matches.OrderBy(x => x.Name).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<CustomFormat> ParseCustomFormat(BookFile bookFile, Author author, List<CustomFormat> allCustomFormats)
|
private static List<CustomFormat> ParseCustomFormat(BookFile bookFile, Author author, List<CustomFormat> allCustomFormats)
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ namespace NzbDrone.Core.Datastore
|
|||||||
{
|
{
|
||||||
public interface IConnectionStringFactory
|
public interface IConnectionStringFactory
|
||||||
{
|
{
|
||||||
string MainDbConnectionString { get; }
|
DatabaseConnectionInfo MainDbConnection { get; }
|
||||||
string LogDbConnectionString { get; }
|
DatabaseConnectionInfo LogDbConnection { get; }
|
||||||
string CacheDbConnectionString { get; }
|
DatabaseConnectionInfo CacheDbConnection { get; }
|
||||||
string GetDatabasePath(string connectionString);
|
string GetDatabasePath(string connectionString);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,19 +23,19 @@ namespace NzbDrone.Core.Datastore
|
|||||||
{
|
{
|
||||||
_configFileProvider = configFileProvider;
|
_configFileProvider = configFileProvider;
|
||||||
|
|
||||||
MainDbConnectionString = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresMainDb) :
|
MainDbConnection = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresMainDb) :
|
||||||
GetConnectionString(appFolderInfo.GetDatabase());
|
GetConnectionString(appFolderInfo.GetDatabase());
|
||||||
|
|
||||||
LogDbConnectionString = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresLogDb) :
|
LogDbConnection = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresLogDb) :
|
||||||
GetConnectionString(appFolderInfo.GetLogDatabase());
|
GetConnectionString(appFolderInfo.GetLogDatabase());
|
||||||
|
|
||||||
CacheDbConnectionString = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresCacheDb) :
|
CacheDbConnection = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresCacheDb) :
|
||||||
GetConnectionString(appFolderInfo.GetCacheDatabase());
|
GetConnectionString(appFolderInfo.GetCacheDatabase());
|
||||||
}
|
}
|
||||||
|
|
||||||
public string MainDbConnectionString { get; private set; }
|
public DatabaseConnectionInfo MainDbConnection { get; private set; }
|
||||||
public string LogDbConnectionString { get; private set; }
|
public DatabaseConnectionInfo LogDbConnection { get; private set; }
|
||||||
public string CacheDbConnectionString { get; private set; }
|
public DatabaseConnectionInfo CacheDbConnection { get; private set; }
|
||||||
|
|
||||||
public string GetDatabasePath(string connectionString)
|
public string GetDatabasePath(string connectionString)
|
||||||
{
|
{
|
||||||
@@ -44,37 +44,40 @@ namespace NzbDrone.Core.Datastore
|
|||||||
return connectionBuilder.DataSource;
|
return connectionBuilder.DataSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetConnectionString(string dbPath)
|
private static DatabaseConnectionInfo GetConnectionString(string dbPath)
|
||||||
{
|
{
|
||||||
var connectionBuilder = new SQLiteConnectionStringBuilder();
|
var connectionBuilder = new SQLiteConnectionStringBuilder
|
||||||
|
{
|
||||||
connectionBuilder.DataSource = dbPath;
|
DataSource = dbPath,
|
||||||
connectionBuilder.CacheSize = -10000;
|
CacheSize = -20000,
|
||||||
connectionBuilder.DateTimeKind = DateTimeKind.Utc;
|
DateTimeKind = DateTimeKind.Utc,
|
||||||
connectionBuilder.JournalMode = OsInfo.IsOsx ? SQLiteJournalModeEnum.Truncate : SQLiteJournalModeEnum.Wal;
|
JournalMode = OsInfo.IsOsx ? SQLiteJournalModeEnum.Truncate : SQLiteJournalModeEnum.Wal,
|
||||||
connectionBuilder.Pooling = true;
|
Pooling = true,
|
||||||
connectionBuilder.Version = 3;
|
Version = 3,
|
||||||
|
BusyTimeout = 100
|
||||||
|
};
|
||||||
|
|
||||||
if (OsInfo.IsOsx)
|
if (OsInfo.IsOsx)
|
||||||
{
|
{
|
||||||
connectionBuilder.Add("Full FSync", true);
|
connectionBuilder.Add("Full FSync", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
return connectionBuilder.ConnectionString;
|
return new DatabaseConnectionInfo(DatabaseType.SQLite, connectionBuilder.ConnectionString);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetPostgresConnectionString(string dbName)
|
private DatabaseConnectionInfo GetPostgresConnectionString(string dbName)
|
||||||
{
|
{
|
||||||
var connectionBuilder = new NpgsqlConnectionStringBuilder();
|
var connectionBuilder = new NpgsqlConnectionStringBuilder
|
||||||
|
{
|
||||||
|
Database = dbName,
|
||||||
|
Host = _configFileProvider.PostgresHost,
|
||||||
|
Username = _configFileProvider.PostgresUser,
|
||||||
|
Password = _configFileProvider.PostgresPassword,
|
||||||
|
Port = _configFileProvider.PostgresPort,
|
||||||
|
Enlist = false
|
||||||
|
};
|
||||||
|
|
||||||
connectionBuilder.Database = dbName;
|
return new DatabaseConnectionInfo(DatabaseType.PostgreSQL, connectionBuilder.ConnectionString);
|
||||||
connectionBuilder.Host = _configFileProvider.PostgresHost;
|
|
||||||
connectionBuilder.Username = _configFileProvider.PostgresUser;
|
|
||||||
connectionBuilder.Password = _configFileProvider.PostgresPassword;
|
|
||||||
connectionBuilder.Port = _configFileProvider.PostgresPort;
|
|
||||||
connectionBuilder.Enlist = false;
|
|
||||||
|
|
||||||
return connectionBuilder.ConnectionString;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
|
using System.Data.SQLite;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using Dapper;
|
using Dapper;
|
||||||
using NLog;
|
using NLog;
|
||||||
@@ -40,14 +41,7 @@ namespace NzbDrone.Core.Datastore
|
|||||||
{
|
{
|
||||||
using (var db = _datamapperFactory())
|
using (var db = _datamapperFactory())
|
||||||
{
|
{
|
||||||
if (db.ConnectionString.Contains(".db"))
|
return db is SQLiteConnection ? DatabaseType.SQLite : DatabaseType.PostgreSQL;
|
||||||
{
|
|
||||||
return DatabaseType.SQLite;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return DatabaseType.PostgreSQL;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
namespace NzbDrone.Core.Datastore
|
||||||
|
{
|
||||||
|
public class DatabaseConnectionInfo
|
||||||
|
{
|
||||||
|
public DatabaseConnectionInfo(DatabaseType databaseType, string connectionString)
|
||||||
|
{
|
||||||
|
DatabaseType = databaseType;
|
||||||
|
ConnectionString = connectionString;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DatabaseType DatabaseType { get; internal set; }
|
||||||
|
public string ConnectionString { get; internal set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ using System;
|
|||||||
using System.Data.Common;
|
using System.Data.Common;
|
||||||
using System.Data.SQLite;
|
using System.Data.SQLite;
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
|
using System.Threading;
|
||||||
using NLog;
|
using NLog;
|
||||||
using Npgsql;
|
using Npgsql;
|
||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
@@ -59,30 +60,30 @@ namespace NzbDrone.Core.Datastore
|
|||||||
|
|
||||||
public IDatabase Create(MigrationContext migrationContext)
|
public IDatabase Create(MigrationContext migrationContext)
|
||||||
{
|
{
|
||||||
string connectionString;
|
DatabaseConnectionInfo connectionInfo;
|
||||||
|
|
||||||
switch (migrationContext.MigrationType)
|
switch (migrationContext.MigrationType)
|
||||||
{
|
{
|
||||||
case MigrationType.Main:
|
case MigrationType.Main:
|
||||||
{
|
{
|
||||||
connectionString = _connectionStringFactory.MainDbConnectionString;
|
connectionInfo = _connectionStringFactory.MainDbConnection;
|
||||||
CreateMain(connectionString, migrationContext);
|
CreateMain(connectionInfo.ConnectionString, migrationContext, connectionInfo.DatabaseType);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case MigrationType.Log:
|
case MigrationType.Log:
|
||||||
{
|
{
|
||||||
connectionString = _connectionStringFactory.LogDbConnectionString;
|
connectionInfo = _connectionStringFactory.LogDbConnection;
|
||||||
CreateLog(connectionString, migrationContext);
|
CreateLog(connectionInfo.ConnectionString, migrationContext, connectionInfo.DatabaseType);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case MigrationType.Cache:
|
case MigrationType.Cache:
|
||||||
{
|
{
|
||||||
connectionString = _connectionStringFactory.CacheDbConnectionString;
|
connectionInfo = _connectionStringFactory.CacheDbConnection;
|
||||||
CreateLog(connectionString, migrationContext);
|
CreateLog(connectionInfo.ConnectionString, migrationContext, connectionInfo.DatabaseType);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -97,14 +98,14 @@ namespace NzbDrone.Core.Datastore
|
|||||||
{
|
{
|
||||||
DbConnection conn;
|
DbConnection conn;
|
||||||
|
|
||||||
if (connectionString.Contains(".db"))
|
if (connectionInfo.DatabaseType == DatabaseType.SQLite)
|
||||||
{
|
{
|
||||||
conn = SQLiteFactory.Instance.CreateConnection();
|
conn = SQLiteFactory.Instance.CreateConnection();
|
||||||
conn.ConnectionString = connectionString;
|
conn.ConnectionString = connectionInfo.ConnectionString;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
conn = new NpgsqlConnection(connectionString);
|
conn = new NpgsqlConnection(connectionInfo.ConnectionString);
|
||||||
}
|
}
|
||||||
|
|
||||||
conn.Open();
|
conn.Open();
|
||||||
@@ -114,12 +115,12 @@ namespace NzbDrone.Core.Datastore
|
|||||||
return db;
|
return db;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CreateMain(string connectionString, MigrationContext migrationContext)
|
private void CreateMain(string connectionString, MigrationContext migrationContext, DatabaseType databaseType)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_restoreDatabaseService.Restore();
|
_restoreDatabaseService.Restore();
|
||||||
_migrationController.Migrate(connectionString, migrationContext);
|
_migrationController.Migrate(connectionString, migrationContext, databaseType);
|
||||||
}
|
}
|
||||||
catch (SQLiteException e)
|
catch (SQLiteException e)
|
||||||
{
|
{
|
||||||
@@ -142,15 +143,17 @@ namespace NzbDrone.Core.Datastore
|
|||||||
{
|
{
|
||||||
Logger.Error(e, "Failure to connect to Postgres DB, {0} retries remaining", retryCount);
|
Logger.Error(e, "Failure to connect to Postgres DB, {0} retries remaining", retryCount);
|
||||||
|
|
||||||
|
Thread.Sleep(5000);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_migrationController.Migrate(connectionString, migrationContext);
|
_migrationController.Migrate(connectionString, migrationContext, databaseType);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
if (--retryCount > 0)
|
if (--retryCount > 0)
|
||||||
{
|
{
|
||||||
System.Threading.Thread.Sleep(5000);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,11 +172,11 @@ namespace NzbDrone.Core.Datastore
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CreateLog(string connectionString, MigrationContext migrationContext)
|
private void CreateLog(string connectionString, MigrationContext migrationContext, DatabaseType databaseType)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_migrationController.Migrate(connectionString, migrationContext);
|
_migrationController.Migrate(connectionString, migrationContext, databaseType);
|
||||||
}
|
}
|
||||||
catch (SQLiteException e)
|
catch (SQLiteException e)
|
||||||
{
|
{
|
||||||
@@ -193,7 +196,7 @@ namespace NzbDrone.Core.Datastore
|
|||||||
Logger.Error("Unable to recreate logging database automatically. It will need to be removed manually.");
|
Logger.Error("Unable to recreate logging database automatically. It will need to be removed manually.");
|
||||||
}
|
}
|
||||||
|
|
||||||
_migrationController.Migrate(connectionString, migrationContext);
|
_migrationController.Migrate(connectionString, migrationContext, databaseType);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
|||||||
{
|
{
|
||||||
public interface IMigrationController
|
public interface IMigrationController
|
||||||
{
|
{
|
||||||
void Migrate(string connectionString, MigrationContext migrationContext);
|
void Migrate(string connectionString, MigrationContext migrationContext, DatabaseType databaseType);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class MigrationController : IMigrationController
|
public class MigrationController : IMigrationController
|
||||||
@@ -29,7 +29,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
|||||||
_migrationLoggerProvider = migrationLoggerProvider;
|
_migrationLoggerProvider = migrationLoggerProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Migrate(string connectionString, MigrationContext migrationContext)
|
public void Migrate(string connectionString, MigrationContext migrationContext, DatabaseType databaseType)
|
||||||
{
|
{
|
||||||
var sw = Stopwatch.StartNew();
|
var sw = Stopwatch.StartNew();
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
|||||||
|
|
||||||
ServiceProvider serviceProvider;
|
ServiceProvider serviceProvider;
|
||||||
|
|
||||||
var db = connectionString.Contains(".db") ? "sqlite" : "postgres";
|
var db = databaseType == DatabaseType.SQLite ? "sqlite" : "postgres";
|
||||||
|
|
||||||
serviceProvider = new ServiceCollection()
|
serviceProvider = new ServiceCollection()
|
||||||
.AddLogging(b => b.AddNLog())
|
.AddLogging(b => b.AddNLog())
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ namespace NzbDrone.Core.Datastore
|
|||||||
|
|
||||||
Mapper.Entity<Book>("Books").RegisterModel()
|
Mapper.Entity<Book>("Books").RegisterModel()
|
||||||
.Ignore(x => x.AuthorId)
|
.Ignore(x => x.AuthorId)
|
||||||
|
.Ignore(x => x.ForeignEditionId)
|
||||||
.HasOne(r => r.AuthorMetadata, r => r.AuthorMetadataId)
|
.HasOne(r => r.AuthorMetadata, r => r.AuthorMetadataId)
|
||||||
.LazyLoad(x => x.BookFiles,
|
.LazyLoad(x => x.BookFiles,
|
||||||
(db, book) => db.Query<BookFile>(new SqlBuilder(db.DatabaseType)
|
(db, book) => db.Query<BookFile>(new SqlBuilder(db.DatabaseType)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ namespace NzbDrone.Core.DecisionEngine
|
|||||||
{
|
{
|
||||||
public interface IMakeDownloadDecision
|
public interface IMakeDownloadDecision
|
||||||
{
|
{
|
||||||
List<DownloadDecision> GetRssDecision(List<ReleaseInfo> reports);
|
List<DownloadDecision> GetRssDecision(List<ReleaseInfo> reports, bool pushedRelease = false);
|
||||||
List<DownloadDecision> GetSearchDecision(List<ReleaseInfo> reports, SearchCriteriaBase searchCriteriaBase);
|
List<DownloadDecision> GetSearchDecision(List<ReleaseInfo> reports, SearchCriteriaBase searchCriteriaBase);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,17 +42,17 @@ namespace NzbDrone.Core.DecisionEngine
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<DownloadDecision> GetRssDecision(List<ReleaseInfo> reports)
|
public List<DownloadDecision> GetRssDecision(List<ReleaseInfo> reports, bool pushedRelease = false)
|
||||||
{
|
{
|
||||||
return GetBookDecisions(reports).ToList();
|
return GetBookDecisions(reports).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<DownloadDecision> GetSearchDecision(List<ReleaseInfo> reports, SearchCriteriaBase searchCriteriaBase)
|
public List<DownloadDecision> GetSearchDecision(List<ReleaseInfo> reports, SearchCriteriaBase searchCriteriaBase)
|
||||||
{
|
{
|
||||||
return GetBookDecisions(reports, searchCriteriaBase).ToList();
|
return GetBookDecisions(reports, false, searchCriteriaBase).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerable<DownloadDecision> GetBookDecisions(List<ReleaseInfo> reports, SearchCriteriaBase searchCriteria = null)
|
private IEnumerable<DownloadDecision> GetBookDecisions(List<ReleaseInfo> reports, bool pushedRelease = false, SearchCriteriaBase searchCriteria = null)
|
||||||
{
|
{
|
||||||
if (reports.Any())
|
if (reports.Any())
|
||||||
{
|
{
|
||||||
@@ -206,6 +206,26 @@ namespace NzbDrone.Core.DecisionEngine
|
|||||||
|
|
||||||
if (decision != null)
|
if (decision != null)
|
||||||
{
|
{
|
||||||
|
var source = pushedRelease ? ReleaseSourceType.ReleasePush : ReleaseSourceType.Rss;
|
||||||
|
|
||||||
|
if (searchCriteria != null)
|
||||||
|
{
|
||||||
|
if (searchCriteria.InteractiveSearch)
|
||||||
|
{
|
||||||
|
source = ReleaseSourceType.InteractiveSearch;
|
||||||
|
}
|
||||||
|
else if (searchCriteria.UserInvokedSearch)
|
||||||
|
{
|
||||||
|
source = ReleaseSourceType.UserInvokedSearch;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
source = ReleaseSourceType.Search;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
decision.RemoteBook.ReleaseSource = source;
|
||||||
|
|
||||||
if (decision.Rejections.Any())
|
if (decision.Rejections.Any())
|
||||||
{
|
{
|
||||||
_logger.Debug("Release rejected for the following reasons: {0}", string.Join(", ", decision.Rejections));
|
_logger.Debug("Release rejected for the following reasons: {0}", string.Join(", ", decision.Rejections));
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user