mirror of
https://github.com/Readarr/Readarr.git
synced 2026-04-18 21:34:28 -04:00
Compare commits
126 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| fc402743aa | |||
| b9d53ed732 | |||
| d248747635 | |||
| d70224c811 | |||
| acdf8c8aa8 | |||
| 3ed41554ce | |||
| ce808c6d7b | |||
| 63b1b56a4f | |||
| a5647bedc8 | |||
| fe659bb79d | |||
| 9918535509 | |||
| f9a6db40b8 | |||
| 6273d69ed6 | |||
| 7012380e95 | |||
| b001ecd698 | |||
| e28becdda4 | |||
| eae06695e8 | |||
| 54a9af2ced | |||
| c9b55266fc | |||
| 05b64406a4 | |||
| 1f37c5387b | |||
| 4a6c7042fe | |||
| d7305b9753 | |||
| bd56643eaa | |||
| 44e6de2e23 | |||
| b209d047fa | |||
| fd5ab27df6 | |||
| 4a89befd79 | |||
| 1a30293c33 | |||
| f5c2a6bf51 | |||
| f3d90fdaf1 | |||
| 04c5671a0a | |||
| 22cc88c5e7 | |||
| ca0c95a2d2 | |||
| 419f790d66 | |||
| 9fe08429bc | |||
| 71f4a88ab3 | |||
| 30b283eda3 | |||
| e23d0bbfa1 | |||
| 765a2aa01b | |||
| 64895c3210 | |||
| 03ab84a814 | |||
| b77e5b14e1 |
@@ -1,5 +1,5 @@
|
|||||||
name: Bug Report
|
name: Bug Report
|
||||||
description: 'Report a new bug, if you are not 100% certain this is a bug please go to our Reddit or Discord first'
|
description: 'Report a new bug, if you are not 100% certain this is a bug please go to our Discord first'
|
||||||
labels: ['Type: Bug', 'Status: Needs Triage']
|
labels: ['Type: Bug', 'Status: Needs Triage']
|
||||||
body:
|
body:
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
|
|||||||
@@ -3,6 +3,3 @@ contact_links:
|
|||||||
- name: Support via Discord
|
- name: Support via Discord
|
||||||
url: https://readarr.com/discord
|
url: https://readarr.com/discord
|
||||||
about: Chat with users and devs on support and setup related topics.
|
about: Chat with users and devs on support and setup related topics.
|
||||||
- name: Support via Reddit
|
|
||||||
url: https://reddit.com/r/Readarr
|
|
||||||
about: Discuss and search thru support topics.
|
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ jobs:
|
|||||||
issue-comment: >
|
issue-comment: >
|
||||||
:wave: @{issue-author}, we use the issue tracker exclusively
|
:wave: @{issue-author}, we use the issue tracker exclusively
|
||||||
for bug reports and feature requests. However, this issue appears
|
for bug reports and feature requests. However, this issue appears
|
||||||
to be a support request. Please hop over onto our [Discord](https://readarr.com/discord)
|
to be a support request. Please hop over onto our [Discord](https://readarr.com/discord).
|
||||||
or [Subreddit](https://reddit.com/r/readarr)
|
|
||||||
close-issue: true
|
close-issue: true
|
||||||
lock-issue: false
|
lock-issue: false
|
||||||
- uses: dessant/support-requests@v3
|
- uses: dessant/support-requests@v3
|
||||||
|
|||||||
+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.4'
|
majorVersion: '0.3.14'
|
||||||
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;
|
||||||
|
|||||||
@@ -7,13 +7,10 @@ function findImage(images, coverType) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getUrl(image, coverType, size) {
|
function getUrl(image, coverType, size) {
|
||||||
if (image) {
|
const imageUrl = image?.url;
|
||||||
// Remove protocol
|
|
||||||
let url = image.url;
|
|
||||||
|
|
||||||
url = url.replace(`${coverType}.jpg`, `${coverType}-${size}.jpg`);
|
if (imageUrl) {
|
||||||
|
return imageUrl.replace(`${coverType}.jpg`, `${coverType}-${size}.jpg`);
|
||||||
return url;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -136,8 +136,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
|
font-weight: 300;
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
line-height: 50px;
|
line-height: 30px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,12 +25,7 @@ const defaultFontSize = parseInt(fonts.defaultFontSize);
|
|||||||
const lineHeight = parseFloat(fonts.lineHeight);
|
const lineHeight = parseFloat(fonts.lineHeight);
|
||||||
|
|
||||||
function getFanartUrl(images) {
|
function getFanartUrl(images) {
|
||||||
const fanartImage = images.find((x) => x.coverType === 'fanart');
|
return images.find((x) => x.coverType === 'fanart')?.url;
|
||||||
|
|
||||||
if (fanartImage) {
|
|
||||||
// Remove protocol
|
|
||||||
return fanartImage.url.replace(/^https?:/, '');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class AuthorDetailsHeader extends Component {
|
class AuthorDetailsHeader extends Component {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -117,8 +117,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
|
font-weight: 300;
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
line-height: 50px;
|
line-height: 30px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,12 +21,7 @@ const defaultFontSize = parseInt(fonts.defaultFontSize);
|
|||||||
const lineHeight = parseFloat(fonts.lineHeight);
|
const lineHeight = parseFloat(fonts.lineHeight);
|
||||||
|
|
||||||
function getFanartUrl(images) {
|
function getFanartUrl(images) {
|
||||||
const fanartImage = images.find((x) => x.coverType === 'fanart');
|
return images.find((x) => x.coverType === 'fanart')?.url;
|
||||||
|
|
||||||
if (fanartImage) {
|
|
||||||
// Remove protocol
|
|
||||||
return fanartImage.url.replace(/^https?:/, '');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class BookDetailsHeader extends Component {
|
class BookDetailsHeader extends Component {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ class Notification extends Component {
|
|||||||
onReleaseImport,
|
onReleaseImport,
|
||||||
onUpgrade,
|
onUpgrade,
|
||||||
onRename,
|
onRename,
|
||||||
|
onAuthorAdded,
|
||||||
onAuthorDelete,
|
onAuthorDelete,
|
||||||
onBookDelete,
|
onBookDelete,
|
||||||
onBookFileDelete,
|
onBookFileDelete,
|
||||||
@@ -73,6 +74,7 @@ class Notification extends Component {
|
|||||||
supportsOnReleaseImport,
|
supportsOnReleaseImport,
|
||||||
supportsOnUpgrade,
|
supportsOnUpgrade,
|
||||||
supportsOnRename,
|
supportsOnRename,
|
||||||
|
supportsOnAuthorAdded,
|
||||||
supportsOnAuthorDelete,
|
supportsOnAuthorDelete,
|
||||||
supportsOnBookDelete,
|
supportsOnBookDelete,
|
||||||
supportsOnBookFileDelete,
|
supportsOnBookFileDelete,
|
||||||
@@ -136,6 +138,14 @@ class Notification extends Component {
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
supportsOnAuthorAdded && onAuthorAdded ?
|
||||||
|
<Label kind={kinds.SUCCESS}>
|
||||||
|
{translate('OnAuthorAdded')}
|
||||||
|
</Label> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
supportsOnAuthorDelete && onAuthorDelete ?
|
supportsOnAuthorDelete && onAuthorDelete ?
|
||||||
<Label kind={kinds.SUCCESS}>
|
<Label kind={kinds.SUCCESS}>
|
||||||
@@ -244,6 +254,7 @@ Notification.propTypes = {
|
|||||||
onReleaseImport: PropTypes.bool.isRequired,
|
onReleaseImport: PropTypes.bool.isRequired,
|
||||||
onUpgrade: PropTypes.bool.isRequired,
|
onUpgrade: PropTypes.bool.isRequired,
|
||||||
onRename: PropTypes.bool.isRequired,
|
onRename: PropTypes.bool.isRequired,
|
||||||
|
onAuthorAdded: PropTypes.bool.isRequired,
|
||||||
onAuthorDelete: PropTypes.bool.isRequired,
|
onAuthorDelete: PropTypes.bool.isRequired,
|
||||||
onBookDelete: PropTypes.bool.isRequired,
|
onBookDelete: PropTypes.bool.isRequired,
|
||||||
onBookFileDelete: PropTypes.bool.isRequired,
|
onBookFileDelete: PropTypes.bool.isRequired,
|
||||||
@@ -257,6 +268,7 @@ Notification.propTypes = {
|
|||||||
supportsOnReleaseImport: PropTypes.bool.isRequired,
|
supportsOnReleaseImport: PropTypes.bool.isRequired,
|
||||||
supportsOnUpgrade: PropTypes.bool.isRequired,
|
supportsOnUpgrade: PropTypes.bool.isRequired,
|
||||||
supportsOnRename: PropTypes.bool.isRequired,
|
supportsOnRename: PropTypes.bool.isRequired,
|
||||||
|
supportsOnAuthorAdded: PropTypes.bool.isRequired,
|
||||||
supportsOnAuthorDelete: PropTypes.bool.isRequired,
|
supportsOnAuthorDelete: PropTypes.bool.isRequired,
|
||||||
supportsOnBookDelete: PropTypes.bool.isRequired,
|
supportsOnBookDelete: PropTypes.bool.isRequired,
|
||||||
supportsOnBookFileDelete: PropTypes.bool.isRequired,
|
supportsOnBookFileDelete: PropTypes.bool.isRequired,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ function NotificationEventItems(props) {
|
|||||||
onReleaseImport,
|
onReleaseImport,
|
||||||
onUpgrade,
|
onUpgrade,
|
||||||
onRename,
|
onRename,
|
||||||
|
onAuthorAdded,
|
||||||
onAuthorDelete,
|
onAuthorDelete,
|
||||||
onBookDelete,
|
onBookDelete,
|
||||||
onBookFileDelete,
|
onBookFileDelete,
|
||||||
@@ -32,6 +33,7 @@ function NotificationEventItems(props) {
|
|||||||
supportsOnReleaseImport,
|
supportsOnReleaseImport,
|
||||||
supportsOnUpgrade,
|
supportsOnUpgrade,
|
||||||
supportsOnRename,
|
supportsOnRename,
|
||||||
|
supportsOnAuthorAdded,
|
||||||
supportsOnAuthorDelete,
|
supportsOnAuthorDelete,
|
||||||
supportsOnBookDelete,
|
supportsOnBookDelete,
|
||||||
supportsOnBookFileDelete,
|
supportsOnBookFileDelete,
|
||||||
@@ -123,6 +125,17 @@ function NotificationEventItems(props) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="onAuthorAdded"
|
||||||
|
helpText={translate('OnAuthorAddedHelpText')}
|
||||||
|
isDisabled={!supportsOnAuthorAdded.value}
|
||||||
|
{...onAuthorAdded}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ export default {
|
|||||||
selectedSchema.onReleaseImport = selectedSchema.supportsOnReleaseImport;
|
selectedSchema.onReleaseImport = selectedSchema.supportsOnReleaseImport;
|
||||||
selectedSchema.onUpgrade = selectedSchema.supportsOnUpgrade;
|
selectedSchema.onUpgrade = selectedSchema.supportsOnUpgrade;
|
||||||
selectedSchema.onRename = selectedSchema.supportsOnRename;
|
selectedSchema.onRename = selectedSchema.supportsOnRename;
|
||||||
|
selectedSchema.onAuthorAdded = selectedSchema.supportsOnAuthorAdded;
|
||||||
selectedSchema.onAuthorDelete = selectedSchema.supportsOnAuthorDelete;
|
selectedSchema.onAuthorDelete = selectedSchema.supportsOnAuthorDelete;
|
||||||
selectedSchema.onBookDelete = selectedSchema.supportsOnBookDelete;
|
selectedSchema.onBookDelete = selectedSchema.supportsOnBookDelete;
|
||||||
selectedSchema.onBookFileDelete = selectedSchema.supportsOnBookFileDelete;
|
selectedSchema.onBookFileDelete = selectedSchema.supportsOnBookFileDelete;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ function getInternalLink(source) {
|
|||||||
function getTestLink(source, props) {
|
function getTestLink(source, props) {
|
||||||
switch (source) {
|
switch (source) {
|
||||||
case 'IndexerStatusCheck':
|
case 'IndexerStatusCheck':
|
||||||
|
case 'IndexerLongTermStatusCheck':
|
||||||
return (
|
return (
|
||||||
<SpinnerIconButton
|
<SpinnerIconButton
|
||||||
name={icons.TEST}
|
name={icons.TEST}
|
||||||
|
|||||||
@@ -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,7 +25,7 @@ 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> = { appName: 'Readarr' }
|
||||||
) {
|
) {
|
||||||
const translation = translations[key] || key;
|
const translation = translations[key] || key;
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
+3
-2
@@ -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",
|
||||||
@@ -95,6 +95,7 @@
|
|||||||
"@babel/preset-react": "7.22.5",
|
"@babel/preset-react": "7.22.5",
|
||||||
"@babel/preset-typescript": "7.22.11",
|
"@babel/preset-typescript": "7.22.11",
|
||||||
"@types/lodash": "4.14.197",
|
"@types/lodash": "4.14.197",
|
||||||
|
"@types/react-lazyload": "3.2.1",
|
||||||
"@types/redux-actions": "2.6.2",
|
"@types/redux-actions": "2.6.2",
|
||||||
"@typescript-eslint/eslint-plugin": "6.5.0",
|
"@typescript-eslint/eslint-plugin": "6.5.0",
|
||||||
"@typescript-eslint/parser": "6.5.0",
|
"@typescript-eslint/parser": "6.5.0",
|
||||||
@@ -120,7 +121,7 @@
|
|||||||
"html-webpack-plugin": "5.5.3",
|
"html-webpack-plugin": "5.5.3",
|
||||||
"loader-utils": "^3.2.1",
|
"loader-utils": "^3.2.1",
|
||||||
"mini-css-extract-plugin": "2.7.6",
|
"mini-css-extract-plugin": "2.7.6",
|
||||||
"postcss": "8.4.23",
|
"postcss": "8.4.31",
|
||||||
"postcss-color-function": "4.1.0",
|
"postcss-color-function": "4.1.0",
|
||||||
"postcss-loader": "7.3.0",
|
"postcss-loader": "7.3.0",
|
||||||
"postcss-mixins": "9.0.4",
|
"postcss-mixins": "9.0.4",
|
||||||
|
|||||||
@@ -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()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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")]
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ namespace NzbDrone.Common.Extensions
|
|||||||
{
|
{
|
||||||
if (text.IsNullOrWhiteSpace())
|
if (text.IsNullOrWhiteSpace())
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException("text");
|
throw new ArgumentNullException(nameof(text));
|
||||||
}
|
}
|
||||||
|
|
||||||
return text.IndexOfAny(Path.GetInvalidPathChars()) >= 0;
|
return text.IndexOfAny(Path.GetInvalidPathChars()) >= 0;
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ namespace NzbDrone.Common.Http.Dispatchers
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
data = responseMessage.Content.ReadAsByteArrayAsync(cts.Token).GetAwaiter().GetResult();
|
data = await responseMessage.Content.ReadAsByteArrayAsync(cts.Token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-2
@@ -1,9 +1,10 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using FizzWare.NBuilder;
|
using FizzWare.NBuilder;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Moq;
|
using Moq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NzbDrone.Core.Books;
|
using NzbDrone.Core.Books;
|
||||||
|
using NzbDrone.Core.Datastore;
|
||||||
using NzbDrone.Core.DecisionEngine.Specifications.RssSync;
|
using NzbDrone.Core.DecisionEngine.Specifications.RssSync;
|
||||||
using NzbDrone.Core.Indexers;
|
using NzbDrone.Core.Indexers;
|
||||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||||
@@ -33,8 +34,13 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
|
|||||||
};
|
};
|
||||||
|
|
||||||
Mocker
|
Mocker
|
||||||
.GetMock<IIndexerRepository>()
|
.GetMock<IIndexerFactory>()
|
||||||
.Setup(m => m.Get(It.IsAny<int>()))
|
.Setup(m => m.Get(It.IsAny<int>()))
|
||||||
|
.Throws(new ModelNotFoundException(typeof(IndexerDefinition), -1));
|
||||||
|
|
||||||
|
Mocker
|
||||||
|
.GetMock<IIndexerFactory>()
|
||||||
|
.Setup(m => m.Get(1))
|
||||||
.Returns(_fakeIndexerDefinition);
|
.Returns(_fakeIndexerDefinition);
|
||||||
|
|
||||||
_specification = Mocker.Resolve<IndexerTagSpecification>();
|
_specification = Mocker.Resolve<IndexerTagSpecification>();
|
||||||
@@ -106,5 +112,25 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
|
|||||||
|
|
||||||
_specification.IsSatisfiedBy(_parseResultMulti, new BookSearchCriteria { MonitoredBooksOnly = true }).Accepted.Should().BeFalse();
|
_specification.IsSatisfiedBy(_parseResultMulti, new BookSearchCriteria { MonitoredBooksOnly = true }).Accepted.Should().BeFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void release_without_indexerid_should_return_true()
|
||||||
|
{
|
||||||
|
_fakeIndexerDefinition.Tags = new HashSet<int> { 456 };
|
||||||
|
_fakeAuthor.Tags = new HashSet<int> { 123, 789 };
|
||||||
|
_fakeRelease.IndexerId = 0;
|
||||||
|
|
||||||
|
_specification.IsSatisfiedBy(_parseResultMulti, new BookSearchCriteria { MonitoredBooksOnly = true }).Accepted.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void release_with_invalid_indexerid_should_return_true()
|
||||||
|
{
|
||||||
|
_fakeIndexerDefinition.Tags = new HashSet<int> { 456 };
|
||||||
|
_fakeAuthor.Tags = new HashSet<int> { 123, 789 };
|
||||||
|
_fakeRelease.IndexerId = 2;
|
||||||
|
|
||||||
|
_specification.IsSatisfiedBy(_parseResultMulti, new BookSearchCriteria { MonitoredBooksOnly = true }).Accepted.Should().BeTrue();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -405,7 +405,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||||||
Size = 1000,
|
Size = 1000,
|
||||||
Progress = 0.7,
|
Progress = 0.7,
|
||||||
Eta = 8640000,
|
Eta = 8640000,
|
||||||
State = "stalledDL",
|
State = "pausedUP",
|
||||||
Label = "",
|
Label = "",
|
||||||
SavePath = @"C:\Torrents".AsOsAgnostic(),
|
SavePath = @"C:\Torrents".AsOsAgnostic(),
|
||||||
ContentPath = @"C:\Torrents\Droned.S01.12".AsOsAgnostic()
|
ContentPath = @"C:\Torrents\Droned.S01.12".AsOsAgnostic()
|
||||||
|
|||||||
@@ -452,6 +452,30 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests
|
|||||||
result.OutputRootFolders.First().Should().Be(fullCategoryDir);
|
result.OutputRootFolders.First().Should().Be(fullCategoryDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestCase("0")]
|
||||||
|
[TestCase("15d")]
|
||||||
|
public void should_set_history_removes_completed_downloads_false(string historyRetention)
|
||||||
|
{
|
||||||
|
_config.Misc.history_retention = historyRetention;
|
||||||
|
|
||||||
|
var downloadClientInfo = Subject.GetStatus();
|
||||||
|
|
||||||
|
downloadClientInfo.RemovesCompletedDownloads.Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase("-1")]
|
||||||
|
[TestCase("15")]
|
||||||
|
[TestCase("3")]
|
||||||
|
[TestCase("3d")]
|
||||||
|
public void should_set_history_removes_completed_downloads_true(string historyRetention)
|
||||||
|
{
|
||||||
|
_config.Misc.history_retention = historyRetention;
|
||||||
|
|
||||||
|
var downloadClientInfo = Subject.GetStatus();
|
||||||
|
|
||||||
|
downloadClientInfo.RemovesCompletedDownloads.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
[TestCase(@"Y:\nzbget\root", @"completed\downloads", @"vv", @"Y:\nzbget\root\completed\downloads", @"Y:\nzbget\root\completed\downloads\vv")]
|
[TestCase(@"Y:\nzbget\root", @"completed\downloads", @"vv", @"Y:\nzbget\root\completed\downloads", @"Y:\nzbget\root\completed\downloads\vv")]
|
||||||
[TestCase(@"Y:\nzbget\root", @"completed", @"vv", @"Y:\nzbget\root\completed", @"Y:\nzbget\root\completed\vv")]
|
[TestCase(@"Y:\nzbget\root", @"completed", @"vv", @"Y:\nzbget\root\completed", @"Y:\nzbget\root\completed\vv")]
|
||||||
[TestCase(@"/nzbget/root", @"completed/downloads", @"vv", @"/nzbget/root/completed/downloads", @"/nzbget/root/completed/downloads/vv")]
|
[TestCase(@"/nzbget/root", @"completed/downloads", @"vv", @"/nzbget/root/completed/downloads", @"/nzbget/root/completed/downloads/vv")]
|
||||||
|
|||||||
+78
@@ -0,0 +1,78 @@
|
|||||||
|
using System;
|
||||||
|
using Moq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Core.Download;
|
||||||
|
using NzbDrone.Core.Download.Clients;
|
||||||
|
using NzbDrone.Core.HealthCheck.Checks;
|
||||||
|
using NzbDrone.Core.Localization;
|
||||||
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
using NzbDrone.Test.Common;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Test.HealthCheck.Checks
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class DownloadClientRemovesCompletedDownloadsCheckFixture : CoreTest<DownloadClientRemovesCompletedDownloadsCheck>
|
||||||
|
{
|
||||||
|
private DownloadClientInfo _clientStatus;
|
||||||
|
private Mock<IDownloadClient> _downloadClient;
|
||||||
|
|
||||||
|
private static Exception[] DownloadClientExceptions =
|
||||||
|
{
|
||||||
|
new DownloadClientUnavailableException("error"),
|
||||||
|
new DownloadClientAuthenticationException("error"),
|
||||||
|
new DownloadClientException("error")
|
||||||
|
};
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
_clientStatus = new DownloadClientInfo
|
||||||
|
{
|
||||||
|
IsLocalhost = true,
|
||||||
|
|
||||||
|
// SortingMode = null,
|
||||||
|
RemovesCompletedDownloads = true
|
||||||
|
};
|
||||||
|
|
||||||
|
_downloadClient = Mocker.GetMock<IDownloadClient>();
|
||||||
|
_downloadClient.Setup(s => s.Definition)
|
||||||
|
.Returns(new DownloadClientDefinition { Name = "Test" });
|
||||||
|
|
||||||
|
_downloadClient.Setup(s => s.GetStatus())
|
||||||
|
.Returns(_clientStatus);
|
||||||
|
|
||||||
|
Mocker.GetMock<IProvideDownloadClient>()
|
||||||
|
.Setup(s => s.GetDownloadClients(It.IsAny<bool>()))
|
||||||
|
.Returns(new IDownloadClient[] { _downloadClient.Object });
|
||||||
|
|
||||||
|
Mocker.GetMock<ILocalizationService>()
|
||||||
|
.Setup(s => s.GetLocalizedString(It.IsAny<string>()))
|
||||||
|
.Returns("Some Warning Message");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_warning_if_removing_completed_downloads_is_enabled()
|
||||||
|
{
|
||||||
|
Subject.Check().ShouldBeWarning();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_ok_if_remove_completed_downloads_is_not_enabled()
|
||||||
|
{
|
||||||
|
_clientStatus.RemovesCompletedDownloads = false;
|
||||||
|
Subject.Check().ShouldBeOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[TestCaseSource(nameof(DownloadClientExceptions))]
|
||||||
|
public void should_return_ok_if_client_throws_downloadclientexception(Exception ex)
|
||||||
|
{
|
||||||
|
_downloadClient.Setup(s => s.GetStatus())
|
||||||
|
.Throws(ex);
|
||||||
|
|
||||||
|
Subject.Check().ShouldBeOk();
|
||||||
|
|
||||||
|
ExceptionVerification.ExpectedErrors(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = "2023-12-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 = "2023-12-31 00:00:00Z")]
|
||||||
public class BookInfoProxySearchFixture : CoreTest<BookInfoProxy>
|
public class BookInfoProxySearchFixture : CoreTest<BookInfoProxy>
|
||||||
{
|
{
|
||||||
[SetUp]
|
[SetUp]
|
||||||
|
|||||||
@@ -64,6 +64,11 @@ namespace NzbDrone.Core.Test.NotificationTests
|
|||||||
TestLogger.Info("OnRename was called");
|
TestLogger.Info("OnRename was called");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override void OnAuthorAdded(Author author)
|
||||||
|
{
|
||||||
|
TestLogger.Info("OnAuthorAdded was called");
|
||||||
|
}
|
||||||
|
|
||||||
public override void OnAuthorDelete(AuthorDeleteMessage message)
|
public override void OnAuthorDelete(AuthorDeleteMessage message)
|
||||||
{
|
{
|
||||||
TestLogger.Info("OnAuthorDelete was called");
|
TestLogger.Info("OnAuthorDelete was called");
|
||||||
@@ -138,6 +143,7 @@ namespace NzbDrone.Core.Test.NotificationTests
|
|||||||
notification.SupportsOnUpgrade.Should().BeTrue();
|
notification.SupportsOnUpgrade.Should().BeTrue();
|
||||||
notification.SupportsOnRename.Should().BeTrue();
|
notification.SupportsOnRename.Should().BeTrue();
|
||||||
notification.SupportsOnHealthIssue.Should().BeTrue();
|
notification.SupportsOnHealthIssue.Should().BeTrue();
|
||||||
|
notification.SupportsOnAuthorAdded.Should().BeTrue();
|
||||||
notification.SupportsOnAuthorDelete.Should().BeTrue();
|
notification.SupportsOnAuthorDelete.Should().BeTrue();
|
||||||
notification.SupportsOnBookDelete.Should().BeTrue();
|
notification.SupportsOnBookDelete.Should().BeTrue();
|
||||||
notification.SupportsOnBookFileDelete.Should().BeTrue();
|
notification.SupportsOnBookFileDelete.Should().BeTrue();
|
||||||
@@ -157,6 +163,7 @@ namespace NzbDrone.Core.Test.NotificationTests
|
|||||||
notification.SupportsOnReleaseImport.Should().BeFalse();
|
notification.SupportsOnReleaseImport.Should().BeFalse();
|
||||||
notification.SupportsOnUpgrade.Should().BeFalse();
|
notification.SupportsOnUpgrade.Should().BeFalse();
|
||||||
notification.SupportsOnRename.Should().BeFalse();
|
notification.SupportsOnRename.Should().BeFalse();
|
||||||
|
notification.SupportsOnAuthorAdded.Should().BeFalse();
|
||||||
notification.SupportsOnAuthorDelete.Should().BeFalse();
|
notification.SupportsOnAuthorDelete.Should().BeFalse();
|
||||||
notification.SupportsOnBookDelete.Should().BeFalse();
|
notification.SupportsOnBookDelete.Should().BeFalse();
|
||||||
notification.SupportsOnBookFileDelete.Should().BeFalse();
|
notification.SupportsOnBookFileDelete.Should().BeFalse();
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using FluentMigrator;
|
||||||
|
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Datastore.Migration
|
||||||
|
{
|
||||||
|
[Migration(038)]
|
||||||
|
public class add_on_author_added_to_notifications : NzbDroneMigrationBase
|
||||||
|
{
|
||||||
|
protected override void MainDbUpgrade()
|
||||||
|
{
|
||||||
|
Alter.Table("Notifications").AddColumn("OnAuthorAdded").AsBoolean().WithDefaultValue(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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())
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ namespace NzbDrone.Core.Datastore
|
|||||||
.Ignore(i => i.SupportsOnReleaseImport)
|
.Ignore(i => i.SupportsOnReleaseImport)
|
||||||
.Ignore(i => i.SupportsOnUpgrade)
|
.Ignore(i => i.SupportsOnUpgrade)
|
||||||
.Ignore(i => i.SupportsOnRename)
|
.Ignore(i => i.SupportsOnRename)
|
||||||
|
.Ignore(i => i.SupportsOnAuthorAdded)
|
||||||
.Ignore(i => i.SupportsOnAuthorDelete)
|
.Ignore(i => i.SupportsOnAuthorDelete)
|
||||||
.Ignore(i => i.SupportsOnBookDelete)
|
.Ignore(i => i.SupportsOnBookDelete)
|
||||||
.Ignore(i => i.SupportsOnBookFileDelete)
|
.Ignore(i => i.SupportsOnBookFileDelete)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NLog;
|
using NLog;
|
||||||
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.CustomFormats;
|
using NzbDrone.Core.CustomFormats;
|
||||||
using NzbDrone.Core.Download.TrackedDownloads;
|
using NzbDrone.Core.Download.TrackedDownloads;
|
||||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||||
@@ -15,16 +16,19 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
|||||||
private readonly IQueueService _queueService;
|
private readonly IQueueService _queueService;
|
||||||
private readonly UpgradableSpecification _upgradableSpecification;
|
private readonly UpgradableSpecification _upgradableSpecification;
|
||||||
private readonly ICustomFormatCalculationService _formatService;
|
private readonly ICustomFormatCalculationService _formatService;
|
||||||
|
private readonly IConfigService _configService;
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
|
|
||||||
public QueueSpecification(IQueueService queueService,
|
public QueueSpecification(IQueueService queueService,
|
||||||
UpgradableSpecification upgradableSpecification,
|
UpgradableSpecification upgradableSpecification,
|
||||||
ICustomFormatCalculationService formatService,
|
ICustomFormatCalculationService formatService,
|
||||||
|
IConfigService configService,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
{
|
{
|
||||||
_queueService = queueService;
|
_queueService = queueService;
|
||||||
_upgradableSpecification = upgradableSpecification;
|
_upgradableSpecification = upgradableSpecification;
|
||||||
_formatService = formatService;
|
_formatService = formatService;
|
||||||
|
_configService = configService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,6 +89,15 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
|||||||
{
|
{
|
||||||
return Decision.Reject("Another release is queued and the Quality profile does not allow upgrades");
|
return Decision.Reject("Another release is queued and the Quality profile does not allow upgrades");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_upgradableSpecification.IsRevisionUpgrade(remoteBook.ParsedBookInfo.Quality, subject.ParsedBookInfo.Quality))
|
||||||
|
{
|
||||||
|
if (_configService.DownloadPropersAndRepacks == ProperDownloadTypes.DoNotUpgrade)
|
||||||
|
{
|
||||||
|
_logger.Debug("Auto downloading of propers is disabled");
|
||||||
|
return Decision.Reject("Proper downloading is disabled");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Decision.Accept();
|
return Decision.Accept();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
|
using NzbDrone.Core.Datastore;
|
||||||
using NzbDrone.Core.Indexers;
|
using NzbDrone.Core.Indexers;
|
||||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
@@ -10,12 +11,12 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync
|
|||||||
public class IndexerTagSpecification : IDecisionEngineSpecification
|
public class IndexerTagSpecification : IDecisionEngineSpecification
|
||||||
{
|
{
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
private readonly IIndexerRepository _indexerRepository;
|
private readonly IIndexerFactory _indexerFactory;
|
||||||
|
|
||||||
public IndexerTagSpecification(Logger logger, IIndexerRepository indexerRepository)
|
public IndexerTagSpecification(Logger logger, IIndexerFactory indexerFactory)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_indexerRepository = indexerRepository;
|
_indexerFactory = indexerFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SpecificationPriority Priority => SpecificationPriority.Default;
|
public SpecificationPriority Priority => SpecificationPriority.Default;
|
||||||
@@ -23,8 +24,24 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync
|
|||||||
|
|
||||||
public virtual Decision IsSatisfiedBy(RemoteBook subject, SearchCriteriaBase searchCriteria)
|
public virtual Decision IsSatisfiedBy(RemoteBook subject, SearchCriteriaBase searchCriteria)
|
||||||
{
|
{
|
||||||
// If indexer has tags, check that at least one of them is present on the author
|
if (subject.Release == null || subject.Author?.Tags == null || subject.Release.IndexerId == 0)
|
||||||
var indexerTags = _indexerRepository.Get(subject.Release.IndexerId).Tags;
|
{
|
||||||
|
return Decision.Accept();
|
||||||
|
}
|
||||||
|
|
||||||
|
IndexerDefinition indexer;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
indexer = _indexerFactory.Get(subject.Release.IndexerId);
|
||||||
|
}
|
||||||
|
catch (ModelNotFoundException)
|
||||||
|
{
|
||||||
|
_logger.Debug("Indexer with id {0} does not exist, skipping indexer tags check", subject.Release.IndexerId);
|
||||||
|
return Decision.Accept();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If indexer has tags, check that at least one of them is present on the series
|
||||||
|
var indexerTags = indexer.Tags;
|
||||||
|
|
||||||
if (indexerTags.Any() && indexerTags.Intersect(subject.Author.Tags).Empty())
|
if (indexerTags.Any() && indexerTags.Intersect(subject.Author.Tags).Empty())
|
||||||
{
|
{
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user