parent
d83d2548e5
commit
45d49117ca
@ -0,0 +1,25 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import EditBookModalContentConnector from './EditBookModalContentConnector';
|
||||
|
||||
function EditBookModal({ isOpen, onModalClose, ...otherProps }) {
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<EditBookModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
EditBookModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default EditBookModal;
|
@ -0,0 +1,39 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import EditBookModal from './EditBookModal';
|
||||
|
||||
const mapDispatchToProps = {
|
||||
clearPendingChanges
|
||||
};
|
||||
|
||||
class EditBookModalConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onModalClose = () => {
|
||||
this.props.clearPendingChanges({ section: 'books' });
|
||||
this.props.onModalClose();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EditBookModal
|
||||
{...this.props}
|
||||
onModalClose={this.onModalClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditBookModalConnector.propTypes = {
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
clearPendingChanges: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(undefined, mapDispatchToProps)(EditBookModalConnector);
|
@ -0,0 +1,133 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
|
||||
class EditBookModalContent extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSavePress = () => {
|
||||
const {
|
||||
onSavePress
|
||||
} = this.props;
|
||||
|
||||
onSavePress(false);
|
||||
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
title,
|
||||
authorName,
|
||||
statistics,
|
||||
item,
|
||||
isSaving,
|
||||
onInputChange,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
monitored,
|
||||
anyEditionOk,
|
||||
editions
|
||||
} = item;
|
||||
|
||||
const hasFile = statistics ? statistics.bookFileCount : 0;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Edit - {authorName} - {title}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<Form
|
||||
{...otherProps}
|
||||
>
|
||||
<FormGroup>
|
||||
<FormLabel>Monitored</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="monitored"
|
||||
helpText="Readarr will search for and download book"
|
||||
{...monitored}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Automatically Switch Edition</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="anyEditionOk"
|
||||
helpText="Readarr will automatically switch to the edition best matching downloaded files"
|
||||
{...anyEditionOk}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Edition</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.BOOK_EDITION_SELECT}
|
||||
name="editions"
|
||||
helpText="Change edition for this book"
|
||||
isDisabled={anyEditionOk.value && hasFile}
|
||||
bookEditions={editions}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
</Form>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
onPress={onModalClose}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<SpinnerButton
|
||||
isSpinning={isSaving}
|
||||
onPress={this.onSavePress}
|
||||
>
|
||||
Save
|
||||
</SpinnerButton>
|
||||
</ModalFooter>
|
||||
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditBookModalContent.propTypes = {
|
||||
bookId: PropTypes.number.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
authorName: PropTypes.string.isRequired,
|
||||
statistics: PropTypes.object.isRequired,
|
||||
item: PropTypes.object.isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default EditBookModalContent;
|
@ -0,0 +1,98 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import selectSettings from 'Store/Selectors/selectSettings';
|
||||
import createBookSelector from 'Store/Selectors/createBookSelector';
|
||||
import createAuthorSelector from 'Store/Selectors/createAuthorSelector';
|
||||
import { setBookValue, saveBook } from 'Store/Actions/bookActions';
|
||||
import EditBookModalContent from './EditBookModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.books,
|
||||
createBookSelector(),
|
||||
createAuthorSelector(),
|
||||
(bookState, book, author) => {
|
||||
const {
|
||||
isSaving,
|
||||
saveError,
|
||||
pendingChanges
|
||||
} = bookState;
|
||||
|
||||
const bookSettings = _.pick(book, [
|
||||
'monitored',
|
||||
'anyEditionOk',
|
||||
'editions'
|
||||
]);
|
||||
|
||||
const settings = selectSettings(bookSettings, pendingChanges, saveError);
|
||||
|
||||
return {
|
||||
title: book.title,
|
||||
authorName: author.authorName,
|
||||
bookType: book.bookType,
|
||||
statistics: book.statistics,
|
||||
isSaving,
|
||||
saveError,
|
||||
item: settings.settings,
|
||||
...settings
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchSetBookValue: setBookValue,
|
||||
dispatchSaveBook: saveBook
|
||||
};
|
||||
|
||||
class EditBookModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
|
||||
this.props.onModalClose();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.dispatchSetBookValue({ name, value });
|
||||
}
|
||||
|
||||
onSavePress = () => {
|
||||
this.props.dispatchSaveBook({
|
||||
id: this.props.bookId
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EditBookModalContent
|
||||
{...this.props}
|
||||
onInputChange={this.onInputChange}
|
||||
onSavePress={this.onSavePress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditBookModalContentConnector.propTypes = {
|
||||
bookId: PropTypes.number,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
dispatchSetBookValue: PropTypes.func.isRequired,
|
||||
dispatchSaveBook: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(EditBookModalContentConnector);
|
@ -0,0 +1,93 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import titleCase from 'Utilities/String/titleCase';
|
||||
import SelectInput from './SelectInput';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { bookEditions }) => bookEditions,
|
||||
(bookEditions) => {
|
||||
const values = _.map(bookEditions.value, (bookEdition) => {
|
||||
|
||||
let value = `${bookEdition.title}`;
|
||||
|
||||
if (bookEdition.disambiguation) {
|
||||
value = `${value} (${titleCase(bookEdition.disambiguation)})`;
|
||||
}
|
||||
|
||||
const extras = [];
|
||||
if (bookEdition.language) {
|
||||
extras.push(bookEdition.language);
|
||||
}
|
||||
if (bookEdition.publisher) {
|
||||
extras.push(bookEdition.publisher);
|
||||
}
|
||||
if (bookEdition.isbn13) {
|
||||
extras.push(bookEdition.isbn13);
|
||||
}
|
||||
if (bookEdition.format) {
|
||||
extras.push(bookEdition.format);
|
||||
}
|
||||
if (bookEdition.pageCount > 0) {
|
||||
extras.push(`${bookEdition.pageCount}p`);
|
||||
}
|
||||
|
||||
if (extras) {
|
||||
value = `${value} [${extras.join(', ')}]`;
|
||||
}
|
||||
|
||||
return {
|
||||
key: bookEdition.foreignEditionId,
|
||||
value
|
||||
};
|
||||
});
|
||||
|
||||
const sortedValues = _.orderBy(values, ['value']);
|
||||
|
||||
const value = _.find(bookEditions.value, { monitored: true }).foreignEditionId;
|
||||
|
||||
return {
|
||||
values: sortedValues,
|
||||
value
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
class BookEditionSelectInputConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onChange = ({ name, value }) => {
|
||||
const {
|
||||
bookEditions
|
||||
} = this.props;
|
||||
|
||||
const updatedEditions = _.map(bookEditions.value, (e) => ({ ...e, monitored: false }));
|
||||
_.find(updatedEditions, { foreignEditionId: value }).monitored = true;
|
||||
|
||||
this.props.onChange({ name, value: updatedEditions });
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
return (
|
||||
<SelectInput
|
||||
{...this.props}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BookEditionSelectInputConnector.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
bookEditions: PropTypes.object
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps)(BookEditionSelectInputConnector);
|
@ -1,70 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import titleCase from 'Utilities/String/titleCase';
|
||||
import SelectInput from './SelectInput';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { bookReleases }) => bookReleases,
|
||||
(bookReleases) => {
|
||||
const values = _.map(bookReleases.value, (bookRelease) => {
|
||||
|
||||
return {
|
||||
key: bookRelease.foreignReleaseId,
|
||||
value: `${bookRelease.title}` +
|
||||
`${bookRelease.disambiguation ? ' (' : ''}${titleCase(bookRelease.disambiguation)}${bookRelease.disambiguation ? ')' : ''}` +
|
||||
`, ${bookRelease.mediumCount} med, ${bookRelease.bookCount} books` +
|
||||
`${bookRelease.country.length > 0 ? ', ' : ''}${bookRelease.country}` +
|
||||
`${bookRelease.format ? ', [' : ''}${bookRelease.format}${bookRelease.format ? ']' : ''}`
|
||||
};
|
||||
});
|
||||
|
||||
const sortedValues = _.orderBy(values, ['value']);
|
||||
|
||||
const value = _.find(bookReleases.value, { monitored: true }).foreignReleaseId;
|
||||
|
||||
return {
|
||||
values: sortedValues,
|
||||
value
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
class BookReleaseSelectInputConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onChange = ({ name, value }) => {
|
||||
const {
|
||||
bookReleases
|
||||
} = this.props;
|
||||
|
||||
const updatedReleases = _.map(bookReleases.value, (e) => ({ ...e, monitored: false }));
|
||||
_.find(updatedReleases, { foreignReleaseId: value }).monitored = true;
|
||||
|
||||
this.props.onChange({ name, value: updatedReleases });
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
return (
|
||||
<SelectInput
|
||||
{...this.props}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BookReleaseSelectInputConnector.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
bookReleases: PropTypes.object
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps)(BookReleaseSelectInputConnector);
|
@ -0,0 +1,37 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import SelectEditionModalContentConnector from './SelectEditionModalContentConnector';
|
||||
|
||||
class SelectEditionModal extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<SelectEditionModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectEditionModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default SelectEditionModal;
|
@ -0,0 +1,18 @@
|
||||
.modalBody {
|
||||
composes: modalBody from '~Components/Modal/ModalBody.css';
|
||||
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filterInput {
|
||||
composes: input from '~Components/Form/TextInput.css';
|
||||
|
||||
flex: 0 0 auto;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.scroller {
|
||||
flex: 1 1 auto;
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Button from 'Components/Link/Button';
|
||||
import { scrollDirections } from 'Helpers/Props';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import SelectEditionRow from './SelectEditionRow';
|
||||
import Alert from 'Components/Alert';
|
||||
import styles from './SelectEditionModalContent.css';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: 'book',
|
||||
label: 'Book',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'edition',
|
||||
label: 'Edition',
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
|
||||
class SelectEditionModalContent extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
books,
|
||||
onEditionSelect,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Manual Import - Select Edition
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody
|
||||
className={styles.modalBody}
|
||||
scrollDirection={scrollDirections.NONE}
|
||||
>
|
||||
<Alert>
|
||||
Overrriding an edition here will <b>disable automatic edition selection</b> for that book in future.
|
||||
</Alert>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
{...otherProps}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
books.map((item) => {
|
||||
return (
|
||||
<SelectEditionRow
|
||||
key={item.book.id}
|
||||
matchedEditionId={item.matchedEditionId}
|
||||
columns={columns}
|
||||
onEditionSelect={onEditionSelect}
|
||||
{...item.book}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectEditionModalContent.propTypes = {
|
||||
books: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onEditionSelect: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default SelectEditionModalContent;
|
@ -0,0 +1,63 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
updateInteractiveImportItem,
|
||||
saveInteractiveImportItem
|
||||
} from 'Store/Actions/interactiveImportActions';
|
||||
import SelectEditionModalContent from './SelectEditionModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return {};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
updateInteractiveImportItem,
|
||||
saveInteractiveImportItem
|
||||
};
|
||||
|
||||
class SelectEditionModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onEditionSelect = (bookId, editionId) => {
|
||||
const ids = this.props.importIdsByBook[bookId];
|
||||
|
||||
ids.forEach((id) => {
|
||||
this.props.updateInteractiveImportItem({
|
||||
id,
|
||||
editionId,
|
||||
disableReleaseSwitching: true,
|
||||
tracks: [],
|
||||
rejections: []
|
||||
});
|
||||
});
|
||||
|
||||
this.props.saveInteractiveImportItem({ id: ids });
|
||||
|
||||
this.props.onModalClose(true);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<SelectEditionModalContent
|
||||
{...this.props}
|
||||
onEditionSelect={this.onEditionSelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectEditionModalContentConnector.propTypes = {
|
||||
importIdsByBook: PropTypes.object.isRequired,
|
||||
books: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
updateInteractiveImportItem: PropTypes.func.isRequired,
|
||||
saveInteractiveImportItem: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(SelectEditionModalContentConnector);
|
@ -0,0 +1,3 @@
|
||||
.albumRow {
|
||||
cursor: pointer;
|
||||
}
|
@ -0,0 +1,125 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import titleCase from 'Utilities/String/titleCase';
|
||||
|
||||
class SelectEditionRow extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.onEditionSelect(parseInt(name), parseInt(value));
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
matchedEditionId,
|
||||
title,
|
||||
disambiguation,
|
||||
editions,
|
||||
columns
|
||||
} = this.props;
|
||||
|
||||
const extendedTitle = disambiguation ? `${title} (${disambiguation})` : title;
|
||||
|
||||
const values = _.map(editions, (bookEdition) => {
|
||||
|
||||
let value = `${bookEdition.title}`;
|
||||
|
||||
if (bookEdition.disambiguation) {
|
||||
value = `${value} (${titleCase(bookEdition.disambiguation)})`;
|
||||
}
|
||||
|
||||
const extras = [];
|
||||
if (bookEdition.language) {
|
||||
extras.push(bookEdition.language);
|
||||
}
|
||||
if (bookEdition.publisher) {
|
||||
extras.push(bookEdition.publisher);
|
||||
}
|
||||
if (bookEdition.isbn13) {
|
||||
extras.push(bookEdition.isbn13);
|
||||
}
|
||||
if (bookEdition.format) {
|
||||
extras.push(bookEdition.format);
|
||||
}
|
||||
if (bookEdition.pageCount > 0) {
|
||||
extras.push(`${bookEdition.pageCount}p`);
|
||||
}
|
||||
|
||||
if (extras) {
|
||||
value = `${value} [${extras.join(', ')}]`;
|
||||
}
|
||||
|
||||
return {
|
||||
key: bookEdition.id,
|
||||
value
|
||||
};
|
||||
});
|
||||
|
||||
const sortedValues = _.orderBy(values, ['value']);
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
{
|
||||
columns.map((column) => {
|
||||
const {
|
||||
name,
|
||||
isVisible
|
||||
} = column;
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name === 'book') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{extendedTitle}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'edition') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name={id.toString()}
|
||||
values={sortedValues}
|
||||
value={matchedEditionId}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
}
|
||||
</TableRow>
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectEditionRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
matchedEditionId: PropTypes.number.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
disambiguation: PropTypes.string.isRequired,
|
||||
editions: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onEditionSelect: PropTypes.func.isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired
|
||||
};
|
||||
|
||||
export default SelectEditionRow;
|
@ -0,0 +1,13 @@
|
||||
function stripHtml(html) {
|
||||
if (!html) {
|
||||
return html;
|
||||
}
|
||||
|
||||
const fiddled = html.replace(/<br\/>/g, ' ');
|
||||
|
||||
const doc = new DOMParser().parseFromString(fiddled, 'text/html');
|
||||
const text = doc.body.textContent || '';
|
||||
return text.replace(/([;,.])([^\s.])/g, '$1 $2').replace(/\s{2,}/g, ' ').replace(/s+…/g, '…');
|
||||
}
|
||||
|
||||
export default stripHtml;
|
@ -1,192 +0,0 @@
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.MediaFiles.BookImport.Identification;
|
||||
using NzbDrone.Test.Common;
|
||||
|
||||
namespace NzbDrone.Core.Test.MediaFiles.BookImport.Identification
|
||||
{
|
||||
[TestFixture]
|
||||
public class MunkresFixture : TestBase
|
||||
{
|
||||
// 2d arrays don't play nicely with attributes
|
||||
public void RunTest(double[,] costMatrix, double expectedCost)
|
||||
{
|
||||
var m = new Munkres(costMatrix);
|
||||
m.Run();
|
||||
m.Cost.Should().Be(expectedCost);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void MunkresSquareTest1()
|
||||
{
|
||||
var c = new double[,]
|
||||
{
|
||||
{ 1, 2, 3 },
|
||||
{ 2, 4, 6 },
|
||||
{ 3, 6, 9 }
|
||||
};
|
||||
|
||||
RunTest(c, 10);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void MunkresSquareTest2()
|
||||
{
|
||||
var c = new double[,]
|
||||
{
|
||||
{ 400, 150, 400 },
|
||||
{ 400, 450, 600 },
|
||||
{ 300, 225, 300 }
|
||||
};
|
||||
|
||||
RunTest(c, 850);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void MunkresSquareTest3()
|
||||
{
|
||||
var c = new double[,]
|
||||
{
|
||||
{ 10, 10, 8 },
|
||||
{ 9, 8, 1 },
|
||||
{ 9, 7, 4 }
|
||||
};
|
||||
|
||||
RunTest(c, 18);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void MunkresSquareTest4()
|
||||
{
|
||||
var c = new double[,]
|
||||
{
|
||||
{ 5, 9, 1 },
|
||||
{ 10, 3, 2 },
|
||||
{ 8, 7, 4 }
|
||||
};
|
||||
|
||||
RunTest(c, 12);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void MunkresSquareTest5()
|
||||
{
|
||||
var c = new double[,]
|
||||
{
|
||||
{ 12, 26, 17, 0, 0 },
|
||||
{ 49, 43, 36, 10, 5 },
|
||||
{ 97, 9, 66, 34, 0 },
|
||||
{ 52, 42, 19, 36, 0 },
|
||||
{ 15, 93, 55, 80, 0 }
|
||||
};
|
||||
|
||||
RunTest(c, 48);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Munkres5x5Test()
|
||||
{
|
||||
var c = new double[,]
|
||||
{
|
||||
{ 12, 9, 27, 10, 23 },
|
||||
{ 7, 13, 13, 30, 19 },
|
||||
{ 25, 18, 26, 11, 26 },
|
||||
{ 9, 28, 26, 23, 13 },
|
||||
{ 16, 16, 24, 6, 9 }
|
||||
};
|
||||
|
||||
RunTest(c, 51);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Munkres10x10Test()
|
||||
{
|
||||
var c = new double[,]
|
||||
{
|
||||
{ 37, 34, 29, 26, 19, 8, 9, 23, 19, 29 },
|
||||
{ 9, 28, 20, 8, 18, 20, 14, 33, 23, 14 },
|
||||
{ 15, 26, 12, 28, 6, 17, 9, 13, 21, 7 },
|
||||
{ 2, 8, 38, 36, 39, 5, 36, 2, 38, 27 },
|
||||
{ 30, 3, 33, 16, 21, 39, 7, 23, 28, 36 },
|
||||
{ 7, 5, 19, 22, 36, 36, 24, 19, 30, 2 },
|
||||
{ 34, 20, 13, 36, 12, 33, 9, 10, 23, 5 },
|
||||
{ 7, 37, 22, 39, 33, 39, 10, 3, 13, 26 },
|
||||
{ 21, 25, 23, 39, 31, 37, 32, 33, 38, 1 },
|
||||
{ 17, 34, 40, 10, 29, 37, 40, 3, 25, 3 }
|
||||
};
|
||||
|
||||
RunTest(c, 66);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Munkres20x20Test()
|
||||
{
|
||||
var c = new double[,]
|
||||
{
|
||||
{ 5, 4, 3, 9, 8, 9, 3, 5, 6, 9, 4, 10, 3, 5, 6, 6, 1, 8, 10, 2 },
|
||||
{ 10, 9, 9, 2, 8, 3, 9, 9, 10, 1, 7, 10, 8, 4, 2, 1, 4, 8, 4, 8 },
|
||||
{ 10, 4, 4, 3, 1, 3, 5, 10, 6, 8, 6, 8, 4, 10, 7, 2, 4, 5, 1, 8 },
|
||||
{ 2, 1, 4, 2, 3, 9, 3, 4, 7, 3, 4, 1, 3, 2, 9, 8, 6, 5, 7, 8 },
|
||||
{ 3, 4, 4, 1, 4, 10, 1, 2, 6, 4, 5, 10, 2, 2, 3, 9, 10, 9, 9, 10 },
|
||||
{ 1, 10, 1, 8, 1, 3, 1, 7, 1, 1, 2, 1, 2, 6, 3, 3, 4, 4, 8, 6 },
|
||||
{ 1, 8, 7, 10, 10, 3, 4, 6, 1, 6, 6, 4, 9, 6, 9, 6, 4, 5, 4, 7 },
|
||||
{ 8, 10, 3, 9, 4, 9, 3, 3, 4, 6, 4, 2, 6, 7, 7, 4, 4, 3, 4, 7 },
|
||||
{ 1, 3, 8, 2, 6, 9, 2, 7, 4, 8, 10, 8, 10, 5, 1, 3, 10, 10, 2, 9 },
|
||||
{ 2, 4, 1, 9, 2, 9, 7, 8, 2, 1, 4, 10, 5, 2, 7, 6, 5, 7, 2, 6 },
|
||||
{ 4, 5, 1, 4, 2, 3, 3, 4, 1, 8, 8, 2, 6, 9, 5, 9, 6, 3, 9, 3 },
|
||||
{ 3, 1, 1, 8, 6, 8, 8, 7, 9, 3, 2, 1, 8, 2, 4, 7, 3, 1, 2, 4 },
|
||||
{ 5, 9, 8, 6, 10, 4, 10, 3, 4, 10, 10, 10, 1, 7, 8, 8, 7, 7, 8, 8 },
|
||||
{ 1, 4, 6, 1, 6, 1, 2, 10, 5, 10, 2, 6, 2, 4, 5, 5, 3, 5, 1, 5 },
|
||||
{ 5, 6, 9, 10, 6, 6, 10, 6, 4, 1, 5, 3, 9, 5, 2, 10, 9, 9, 5, 1 },
|
||||
{ 10, 9, 4, 6, 9, 5, 3, 7, 10, 1, 6, 8, 1, 1, 10, 9, 5, 7, 7, 5 },
|
||||
{ 2, 6, 6, 6, 6, 2, 9, 4, 7, 5, 3, 2, 10, 3, 4, 5, 10, 9, 1, 7 },
|
||||
{ 5, 2, 4, 9, 8, 4, 8, 2, 4, 1, 3, 7, 6, 8, 1, 6, 8, 8, 10, 10 },
|
||||
{ 9, 6, 3, 1, 8, 5, 7, 8, 7, 2, 1, 8, 2, 8, 3, 7, 4, 8, 7, 7 },
|
||||
{ 8, 4, 4, 9, 7, 10, 6, 2, 1, 5, 8, 5, 1, 1, 1, 9, 1, 3, 5, 3 }
|
||||
};
|
||||
|
||||
RunTest(c, 22);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void MunkresRectangularTest1()
|
||||
{
|
||||
var c = new double[,]
|
||||
{
|
||||
{ 400, 150, 400, 1 },
|
||||
{ 400, 450, 600, 2 },
|
||||
{ 300, 225, 300, 3 }
|
||||
};
|
||||
|
||||
RunTest(c, 452);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void MunkresRectangularTest2()
|
||||
{
|
||||
var c = new double[,]
|
||||
{
|
||||
{ 10, 10, 8, 11 },
|
||||
{ 9, 8, 1, 1 },
|
||||
{ 9, 7, 4, 10 }
|
||||
};
|
||||
|
||||
RunTest(c, 15);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void MunkresRectangularTest3()
|
||||
{
|
||||
var c = new double[,]
|
||||
{
|
||||
{ 34, 26, 17, 12 },
|
||||
{ 43, 43, 36, 10 },
|
||||
{ 97, 47, 66, 34 },
|
||||
{ 52, 42, 19, 36 },
|
||||
{ 15, 93, 55, 80 }
|
||||
};
|
||||
|
||||
RunTest(c, 70);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,108 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FizzWare.NBuilder;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Books;
|
||||
using NzbDrone.Core.Exceptions;
|
||||
using NzbDrone.Core.History;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.MetadataSource;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Test.Common;
|
||||
|
||||
namespace NzbDrone.Core.Test.MusicTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class RefreshAlbumServiceFixture : CoreTest<RefreshBookService>
|
||||
{
|
||||
private Author _artist;
|
||||
private List<Book> _albums;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
var album1 = Builder<Book>.CreateNew()
|
||||
.With(x => x.AuthorMetadata = Builder<AuthorMetadata>.CreateNew().Build())
|
||||
.With(s => s.Id = 1234)
|
||||
.With(s => s.ForeignBookId = "1")
|
||||
.Build();
|
||||
|
||||
_albums = new List<Book> { album1 };
|
||||
|
||||
_artist = Builder<Author>.CreateNew()
|
||||
.With(s => s.Books = _albums)
|
||||
.Build();
|
||||
|
||||
Mocker.GetMock<IAuthorService>()
|
||||
.Setup(s => s.GetAuthor(_artist.Id))
|
||||
.Returns(_artist);
|
||||
|
||||
Mocker.GetMock<IAuthorMetadataService>()
|
||||
.Setup(s => s.UpsertMany(It.IsAny<List<AuthorMetadata>>()))
|
||||
.Returns(true);
|
||||
|
||||
Mocker.GetMock<IProvideBookInfo>()
|
||||
.Setup(s => s.GetBookInfo(It.IsAny<string>()))
|
||||
.Callback(() => { throw new BookNotFoundException(album1.ForeignBookId); });
|
||||
|
||||
Mocker.GetMock<ICheckIfBookShouldBeRefreshed>()
|
||||
.Setup(s => s.ShouldRefresh(It.IsAny<Book>()))
|
||||
.Returns(true);
|
||||
|
||||
Mocker.GetMock<IMediaFileService>()
|
||||
.Setup(x => x.GetFilesByBook(It.IsAny<int>()))
|
||||
.Returns(new List<BookFile>());
|
||||
|
||||
Mocker.GetMock<IHistoryService>()
|
||||
.Setup(x => x.GetByBook(It.IsAny<int>(), It.IsAny<HistoryEventType?>()))
|
||||
.Returns(new List<History.History>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_update_if_musicbrainz_id_changed_and_no_clash()
|
||||
{
|
||||
var newAlbumInfo = _albums.First().JsonClone();
|
||||
newAlbumInfo.AuthorMetadata = _albums.First().AuthorMetadata.Value.JsonClone();
|
||||
newAlbumInfo.ForeignBookId = _albums.First().ForeignBookId + 1;
|
||||
|
||||
Subject.RefreshBookInfo(_albums, new List<Book> { newAlbumInfo }, null, false, false, null);
|
||||
|
||||
Mocker.GetMock<IBookService>()
|
||||
.Verify(v => v.UpdateMany(It.Is<List<Book>>(s => s.First().ForeignBookId == newAlbumInfo.ForeignBookId)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_merge_if_musicbrainz_id_changed_and_new_already_exists()
|
||||
{
|
||||
var existing = _albums.First();
|
||||
|
||||
var clash = existing.JsonClone();
|
||||
clash.Id = 100;
|
||||
clash.AuthorMetadata = existing.AuthorMetadata.Value.JsonClone();
|
||||
clash.ForeignBookId += 1;
|
||||
|
||||
Mocker.GetMock<IBookService>()
|
||||
.Setup(x => x.FindById(clash.ForeignBookId))
|
||||
.Returns(clash);
|
||||
|
||||
var newAlbumInfo = existing.JsonClone();
|
||||
newAlbumInfo.AuthorMetadata = existing.AuthorMetadata.Value.JsonClone();
|
||||
newAlbumInfo.ForeignBookId = _albums.First().ForeignBookId + 1;
|
||||
|
||||
Subject.RefreshBookInfo(_albums, new List<Book> { newAlbumInfo }, null, false, false, null);
|
||||
|
||||
// check old album is deleted
|
||||
Mocker.GetMock<IBookService>()
|
||||
.Verify(v => v.DeleteMany(It.Is<List<Book>>(x => x.First().ForeignBookId == existing.ForeignBookId)));
|
||||
|
||||
// check that clash gets updated
|
||||
Mocker.GetMock<IBookService>()
|
||||
.Verify(v => v.UpdateMany(It.Is<List<Book>>(s => s.First().ForeignBookId == newAlbumInfo.ForeignBookId)));
|
||||
|
||||
ExceptionVerification.ExpectedWarns(1);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,91 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Equ;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
|
||||
namespace NzbDrone.Core.Books
|
||||
{
|
||||
public class Edition : Entity<Edition>
|
||||
{
|
||||
public Edition()
|
||||
{
|
||||
Overview = string.Empty;
|
||||
Images = new List<MediaCover.MediaCover>();
|
||||
Links = new List<Links>();
|
||||
Ratings = new Ratings();
|
||||
}
|
||||
|
||||
// These correspond to columns in the Albums table
|
||||
// These are metadata entries
|
||||
public int BookId { get; set; }
|
||||
public string ForeignEditionId { get; set; }
|
||||
public string TitleSlug { get; set; }
|
||||
public string Isbn13 { get; set; }
|
||||
public string Asin { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string Language { get; set; }
|
||||
public string Overview { get; set; }
|
||||
public string Format { get; set; }
|
||||
public bool IsEbook { get; set; }
|
||||
public string Disambiguation { get; set; }
|
||||
public string Publisher { get; set; }
|
||||
public int PageCount { get; set; }
|
||||
public DateTime? ReleaseDate { get; set; }
|
||||
public List<MediaCover.MediaCover> Images { get; set; }
|
||||
public List<Links> Links { get; set; }
|
||||
public Ratings Ratings { get; set; }
|
||||
|
||||
// These are Readarr generated/config
|
||||
public bool Monitored { get; set; }
|
||||
public bool ManualAdd { get; set; }
|
||||
|
||||
// These are dynamically queried from other tables
|
||||
[MemberwiseEqualityIgnore]
|
||||
public LazyLoaded<Book> Book { get; set; }
|
||||
[MemberwiseEqualityIgnore]
|
||||
public LazyLoaded<List<BookFile>> BookFiles { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("[{0}][{1}]", ForeignEditionId, Title.NullSafe());
|
||||
}
|
||||
|
||||
public override void UseMetadataFrom(Edition other)
|
||||
{
|
||||
ForeignEditionId = other.ForeignEditionId;
|
||||
TitleSlug = other.TitleSlug;
|
||||
Isbn13 = other.Isbn13;
|
||||
Asin = other.Asin;
|
||||
Title = other.Title;
|
||||
Language = other.Language;
|
||||
Overview = other.Overview.IsNullOrWhiteSpace() ? Overview : other.Overview;
|
||||
Format = other.Format;
|
||||
IsEbook = other.IsEbook;
|
||||
Disambiguation = other.Disambiguation;
|
||||
Publisher = other.Publisher;
|
||||
PageCount = other.PageCount;
|
||||
ReleaseDate = other.ReleaseDate;
|
||||
Images = other.Images.Any() ? other.Images : Images;
|
||||
Links = other.Links;
|
||||
Ratings = other.Ratings;
|
||||
}
|
||||
|
||||
public override void UseDbFieldsFrom(Edition other)
|
||||
{
|
||||
Id = other.Id;
|
||||
BookId = other.BookId;
|
||||
Book = other.Book;
|
||||
Monitored = other.Monitored;
|
||||
ManualAdd = other.ManualAdd;
|
||||
}
|
||||
|
||||
public override void ApplyChanges(Edition other)
|
||||
{
|
||||
ForeignEditionId = other.ForeignEditionId;
|
||||
Monitored = other.Monitored;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Common.EnsureThat;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
namespace NzbDrone.Core.Books
|
||||
{
|
||||
public interface IEditionRepository : IBasicRepository<Edition>
|
||||
{
|
||||
Edition FindByForeignEditionId(string foreignEditionId);
|
||||
List<Edition> FindByBook(int id);
|
||||
List<Edition> FindByAuthor(int id);
|
||||
List<Edition> GetEditionsForRefresh(int albumId, IEnumerable<string> foreignEditionIds);
|
||||
List<Edition> SetMonitored(Edition edition);
|
||||
}
|
||||
|
||||
public class EditionRepository : BasicRepository<Edition>, IEditionRepository
|
||||
{
|
||||
public EditionRepository(IMainDatabase database, IEventAggregator eventAggregator)
|
||||
: base(database, eventAggregator)
|
||||
{
|
||||
}
|
||||
|
||||
public Edition FindByForeignEditionId(string foreignEditionId)
|
||||
{
|
||||
var edition = Query(x => x.ForeignEditionId == foreignEditionId).SingleOrDefault();
|
||||
|
||||
return edition;
|
||||
}
|
||||
|
||||
public List<Edition> GetEditionsForRefresh(int albumId, IEnumerable<string> foreignEditionIds)
|
||||
{
|
||||
return Query(r => r.BookId == albumId || foreignEditionIds.Contains(r.ForeignEditionId));
|
||||
}
|
||||
|
||||
public List<Edition> FindByBook(int id)
|
||||
{
|
||||
// populate the albums and artist metadata also
|
||||
// this hopefully speeds up the track matching a lot
|
||||
var builder = new SqlBuilder()
|
||||
.LeftJoin<Edition, Book>((e, b) => e.BookId == b.Id)
|
||||
.LeftJoin<Book, AuthorMetadata>((b, a) => b.AuthorMetadataId == a.Id)
|
||||
.Where<Edition>(r => r.BookId == id);
|
||||
|
||||
return _database.QueryJoined<Edition, Book, AuthorMetadata>(builder, (edition, book, metadata) =>
|
||||
{
|
||||
if (book != null)
|
||||
{
|
||||
book.AuthorMetadata = metadata;
|
||||
edition.Book = book;
|
||||
}
|
||||
|
||||
return edition;
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public List<Edition> FindByAuthor(int id)
|
||||
{
|
||||
return Query(Builder().Join<Edition, Book>((e, b) => e.BookId == b.Id)
|
||||
.Join<Book, Author>((b, a) => b.AuthorMetadataId == a.AuthorMetadataId)
|
||||
.Where<Author>(a => a.Id == id));
|
||||
}
|
||||
|
||||
public List<Edition> SetMonitored(Edition edition)
|
||||
{
|
||||
var allEditions = FindByBook(edition.BookId);
|
||||
allEditions.ForEach(r => r.Monitored = r.Id == edition.Id);
|
||||
Ensure.That(allEditions.Count(x => x.Monitored) == 1).IsTrue();
|
||||
UpdateMany(allEditions);
|
||||
return allEditions;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,95 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.Books.Events;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
namespace NzbDrone.Core.Books
|
||||
{
|
||||
public interface IEditionService
|
||||
{
|
||||
Edition GetEdition(int id);
|
||||
Edition GetEditionByForeignEditionId(string foreignEditionId);
|
||||
List<Edition> GetAllEditions();
|
||||
void InsertMany(List<Edition> editions);
|
||||
void UpdateMany(List<Edition> editions);
|
||||
void DeleteMany(List<Edition> editions);
|
||||
List<Edition> GetEditionsForRefresh(int albumId, IEnumerable<string> foreignEditionIds);
|
||||
List<Edition> GetEditionsByBook(int bookId);
|
||||
List<Edition> GetEditionsByAuthor(int authorId);
|
||||
List<Edition> SetMonitored(Edition edition);
|
||||
}
|
||||
|
||||
public class EditionService : IEditionService,
|
||||
IHandle<BookDeletedEvent>
|
||||
{
|
||||
private readonly IEditionRepository _editionRepository;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
|
||||
public EditionService(IEditionRepository editionRepository,
|
||||
IEventAggregator eventAggregator)
|
||||
{
|
||||
_editionRepository = editionRepository;
|
||||
_eventAggregator = eventAggregator;
|
||||
}
|
||||
|
||||
public Edition GetEdition(int id)
|
||||
{
|
||||
return _editionRepository.Get(id);
|
||||
}
|
||||
|
||||
public Edition GetEditionByForeignEditionId(string foreignEditionId)
|
||||
{
|
||||
return _editionRepository.FindByForeignEditionId(foreignEditionId);
|
||||
}
|
||||
|
||||
public List<Edition> GetAllEditions()
|
||||
{
|
||||
return _editionRepository.All().ToList();
|
||||
}
|
||||
|
||||
public void InsertMany(List<Edition> editions)
|
||||
{
|
||||
_editionRepository.InsertMany(editions);
|
||||
}
|
||||
|
||||
public void UpdateMany(List<Edition> editions)
|
||||
{
|
||||
_editionRepository.UpdateMany(editions);
|
||||
}
|
||||
|
||||
public void DeleteMany(List<Edition> editions)
|
||||
{
|
||||
_editionRepository.DeleteMany(editions);
|
||||
foreach (var edition in editions)
|
||||
{
|
||||
_eventAggregator.PublishEvent(new EditionDeletedEvent(edition));
|
||||
}
|
||||
}
|
||||
|
||||
public List<Edition> GetEditionsForRefresh(int albumId, IEnumerable<string> foreignEditionIds)
|
||||
{
|
||||
return _editionRepository.GetEditionsForRefresh(albumId, foreignEditionIds);
|
||||
}
|
||||
|
||||
public List<Edition> GetEditionsByBook(int bookId)
|
||||
{
|
||||
return _editionRepository.FindByBook(bookId);
|
||||
}
|
||||
|
||||
public List<Edition> GetEditionsByAuthor(int authorId)
|
||||
{
|
||||
return _editionRepository.FindByAuthor(authorId);
|
||||
}
|
||||
|
||||
public List<Edition> SetMonitored(Edition edition)
|
||||
{
|
||||
return _editionRepository.SetMonitored(edition);
|
||||
}
|
||||
|
||||
public void Handle(BookDeletedEvent message)
|
||||
{
|
||||
var editions = GetEditionsByBook(message.Book.Id);
|
||||
DeleteMany(editions);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
|
||||
namespace NzbDrone.Core.Books
|
||||
{
|
||||
public interface IRefreshEditionService
|
||||
{
|
||||
bool RefreshEditionInfo(List<Edition> add, List<Edition> update, List<Tuple<Edition, Edition>> merge, List<Edition> delete, List<Edition> upToDate, List<Edition> remoteEditions, bool forceUpdateFileTags);
|
||||
}
|
||||
|
||||
public class RefreshEditionService : IRefreshEditionService
|
||||
{
|
||||
private readonly IEditionService _editionService;
|
||||
private readonly IAudioTagService _audioTagService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public RefreshEditionService(IEditionService editionService,
|
||||
IAudioTagService audioTagService,
|
||||
Logger logger)
|
||||
{
|
||||
_editionService = editionService;
|
||||
_audioTagService = audioTagService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public bool RefreshEditionInfo(List<Edition> add, List<Edition> update, List<Tuple<Edition, Edition>> merge, List<Edition> delete, List<Edition> upToDate, List<Edition> remoteEditions, bool forceUpdateFileTags)
|
||||
{
|
||||
var updateList = new List<Edition>();
|
||||
|
||||
// for editions that need updating, just grab the remote edition and set db ids
|
||||
foreach (var edition in update)
|
||||
{
|
||||
var remoteEdition = remoteEditions.Single(e => e.ForeignEditionId == edition.ForeignEditionId);
|
||||
edition.UseMetadataFrom(remoteEdition);
|
||||
|
||||
// make sure title is not null
|
||||
edition.Title = edition.Title ?? "Unknown";
|
||||
updateList.Add(edition);
|
||||
}
|
||||
|
||||
_editionService.DeleteMany(delete.Concat(merge.Select(x => x.Item1)).ToList());
|
||||
_editionService.UpdateMany(updateList);
|
||||
|
||||
var tagsToUpdate = updateList;
|
||||
if (forceUpdateFileTags)
|
||||
{
|
||||
_logger.Debug("Forcing tag update due to Author/Book/Edition updates");
|
||||
tagsToUpdate = updateList.Concat(upToDate).ToList();
|
||||
}
|
||||
|
||||
_audioTagService.SyncTags(tagsToUpdate);
|
||||
|
||||
return add.Any() || delete.Any() || updateList.Any() || merge.Any();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Core.Books;
|
||||
|
||||
namespace NzbDrone.Core.MediaFiles.BookImport.Identification
|
||||
{
|
||||
public class CandidateAlbumRelease
|
||||
{
|
||||
public CandidateAlbumRelease()
|
||||
{
|
||||
}
|
||||
|
||||
public CandidateAlbumRelease(Book book)
|
||||
{
|
||||
Book = book;
|
||||
ExistingTracks = new List<BookFile>();
|
||||
}
|
||||
|
||||
public Book Book { get; set; }
|
||||
public List<BookFile> ExistingTracks { get; set; }
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue