New: Use Goodreads directly, allow multiple editions of a book (new DB required)

This commit is contained in:
ta264
2020-06-30 21:46:01 +01:00
parent d83d2548e5
commit 45d49117ca
178 changed files with 3332 additions and 1786 deletions

View File

@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import AuthorImage from './AuthorImage';
const bannerPlaceholder = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA+gAAAC5AgMAAADG9/24AAAADFBMVEUyMjI7Ozs1NTU4ODjgOsZvAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4QkRBgAc5PUQ8QAAB7tJREFUeNrtnb1rHEsMwMdjArtrUrpf3uMg2cOl+5QpXWRvjQkXlyHVK8MVYXFl3F+/GAzrNfdSuXkQnH9ie/Oqw32aFM6bkTQfd85rHrwi0qjJJql+pxlJo9FISv0XqX8mKkmSJEmSJEmSJEmSJEmSJEmS5NeVYrh7HDqJ5DeY23kQB64/u7zW91amzgXqvRqjfGYvarnfxqncuaQlf72Zxv4gSOluudOfjRy1Hzh1L+nPj+KU3jh0MWr3Sp87dDFq98An/msmg3zPW/aFR6/vRaBPglML6Mci0H0g92MYfrjvRgJ5TrDvhuFyGIZv9NdTOev93dDry1Z59mM56/2hU8WZcefFjZgVT/Z90SlEV9VKio3HeKYZLLSGII6WPP+oBv3ZwkL3iE4JG/ZRjUYb16mAripUO/c4Hl3bd/juCF19FuHeJn6nK90VBh0s3SjBvcFW/wTrvfBa118EbHY8qmMOtmgdupoKOLTvAmOH1k059LKAX+Qra/TncExH4hcBXV3Zf/+Dv5Vb43cfod/wt3PLsN5VF6HDiudt56IbB23Ql5/oZ8A7CfZWbgF61sap62XdlOZTZ2rF3c7ltNW1+f7LuDez/uc6uDfO8dwkbPXcKF8vPS9sds527pC2utH0uCb0Jmz2t8wNvN3q2jj4ntDJna94m3g4sb7HH8Gue0RH4Je8z61g4GGr79Wz1qHjbi94m/jcH1IOccsj+lt/sOFr4m0EPz+3XyNueURvSmfn+Ebx1rctMvOxU8foqOwVa+9mfdvHDH+DdYQOxAesvZslvc/wo4vQZy6eY+vdrCVrut/AyTW9CWvO3E1ra4L6i5Fxoka7MDan45vTOmx2MPGc0QH52Tb6kTPxXNGtW5/Tnl+oGP2N/dstY8eeO2M+bqM3zvVxRT+gS8Vdl5/z6CaCzfx/c41o3pP2+030UzrAHDNGv8d4FvMVAf1I4c07V/QlnNsyG9TN23ID/ZjObnO+6CZmydS+y8oG9Dfk2Gd80WcW3Rv44e5bZOLtD8EUHbRq0V0F/DAMn8fQ2UUv2UayEMxplaFvM0G7QR/ufqBcmH/gG85Z9BM8rMO5bdiUDu4ceaLvUjqWfJveQm8hpvnKFv1jOLxUTtkoBWf0nIK5Cd6wO207dAznTlmjH+K630KvuKPbOHYffJsOexykx0iWJ7pNRf+ttEXvW1U49F7ZbJ26RHSe6ehn5NRGMPBVQAfP12EQf8QZPTMxXac30M1RxtaWXLBFn3j0eRsHNIBuCycLtqfWCQZrBcZ0W+gVhXts0RtEXyit4zDOoE/N/5yNzNF3oVB0A93I6ise7bijr1XwbYiurySg7xjfpp+g375mjj5D9HYD3fr6YvkKcxU80Q8duvVt2+gjob/ljX7yFH1a80dvDfpia8GXqp3Wr1XJXevljvFt2QZ6a6tJ2Gvd2Pa8Xuu2iNB7o++r+lUJlQas93oOl04be73Ut7y1Ts4tn/0EfaxZOzcKaXIsqNiI4QtE5x7N7Z08RZ9CKpb38cWs9V26b4sDWURnr3W9foq+gpM8a3S4V+rhKUCcqrBXTufMUxU2QWUTkFtZGls3pjgnqJ4FdB1lZAd8/qMEZGRtAlJvJqONb2syzuj2CuK+1YU5reg2CzGNyqq6fpOpku8VRI7lchX+7SIy8NdQSaKn3K8bsWJOlZGBX2Ed0bUIdBXftJpzG1h2vjetWFqgHXrhczTWtx2h8llXVTytI4FHnaes0bGMqNhC1+jUXmFMx7iCaq6qy4HqxR7dFfMUtc34LQCVDPppcM13Kie5RmTGJYNUKBraDL57xEKalS0UzTgXiiqn1X3fRxR0bBf6TLEvD4aKkslTdO5F4fQUIHseo5ugfrShewbWjvsriHxT65WAByAHVCT7u0fvoKDC+rZClZyf/eSAngUTj1pfCXjshU/8smDiPTr7J374sDPDfI1Hd4cX1g874TmvRc+30U8V9+e88Ig7U6V26Ofk21rzg1ScH3Hj033bTXIZab2iGG6PdWOaQzLx7cShQ0FFfdxqlfFu2DAhxw4vf5zWV3hYZ96m47l7u0+DMAgdu1Dxbs4St+Rx6MbAwzIveLfk8Y2Y9J5HP1tij3DmjZii9lujQy9GXO/M22/5pmvUVdWiV3RkYd50zbfaI7Vb9GmDD0C4t9qLGiy+JPTVKT4A4d5gMY866N4i+p9KuUM767aavpkqLnmDDl10S8W/mSq20O3I3n8x6AXufP4tdKlxcpkZ4HN43FZ0mT3GCmicTO2ysW2uVXmFWp/yb5cdN0kHrb/ADwFN0uPW+ICOt+0SWuPjkW2tPTr+CjcSphjiGIzWodPQGwljMGj4Se/Q0bfJGH4Sj7zx6DJG3tCgo4HQoYiukDHoKB5vZdCtb9MrIUM7DyK169Zu+mEUMtQsjLJrtT7vWl2IGWXnBxjaFwFnraQBhmFs5dDqi66SNLYyGlY6XMgaVip4RG00mHh2LWwwcRhHPVsJG0cdhpDProQNIQ+j52e30kbPa19B5T64H9Wfqn0mTelB7Y04pWMFfCQf5JDTjYOTuSClu5QUSa9EyU0gf5BF7iaP2zxdq6TJjUydgxTD3aPvm5wkSZIkSZIkSZIkSZIkSZIk+Z9kv/534U2+Uyf0XwP9H83PZBlAqkdjAAAAAElFTkSuQmCC';
const bannerPlaceholder = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAPcAAAD3AQMAAAD7QlAQAAAABlBMVEUnJychISEIs8G4AAAEFklEQVRYw+2YMdPOQBDH95KQDEVSMKOj1FFQJx9BQc0nkeuUvoLS+AQ6MQqlRu8xCjpUgsjK7iXz2t1LMsbo8i/ey5vfc3e7m7tk9+DQoUOHDpGe3bu7hS8BwJ117BoAOLfOb/Hf62s4EY1VNrcPVvjNua1WuJ/b8xqoeR3sqFkllx8+AYAra9PniDg1ydr07cT7FQMy6k7ycQMKgJr5F4BrhvI9ZA3xCDU8fJggs9gBXJ35acX8lil74CPmO5w1xhwoIMVFMQcqKCfynH3soLLuEfkB4O5TBArDPZlH05ZkYMxBigyJDEyseylHFjjK4CzPyS4IE3gTgIxuAyulHzbG/as0PYsifM24X8/TA19Vxn2efjagNwFoHE2/GDAKpm86HE2AfMrmLQbqADnI2bzFQPv8y7NlM7naORU+uid+62X4xJg0V6PC1+KfvvSghWMgnh0cVIArCO694Ib+qWR4HQ257F9oRxu+L2FpzK3h7D5vPwqA5k1OPOwA4iaAOYWnZM4XPhPYT3eWDXriX4sHROjpskF7cC2eBHfUdVjeDw6/4Uk9oHqEz18DH9se8IvgCdQDBS/oLUxcPcB24mnAv+jfXvCMOdwI9jNXDxiJp9w9DCd4Afgdz96fF5GGk3xSCFBHw+gF4PAz9SQCwE7K5UGculJHGuTdKPun+IYHrafAUPfPKJdP4OhL7ErDuf9jfnXn6Gu6+Kj654EPKQIG7iu5PMLacGPO7Qf0EOMvx3LhhRh/5l+GOsahnPkw4Mw7sXzLedzxV+DvscsMZ8X51W0Olp/+5P7qIPlLPMEWP+3z5G94rXinuen/RWzAbe6g7hVvRX/DO8FdjMPB9+O3yD5fwf1fc72+/jcfN/cHRPZPJva/7q/27z9zlPyVfL9Abrgv/oW/Nvyx5vL9rbl5f78R/I3iTnP7fRH83QjVDpfCb4Kr71uxz1FzkN9nxfX32XKVHyj+BfweV/mJkM5Pdnkpsc6PfK64BynDM8lTiU1+l+LPP2iLUJj8sj5z3uaXgMPZFDY/rQDHs/rLTRxMfkwx4mX4hPLjaza/TgIfI/l1xvl5y/wT5+dSCd8rmXf8W2/qgx5S5rRYvAMlri+Ic2MKME9FCdQT/wJ8Ga1vSnzE+Z3l06REJi7qI1VfOXw0xusrCPVZ+6aP12dFqO/qN6d4fZeF+rB804X6sInXl/lrT1vBFtAu1KcuCfWpi9e33VLfJjZAS33ckvlZpH4uedu2nOcWhleiPr9peLFT32fyfGD7fMGBlf/jfCLZOd8oIrw6q4/o2jogzlc2z2fAW8w2nwvd3eqp0YXxCcdiS1HzRC8fw2ezJjvHVtn2tPbhqnOzTgNp1/kdv6pV7ig4RQOruuDBCax1+94dOHTo0KFDk34DoJynpPus3GIAAAAASUVORK5CYII=';
function AuthorBanner(props) {
return (

View File

@@ -9,7 +9,7 @@ function findImage(images, coverType) {
function getUrl(image, coverType, size) {
if (image) {
// Remove protocol
let url = image.url.replace(/^https?:/, '');
let url = image.url;
url = url.replace(`${coverType}.jpg`, `${coverType}-${size}.jpg`);

View File

@@ -6,6 +6,7 @@ import TextTruncate from 'react-text-truncate';
import formatBytes from 'Utilities/Number/formatBytes';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import stripHtml from 'Utilities/String/stripHtml';
import { align, icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
import fonts from 'Styles/Variables/fonts';
import HeartRating from 'Components/HeartRating';
@@ -166,7 +167,6 @@ class AuthorDetails extends Component {
overview,
links,
images,
authorType,
alternateTitles,
tags,
isSaving,
@@ -206,7 +206,6 @@ class AuthorDetails extends Component {
} = this.state;
const continuing = status === 'continuing';
const endedString = authorType === 'Person' ? 'Deceased' : 'Ended';
let bookFilesCountMessage = 'No book files';
@@ -458,7 +457,7 @@ class AuthorDetails extends Component {
/>
<span className={styles.qualityProfileName}>
{continuing ? 'Continuing' : endedString}
{continuing ? 'Continuing' : 'Deceased'}
</span>
</Label>
@@ -515,7 +514,7 @@ class AuthorDetails extends Component {
<div className={styles.overview}>
<TextTruncate
line={Math.floor(125 / (defaultFontSize * lineHeight))}
text={overview.replace(/<[^>]*>?/gm, '')}
text={stripHtml(overview)}
/>
</div>
</div>
@@ -697,9 +696,8 @@ AuthorDetails.propTypes = {
statistics: PropTypes.object.isRequired,
qualityProfileId: PropTypes.number.isRequired,
monitored: PropTypes.bool.isRequired,
authorType: PropTypes.string,
status: PropTypes.string.isRequired,
overview: PropTypes.string.isRequired,
overview: PropTypes.string,
links: PropTypes.arrayOf(PropTypes.object).isRequired,
images: PropTypes.arrayOf(PropTypes.object).isRequired,
alternateTitles: PropTypes.arrayOf(PropTypes.string).isRequired,

View File

@@ -226,7 +226,7 @@ AuthorDetailsSeries.propTypes = {
onSortPress: PropTypes.func.isRequired,
onMonitorBookPress: PropTypes.func.isRequired,
uiSettings: PropTypes.object.isRequired,
authorMonitored: PropTypes.object.isRequired
authorMonitored: PropTypes.bool.isRequired
};
export default AuthorDetailsSeries;

View File

@@ -73,7 +73,6 @@ class BookRow extends Component {
title,
position,
ratings,
disambiguation,
isSaving,
authorMonitored,
titleSlug,
@@ -124,7 +123,6 @@ class BookRow extends Component {
<BookTitleLink
titleSlug={titleSlug}
title={title}
disambiguation={disambiguation}
/>
</TableRowCell>
);
@@ -208,7 +206,6 @@ BookRow.propTypes = {
title: PropTypes.string.isRequired,
position: PropTypes.string,
ratings: PropTypes.object.isRequired,
disambiguation: PropTypes.string,
titleSlug: PropTypes.string.isRequired,
isSaving: PropTypes.bool,
authorMonitored: PropTypes.bool.isRequired,

View File

@@ -4,6 +4,7 @@ import TextTruncate from 'react-text-truncate';
import { icons } from 'Helpers/Props';
import dimensions from 'Styles/Variables/dimensions';
import fonts from 'Styles/Variables/fonts';
import stripHtml from 'Utilities/String/stripHtml';
import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
@@ -113,7 +114,8 @@ class AuthorIndexOverview extends Component {
const elementStyle = {
width: `${posterWidth}px`,
height: `${posterHeight}px`
height: `${posterHeight}px`,
objectFit: 'contain'
};
const contentHeight = getContentHeight(rowHeight, isSmallScreen);
@@ -203,7 +205,7 @@ class AuthorIndexOverview extends Component {
>
<TextTruncate
line={Math.floor(overviewHeight / (defaultFontSize * lineHeight))}
text={overview}
text={stripHtml(overview)}
/>
</Link>

View File

@@ -110,9 +110,9 @@ class AuthorIndexPoster extends Component {
const elementStyle = {
width: `${posterWidth}px`,
height: `${posterHeight}px`
height: `${posterHeight}px`,
objectFit: 'contain'
};
elementStyle.objectFit = 'contain';
return (
<div className={styles.container}>

View File

@@ -82,7 +82,6 @@ class AuthorIndexRow extends Component {
status,
authorName,
titleSlug,
authorType,
qualityProfile,
metadataProfile,
nextBook,
@@ -134,7 +133,6 @@ class AuthorIndexRow extends Component {
<AuthorStatusCell
key={name}
className={styles[name]}
authorType={authorType}
monitored={monitored}
status={status}
component={VirtualTableRowCell}
@@ -184,17 +182,6 @@ class AuthorIndexRow extends Component {
);
}
if (name === 'authorType') {
return (
<VirtualTableRowCell
key={name}
className={styles[name]}
>
{authorType}
</VirtualTableRowCell>
);
}
if (name === 'qualityProfileId') {
return (
<VirtualTableRowCell
@@ -421,7 +408,6 @@ AuthorIndexRow.propTypes = {
status: PropTypes.string.isRequired,
authorName: PropTypes.string.isRequired,
titleSlug: PropTypes.string.isRequired,
authorType: PropTypes.string,
qualityProfile: PropTypes.object.isRequired,
metadataProfile: PropTypes.object.isRequired,
nextBook: PropTypes.object,

View File

@@ -8,15 +8,12 @@ import styles from './AuthorStatusCell.css';
function AuthorStatusCell(props) {
const {
className,
authorType,
monitored,
status,
component: Component,
...otherProps
} = props;
const endedString = authorType === 'Person' ? 'Deceased' : 'Ended';
return (
<Component
className={className}
@@ -31,7 +28,7 @@ function AuthorStatusCell(props) {
<Icon
className={styles.statusIcon}
name={status === 'ended' ? icons.AUTHOR_ENDED : icons.AUTHOR_CONTINUING}
title={status === 'ended' ? endedString : 'Continuing'}
title={status === 'ended' ? 'Deceased' : 'Continuing'}
/>
</Component>
);
@@ -39,7 +36,6 @@ function AuthorStatusCell(props) {
AuthorStatusCell.propTypes = {
className: PropTypes.string.isRequired,
authorType: PropTypes.string,
monitored: PropTypes.bool.isRequired,
status: PropTypes.string.isRequired,
component: PropTypes.elementType

View File

@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import AuthorImage from 'Author/AuthorImage';
const coverPlaceholder = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAPcAAAD3AgMAAAC84irAAAAADFBMVEUyMjI7Ozs1NTU4ODjgOsZvAAAAAWJLR0QAiAUdSAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB+EJEBIzDdm9OfoAAAbkSURBVGje7Zq9b9s4FMBZFgUkBR27C3cw0MromL1jxwyVZASB67G4qWPgoSAyBdm9CwECKCp8nbIccGj/Ce/BTUb3Lh3aI997pCjnTnyyt0JcIif5+ZHvPZLvQ0KMYxzjGMc4xjGOcYxjHOP4JUfSfP7RVPvSH3MYX/eC5aecxne1v+w95WebFs/rwVO/8+h8PnT6t3ln/DFQuJ06/SyHiX9pxa7o5/lewkuLDxLvhM8tPki8g07dU8Gnj5zGlw7P79n4pDVYi8/YuHO4n03z0z6XXDom4G3TXDdN840+LobN/W1Ty2slHD8bNvevlUgutLmTj4NmT3pf6mMGcJGth+gefaZsDCjB2Wj65wN8ZmnAGnE6eFieI1FvcEISLjIUr9hm+w7PFeHiE9t0E7dyIatE48odXTPu0j/A3BMnXf7NXDxudTxbE2VxMWVu+sfwf3i1ZMLiaQLf+iWIP4VtjtTzFhc35vfveZrb4nPt4R95ulu1cxeVh8Psw7rzbgWp8dWHyr83WJpbgjypjS5XeZnqRxmJNUd3MS1d6ue/tOn0WuayNd2CoTlaeqwnIVeOgcWHdHdMS9cSN1vCy3bxZwzFm6VL7QA14WTudVj1sFvf4ReZNSCO0IvwngXFV3hkFcriuPokrPrYbYxjVAHiZ24zLYIeP7/E4xZUgHiZWt29D9ptGemHR7mPo9B10HLGbucRfs/Ww2f2CD4L2u0+wofKwwvrd0XoqCmr38CAZa1d58LesEpvgqtN4MCR1mVj2nZWOiweVB/CAXuyi59Y1auA2eekg6Xw8Tfm013A8LFV8mYXL61ZF4Hb8Zx8d9vBtbdG7s99XvOOZlF38QVtmlkAv0ffxTOjxU/o5p8FvKbSszw2ik87+Iz23Lwf134RiWf2tG3xN2T4oh8vDO4U33z+5qnefFnR77OA2wheh2WfbJBHeI/XgtNJEaHdtJNrvPn8E8eV/kW/2xn8FDc77LemOyq4J1XvSbds7SZ3cAV+86UXP283TGaFUk4ZwmNyugne8FaqxdHtFkH8GNewg2cc3PjsM7CbbNdMwQJ47aL3mP5H308ar5XOn2nUwpx+4hrx/z+qn5DBNqD4rMUpWACnPwnhkfa9SnZwvX1MnHLVi08cPle+0wBuAsykd8dO0KkS9L0dPCO37MVLxJc6nPHdTeNT/ZeLDQN/DEFpBzc33Bfckhx8K1q7IS5vuPgjbTf5AL97zcALxFUHN76QrF7heTHru54RN3bbxTeEn4Xx04f4NOfhSuPLncmnQk3z1yLlSE8fabtFHVyZyIQlXes8zrdSJR5ea7k3+asUooXg2mO4oDprT/XdHpROhouL/8A3edBw5DYxBhYdn08Q53jd0elDfApHbHjL6Hk/pvvNd1rEWdLl9iG+hpMgiMMdVEM64B8X5nq6ZBwX5rCSeK/4uInJROiwetLi0jtpG0yJBPOkTVQXryEPKqMQbq6JeyUTvUOkilq/EVGmo5NIpP3XRIzhXIafrjzF30JUIqecKxIjOpF6il9jbHTLxjs3rN5voPH+GxbDA1m7GrM9a4zdTigdCUUXD2MSSEAXQRxDo2QHl2iwV+h7gchqLrLrhmKxH/Z6nqLUQD5AYSHWAEwk+Z1Ck1vEAmEhBaVtufDtj8Zmv6U+PQNBqbDf/szVR5XNvQteSAzRyeQhzgnIKR2Invq43gQb4+oRaJCTTcRd6RkzGXlJQe3vDq8gsDB2S0QaSoViwKNW9Sh9zUzEMA2MWtU7nJUGYhIa4bnjcLthgkkopMAGj3dxXgoMCbg+laTFL8luSn9pFkrAMf031cmVJz0jXzsKFm6OSfVqYnEILPKZDjeicPFhQoaHbMhKX+NmZ5Q+ntr8n5obhGPVKlx48cs+FteKP3MlswWv6CSPHK4Dmntm0ckreW0snmxKbsnLFdyo4mrwjLYJo+Dmyn0k3uDTEpMRTrnPKza+IHy9wGSEU2yMvSrvHeJ/Qt2UV+p0hVacvsah0psKXqEVy7y2tPu3xhM1oMxLReY00tAlJG9JFZktzCwyU4lbuqQ7U22VN1zi9gvsIP05PjAL7H55H/C6rREzyvu41bbS4VXb1OV0FLG1YVsa1J1gtzaosVJbHO3Gb6z4bR2H89s61FRqCIcgL+E3lfyWlsaN3eR6QDP0pSdeKqOEZjOgoda285SUl5W+Jga181wz0WQFF2poM7FtZTZKXlXZ0Fam10htroY3Ug9s43pN5OJ2jyZy28Iu1nu0sNsGenGzRwO9bd8Xd/u0793LA8Vmn5cHnPhiH+Gt+HIv4Ye+tnHoSyMHvrJy6Aszh76uc+DLQuLQV5XGMY5xjGMc4xjHOMYxjnH80uNfW99BeoyzJCoAAAAASUVORK5CYII=';
const coverPlaceholder = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAPcAAAD3AQMAAAD7QlAQAAAABlBMVEUnJychISEIs8G4AAAEFklEQVRYw+2YMdPOQBDH95KQDEVSMKOj1FFQJx9BQc0nkeuUvoLS+AQ6MQqlRu8xCjpUgsjK7iXz2t1LMsbo8i/ey5vfc3e7m7tk9+DQoUOHDpGe3bu7hS8BwJ117BoAOLfOb/Hf62s4EY1VNrcPVvjNua1WuJ/b8xqoeR3sqFkllx8+AYAra9PniDg1ydr07cT7FQMy6k7ycQMKgJr5F4BrhvI9ZA3xCDU8fJggs9gBXJ35acX8lil74CPmO5w1xhwoIMVFMQcqKCfynH3soLLuEfkB4O5TBArDPZlH05ZkYMxBigyJDEyseylHFjjK4CzPyS4IE3gTgIxuAyulHzbG/as0PYsifM24X8/TA19Vxn2efjagNwFoHE2/GDAKpm86HE2AfMrmLQbqADnI2bzFQPv8y7NlM7naORU+uid+62X4xJg0V6PC1+KfvvSghWMgnh0cVIArCO694Ib+qWR4HQ257F9oRxu+L2FpzK3h7D5vPwqA5k1OPOwA4iaAOYWnZM4XPhPYT3eWDXriX4sHROjpskF7cC2eBHfUdVjeDw6/4Uk9oHqEz18DH9se8IvgCdQDBS/oLUxcPcB24mnAv+jfXvCMOdwI9jNXDxiJp9w9DCd4Afgdz96fF5GGk3xSCFBHw+gF4PAz9SQCwE7K5UGculJHGuTdKPun+IYHrafAUPfPKJdP4OhL7ErDuf9jfnXn6Gu6+Kj654EPKQIG7iu5PMLacGPO7Qf0EOMvx3LhhRh/5l+GOsahnPkw4Mw7sXzLedzxV+DvscsMZ8X51W0Olp/+5P7qIPlLPMEWP+3z5G94rXinuen/RWzAbe6g7hVvRX/DO8FdjMPB9+O3yD5fwf1fc72+/jcfN/cHRPZPJva/7q/27z9zlPyVfL9Abrgv/oW/Nvyx5vL9rbl5f78R/I3iTnP7fRH83QjVDpfCb4Kr71uxz1FzkN9nxfX32XKVHyj+BfweV/mJkM5Pdnkpsc6PfK64BynDM8lTiU1+l+LPP2iLUJj8sj5z3uaXgMPZFDY/rQDHs/rLTRxMfkwx4mX4hPLjaza/TgIfI/l1xvl5y/wT5+dSCd8rmXf8W2/qgx5S5rRYvAMlri+Ic2MKME9FCdQT/wJ8Ga1vSnzE+Z3l06REJi7qI1VfOXw0xusrCPVZ+6aP12dFqO/qN6d4fZeF+rB804X6sInXl/lrT1vBFtAu1KcuCfWpi9e33VLfJjZAS33ckvlZpH4uedu2nOcWhleiPr9peLFT32fyfGD7fMGBlf/jfCLZOd8oIrw6q4/o2jogzlc2z2fAW8w2nwvd3eqp0YXxCcdiS1HzRC8fw2ezJjvHVtn2tPbhqnOzTgNp1/kdv6pV7ig4RQOruuDBCax1+94dOHTo0KFDk34DoJynpPus3GIAAAAASUVORK5CYII=';
function BookCover(props) {
return (

View File

@@ -7,6 +7,7 @@ import TextTruncate from 'react-text-truncate';
import formatBytes from 'Utilities/Number/formatBytes';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import stripHtml from 'Utilities/String/stripHtml';
import { align, icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
import fonts from 'Styles/Variables/fonts';
import HeartRating from 'Components/HeartRating';
@@ -18,6 +19,7 @@ import Tooltip from 'Components/Tooltip/Tooltip';
import BookCover from 'Book/BookCover';
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
// import RetagPreviewModalConnector from 'Retag/RetagPreviewModalConnector';
import EditBookModalConnector from 'Book/Edit/EditBookModalConnector';
import DeleteBookModal from 'Book/Delete/DeleteBookModal';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
@@ -44,28 +46,6 @@ function getFanartUrl(images) {
}
}
function formatDuration(timeSpan) {
const duration = moment.duration(timeSpan);
const hours = duration.get('hours');
const minutes = duration.get('minutes');
let hoursText = 'Hours';
let minText = 'Minutes';
if (minutes === 1) {
minText = 'Minute';
}
if (hours === 0) {
return `${minutes} ${minText}`;
}
if (hours === 1) {
hoursText = 'Hour';
}
return `${hours} ${hoursText} ${minutes} ${minText}`;
}
function getExpandedState(newState) {
return {
allExpanded: newState.allSelected,
@@ -85,6 +65,7 @@ class BookDetails extends Component {
this.state = {
isOrganizeModalOpen: false,
isRetagModalOpen: false,
isEditBookModalOpen: false,
isDeleteBookModalOpen: false,
allExpanded: false,
allCollapsed: false,
@@ -112,8 +93,17 @@ class BookDetails extends Component {
this.setState({ isRetagModalOpen: false });
}
onEditBookPress = () => {
this.setState({ isEditBookModalOpen: true });
}
onEditBookModalClose = () => {
this.setState({ isEditBookModalOpen: false });
}
onDeleteBookPress = () => {
this.setState({
isEditBookModalOpen: false,
isDeleteBookModalOpen: true
});
}
@@ -153,8 +143,7 @@ class BookDetails extends Component {
id,
titleSlug,
title,
disambiguation,
duration,
pageCount,
overview,
statistics = {},
monitored,
@@ -179,6 +168,7 @@ class BookDetails extends Component {
const {
isOrganizeModalOpen,
// isRetagModalOpen,
isEditBookModalOpen,
isDeleteBookModalOpen,
allExpanded,
allCollapsed,
@@ -222,6 +212,12 @@ class BookDetails extends Component {
<PageToolbarSeparator />
<PageToolbarButton
label="Edit"
iconName={icons.EDIT}
onPress={this.onEditBookPress}
/>
<PageToolbarButton
label="Delete"
iconName={icons.DELETE}
@@ -272,8 +268,9 @@ class BookDetails extends Component {
</div>
<div className={styles.title}>
{title}{disambiguation ? ` (${disambiguation})` : ''}
{title}
</div>
</div>
<div className={styles.bookNavigationButtons}>
@@ -306,9 +303,9 @@ class BookDetails extends Component {
<div className={styles.details}>
<div>
{
!!duration &&
!!pageCount &&
<span className={styles.duration}>
{formatDuration(duration)}
{`${pageCount} pages`}
</span>
}
@@ -397,7 +394,7 @@ class BookDetails extends Component {
<div className={styles.overview}>
<TextTruncate
line={Math.floor(125 / (defaultFontSize * lineHeight))}
text={overview.replace(/<[^>]*>?/gm, '')}
text={stripHtml(overview)}
/>
</div>
</div>
@@ -488,6 +485,14 @@ class BookDetails extends Component {
{/* onModalClose={this.onRetagModalClose} */}
{/* /> */}
<EditBookModalConnector
isOpen={isEditBookModalOpen}
bookId={id}
authorId={author.id}
onModalClose={this.onEditBookModalClose}
onDeleteAuthorPress={this.onDeleteBookPress}
/>
<DeleteBookModal
isOpen={isDeleteBookModalOpen}
bookId={id}
@@ -505,8 +510,7 @@ BookDetails.propTypes = {
id: PropTypes.number.isRequired,
titleSlug: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
disambiguation: PropTypes.string,
duration: PropTypes.number,
pageCount: PropTypes.number,
overview: PropTypes.string,
statistics: PropTypes.object.isRequired,
releaseDate: PropTypes.string.isRequired,

View File

@@ -98,6 +98,10 @@ const mapDispatchToProps = {
toggleBooksMonitored
};
function getMonitoredEditions(props) {
return _.map(_.filter(props.editions, { monitored: true }), 'id').sort();
}
class BookDetailsConnector extends Component {
componentDidMount() {
@@ -106,10 +110,8 @@ class BookDetailsConnector extends Component {
}
componentDidUpdate(prevProps) {
// If the id has changed we need to clear the books
// files and fetch from the server.
if (prevProps.id !== this.props.id) {
if (!_.isEqual(getMonitoredEditions(prevProps), getMonitoredEditions(this.props)) ||
(prevProps.anyReleaseOk === false && this.props.anyReleaseOk === true)) {
this.unpopulate();
this.populate();
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -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;

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -15,7 +15,7 @@ import PasswordInput from './PasswordInput';
import PathInputConnector from './PathInputConnector';
import QualityProfileSelectInputConnector from './QualityProfileSelectInputConnector';
import MetadataProfileSelectInputConnector from './MetadataProfileSelectInputConnector';
import BookReleaseSelectInputConnector from './BookReleaseSelectInputConnector';
import BookEditionSelectInputConnector from './BookEditionSelectInputConnector';
import RootFolderSelectInputConnector from './RootFolderSelectInputConnector';
import SeriesTypeSelectInput from './SeriesTypeSelectInput';
import EnhancedSelectInput from './EnhancedSelectInput';
@@ -66,8 +66,8 @@ function getComponent(type) {
case inputTypes.METADATA_PROFILE_SELECT:
return MetadataProfileSelectInputConnector;
case inputTypes.BOOK_RELEASE_SELECT:
return BookReleaseSelectInputConnector;
case inputTypes.BOOK_EDITION_SELECT:
return BookEditionSelectInputConnector;
case inputTypes.ROOT_FOLDER_SELECT:
return RootFolderSelectInputConnector;

View File

@@ -11,7 +11,7 @@ export const PASSWORD = 'password';
export const PATH = 'path';
export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect';
export const METADATA_PROFILE_SELECT = 'metadataProfileSelect';
export const BOOK_RELEASE_SELECT = 'bookReleaseSelect';
export const BOOK_EDITION_SELECT = 'bookEditionSelect';
export const ROOT_FOLDER_SELECT = 'rootFolderSelect';
export const SELECT = 'select';
export const SERIES_TYPE_SELECT = 'authorTypeSelect';
@@ -33,7 +33,7 @@ export const all = [
PATH,
QUALITY_PROFILE_SELECT,
METADATA_PROFILE_SELECT,
BOOK_RELEASE_SELECT,
BOOK_EDITION_SELECT,
ROOT_FOLDER_SELECT,
SELECT,
SERIES_TYPE_SELECT,

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -0,0 +1,3 @@
.albumRow {
cursor: pointer;
}

View File

@@ -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;

View File

@@ -23,6 +23,7 @@ import TableBody from 'Components/Table/TableBody';
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
import SelectAuthorModal from 'InteractiveImport/Author/SelectAuthorModal';
import SelectBookModal from 'InteractiveImport/Book/SelectBookModal';
import SelectEditionModal from 'InteractiveImport/Edition/SelectEditionModal';
import ConfirmImportModal from 'InteractiveImport/Confirmation/ConfirmImportModal';
import InteractiveImportRow from './InteractiveImportRow';
import styles from './InteractiveImportModalContent.css';
@@ -79,6 +80,7 @@ const importModeOptions = [
const SELECT = 'select';
const AUTHOR = 'author';
const BOOK = 'book';
const EDITION = 'edition';
const QUALITY = 'quality';
const replaceExistingFilesOptions = {
@@ -112,7 +114,7 @@ class InteractiveImportModalContent extends Component {
const selectedItems = _.filter(this.props.items, (x) => _.includes(selectedIds, x.id));
const inconsistent = _(selectedItems)
.map((x) => ({ bookId: x.book ? x.book.id : 0, releaseId: x.bookReleaseId }))
.map((x) => ({ bookId: x.book ? x.book.id : 0, releaseId: x.EditionId }))
.groupBy('bookId')
.mapValues((book) => _(book).groupBy((x) => x.releaseId).values().value().length)
.values()
@@ -273,6 +275,7 @@ class InteractiveImportModalContent extends Component {
const bulkSelectOptions = [
{ key: SELECT, value: 'Select...', disabled: true },
{ key: BOOK, value: 'Select Book' },
{ key: EDITION, value: 'Select Edition' },
{ key: QUALITY, value: 'Select Quality' }
];
@@ -469,6 +472,13 @@ class InteractiveImportModalContent extends Component {
onModalClose={this.onSelectModalClose}
/>
<SelectEditionModal
isOpen={selectModalOpen === EDITION}
importIdsByBook={_.chain(items).filter((x) => x.album).groupBy((x) => x.book.id).mapValues((x) => x.map((y) => y.id)).value()}
books={_.chain(items).filter((x) => x.book).keyBy((x) => x.book.id).mapValues((x) => ({ matchedEditionId: x.editionId, book: x.book })).values().value()}
onModalClose={this.onSelectModalClose}
/>
<SelectQualityModal
isOpen={selectModalOpen === QUALITY}
ids={selectedIds}

View File

@@ -128,6 +128,7 @@ class InteractiveImportModalContentConnector extends Component {
const {
author,
book,
editionId,
quality,
disableReleaseSwitching
} = item;
@@ -151,6 +152,7 @@ class InteractiveImportModalContentConnector extends Component {
path: item.path,
authorId: author.id,
bookId: book.id,
editionId,
quality,
downloadId: this.props.downloadId,
disableReleaseSwitching

View File

@@ -141,10 +141,11 @@ class AddNewItem extends Component {
);
} else if (item.book) {
const book = item.book;
const edition = book.editions[0];
return (
<AddNewBookSearchResultConnector
key={item.id}
isExistingBook={'id' in book && book.id !== 0}
isExistingBook={'id' in edition && edition.id !== 0}
isExistingAuthor={'id' in book.author && book.author.id !== 0}
{...book}
/>

View File

@@ -3,6 +3,7 @@ import React, { Component } from 'react';
import TextTruncate from 'react-text-truncate';
import dimensions from 'Styles/Variables/dimensions';
import fonts from 'Styles/Variables/fonts';
import stripHtml from 'Utilities/String/stripHtml';
import { icons, kinds, sizes } from 'Helpers/Props';
import HeartRating from 'Components/HeartRating';
import Icon from 'Components/Icon';
@@ -69,12 +70,10 @@ class AddNewAuthorSearchResult extends Component {
render() {
const {
foreignAuthorId,
goodreadsId,
titleSlug,
authorName,
year,
disambiguation,
authorType,
status,
overview,
ratings,
@@ -89,7 +88,7 @@ class AddNewAuthorSearchResult extends Component {
const linkProps = isExistingAuthor ? { to: `/author/${titleSlug}` } : { onPress: this.onPress };
const endedString = authorType === 'Person' ? 'Deceased' : 'Ended';
const endedString = 'Deceased';
const height = calculateHeight(230, isSmallScreen);
@@ -143,7 +142,7 @@ class AddNewAuthorSearchResult extends Component {
<Link
className={styles.mbLink}
to={`https://goodreads.com/author/show/${goodreadsId}`}
to={`https://goodreads.com/author/show/${foreignAuthorId}`}
onPress={this.onMBLinkPress}
>
<Icon
@@ -155,17 +154,13 @@ class AddNewAuthorSearchResult extends Component {
</div>
<div>
{
ratings.votes > 0 ?
<Label size={sizes.LARGE}>
<HeartRating
rating={ratings.value}
iconSize={13}
/>
</Label>
{
authorType ?
<Label size={sizes.LARGE}>
{authorType}
</Label> :
null
}
@@ -191,7 +186,7 @@ class AddNewAuthorSearchResult extends Component {
<TextTruncate
truncateText="…"
line={Math.floor(height / (defaultFontSize * lineHeight))}
text={overview}
text={stripHtml(overview)}
/>
</div>
</div>
@@ -214,12 +209,10 @@ class AddNewAuthorSearchResult extends Component {
AddNewAuthorSearchResult.propTypes = {
foreignAuthorId: PropTypes.string.isRequired,
goodreadsId: PropTypes.number.isRequired,
titleSlug: PropTypes.string.isRequired,
authorName: PropTypes.string.isRequired,
year: PropTypes.number,
disambiguation: PropTypes.string,
authorType: PropTypes.string,
status: PropTypes.string.isRequired,
overview: PropTypes.string,
ratings: PropTypes.object.isRequired,

View File

@@ -1,6 +1,7 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import TextTruncate from 'react-text-truncate';
import stripHtml from 'Utilities/String/stripHtml';
import { kinds } from 'Helpers/Props';
import SpinnerButton from 'Components/Link/SpinnerButton';
import CheckInput from 'Components/Form/CheckInput';
@@ -93,7 +94,7 @@ class AddNewBookModalContent extends Component {
<TextTruncate
truncateText="…"
line={8}
text={overview}
text={stripHtml(overview)}
/>
</div> :
null

View File

@@ -4,6 +4,7 @@ import React, { Component } from 'react';
import TextTruncate from 'react-text-truncate';
import dimensions from 'Styles/Variables/dimensions';
import fonts from 'Styles/Variables/fonts';
import stripHtml from 'Utilities/String/stripHtml';
import { icons, sizes } from 'Helpers/Props';
import HeartRating from 'Components/HeartRating';
import Icon from 'Components/Icon';
@@ -70,7 +71,6 @@ class AddNewBookSearchResult extends Component {
render() {
const {
foreignBookId,
goodreadsId,
titleSlug,
title,
releaseDate,
@@ -79,6 +79,7 @@ class AddNewBookSearchResult extends Component {
ratings,
images,
author,
editions,
isExistingBook,
isExistingAuthor,
isSmallScreen
@@ -132,7 +133,7 @@ class AddNewBookSearchResult extends Component {
<Link
className={styles.mbLink}
to={`https://goodreads.com/book/show/${goodreadsId}`}
to={`https://goodreads.com/book/show/${editions[0].foreignEditionId}`}
onPress={this.onMBLinkPress}
>
<Icon
@@ -185,7 +186,7 @@ class AddNewBookSearchResult extends Component {
<TextTruncate
truncateText="…"
line={Math.floor(height / (defaultFontSize * lineHeight))}
text={overview}
text={stripHtml(overview)}
/>
</div>
</div>
@@ -209,7 +210,6 @@ class AddNewBookSearchResult extends Component {
AddNewBookSearchResult.propTypes = {
foreignBookId: PropTypes.string.isRequired,
goodreadsId: PropTypes.number.isRequired,
titleSlug: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
releaseDate: PropTypes.string,
@@ -217,6 +217,7 @@ AddNewBookSearchResult.propTypes = {
overview: PropTypes.string,
ratings: PropTypes.object.isRequired,
author: PropTypes.object,
editions: PropTypes.arrayOf(PropTypes.object).isRequired,
images: PropTypes.arrayOf(PropTypes.object).isRequired,
isExistingBook: PropTypes.bool.isRequired,
isExistingAuthor: PropTypes.bool.isRequired,

View File

@@ -32,8 +32,7 @@ function EditMetadataProfileModalContent(props) {
const {
id,
name,
minRating,
minRatingCount,
minPopularity,
skipMissingDate,
skipMissingIsbn,
skipPartsAndSets,
@@ -73,27 +72,15 @@ function EditMetadataProfileModalContent(props) {
</FormGroup>
<FormGroup>
<FormLabel>Minimum Rating</FormLabel>
<FormLabel>Minimum Popularity</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="minRating"
{...minRating}
name="minPopularity"
{...minPopularity}
helpText="Popularity is average rating * number of votes"
isFloat={true}
min={0}
max={5}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>Minimum Number of Ratings</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="minRatingCount"
{...minRatingCount}
min={0}
onChange={onInputChange}
/>
</FormGroup>

View File

@@ -73,12 +73,6 @@ export const defaultState = {
isVisible: true,
isModifiable: false
},
{
name: 'authorType',
label: 'Type',
isSortable: true,
isVisible: true
},
{
name: 'qualityProfileId',
label: 'Quality Profile',

View File

@@ -158,7 +158,7 @@ export const actionHandlers = handleThunks({
}).request;
promise.done((data) => {
data.releases = itemToAdd.book.releases;
data.editions = itemToAdd.book.editions;
itemToAdd.book = data;
dispatch(batchActions([
updateItem({ section: 'authors', ...data.author }),

View File

@@ -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;

View File

@@ -186,7 +186,8 @@ namespace NzbDrone.Common.Http.Dispatchers
webRequest.TransferEncoding = header.Value;
break;
case "User-Agent":
throw new NotSupportedException("User-Agent other than Readarr not allowed.");
webRequest.UserAgent = header.Value;
break;
case "Proxy-Connection":
throw new NotImplementedException();
default:

View File

@@ -16,6 +16,7 @@ namespace NzbDrone.Core.Test.ArtistStatsTests
{
private Author _artist;
private Book _album;
private Edition _edition;
private BookFile _trackFile;
[SetUp]
@@ -32,10 +33,16 @@ namespace NzbDrone.Core.Test.ArtistStatsTests
.BuildNew();
Db.Insert(_album);
_edition = Builder<Edition>.CreateNew()
.With(e => e.BookId = _album.Id)
.With(e => e.Monitored = true)
.BuildNew();
Db.Insert(_edition);
_trackFile = Builder<BookFile>.CreateNew()
.With(e => e.Author = _artist)
.With(e => e.Book = _album)
.With(e => e.BookId == _album.Id)
.With(e => e.Edition = _edition)
.With(e => e.EditionId == _edition.Id)
.With(e => e.Quality = new QualityModel(Quality.MP3_320))
.BuildNew();
}

View File

@@ -51,10 +51,23 @@ namespace NzbDrone.Core.Test.Datastore
Db.InsertMany(albums);
var editions = new List<Edition>();
foreach (var album in albums)
{
editions.Add(
Builder<Edition>.CreateNew()
.With(v => v.Id = 0)
.With(v => v.BookId = album.Id)
.With(v => v.ForeignEditionId = "test" + album.Id)
.Build());
}
Db.InsertMany(editions);
var trackFiles = Builder<BookFile>.CreateListOfSize(1)
.All()
.With(v => v.Id = 0)
.With(v => v.BookId = albums[0].Id)
.With(v => v.EditionId = editions[0].Id)
.With(v => v.Quality = new QualityModel())
.BuildListOfNew();
@@ -97,40 +110,15 @@ namespace NzbDrone.Core.Test.Datastore
var db = Mocker.Resolve<IDatabase>();
var files = MediaFileRepository.Query(db,
new SqlBuilder()
.Join<BookFile, Book>((t, a) => t.BookId == a.Id)
.Join<BookFile, Edition>((t, a) => t.EditionId == a.Id)
.Join<Edition, Book>((e, b) => e.BookId == b.Id)
.Join<Book, Author>((album, artist) => album.AuthorMetadataId == artist.AuthorMetadataId)
.Join<Author, AuthorMetadata>((a, m) => a.AuthorMetadataId == m.Id));
Assert.IsNotEmpty(files);
foreach (var file in files)
{
Assert.IsTrue(file.Book.IsLoaded);
Assert.IsTrue(file.Author.IsLoaded);
Assert.IsTrue(file.Author.Value.Metadata.IsLoaded);
}
}
[Test]
public void should_lazy_load_tracks_if_not_joined_to_trackfile()
{
var db = Mocker.Resolve<IDatabase>();
var files = db.QueryJoined<BookFile, Book, Author, AuthorMetadata>(
new SqlBuilder()
.Join<BookFile, Book>((t, a) => t.BookId == a.Id)
.Join<Book, Author>((album, artist) => album.AuthorMetadataId == artist.AuthorMetadataId)
.Join<Author, AuthorMetadata>((a, m) => a.AuthorMetadataId == m.Id),
(file, album, artist, metadata) =>
{
file.Book = album;
file.Author = artist;
file.Author.Value.Metadata = metadata;
return file;
});
Assert.IsNotEmpty(files);
foreach (var file in files)
{
Assert.IsTrue(file.Book.IsLoaded);
Assert.IsTrue(file.Edition.IsLoaded);
Assert.IsTrue(file.Author.IsLoaded);
Assert.IsTrue(file.Author.Value.Metadata.IsLoaded);
}

View File

@@ -37,7 +37,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
_trackFiles = Builder<BookFile>.CreateListOfSize(3)
.All()
.With(t => t.BookId = _albums.First().Id)
.With(t => t.EditionId = _albums.First().Id)
.BuildList();
Mocker.GetMock<IMediaFileService>()

View File

@@ -36,7 +36,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
Path = "/My.Artist.S01E01.mp3",
Quality = new QualityModel(Quality.FLAC, new Revision(version: 1)),
DateAdded = DateTime.Now,
BookId = 1
EditionId = 1
};
_secondFile =
new BookFile
@@ -45,7 +45,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
Path = "/My.Artist.S01E02.mp3",
Quality = new QualityModel(Quality.FLAC, new Revision(version: 1)),
DateAdded = DateTime.Now,
BookId = 2
EditionId = 2
};
var singleAlbumList = new List<Book> { new Book { Id = 1 } };

View File

@@ -18,12 +18,12 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
{
var trackFile = Builder<BookFile>.CreateNew()
.With(h => h.Quality = new QualityModel())
.With(h => h.BookId = 1)
.With(h => h.EditionId = 1)
.BuildNew();
Db.Insert(trackFile);
Subject.Clean();
AllStoredModels[0].BookId.Should().Be(0);
AllStoredModels[0].EditionId.Should().Be(0);
}
}
}

View File

@@ -43,7 +43,6 @@ namespace NzbDrone.Core.Test.ImportListTests
.Returns<int>(x => Builder<Book>
.CreateListOfSize(1)
.TheFirst(1)
.With(b => b.GoodreadsId = x)
.With(b => b.ForeignBookId = x.ToString())
.BuildList());

View File

@@ -18,8 +18,9 @@ namespace NzbDrone.Core.Test.MediaCoverTests
[TestFixture]
public class MediaCoverServiceFixture : CoreTest<MediaCoverService>
{
private Author _artist;
private Book _album;
private Author _author;
private Book _book;
private Edition _edition;
private HttpResponse _httpResponse;
[SetUp]
@@ -27,14 +28,20 @@ namespace NzbDrone.Core.Test.MediaCoverTests
{
Mocker.SetConstant<IAppFolderInfo>(new AppFolderInfo(Mocker.Resolve<IStartupContext>()));
_artist = Builder<Author>.CreateNew()
_author = Builder<Author>.CreateNew()
.With(v => v.Id = 2)
.With(v => v.Metadata.Value.Images = new List<MediaCover.MediaCover> { new MediaCover.MediaCover(MediaCoverTypes.Poster, "") })
.Build();
_album = Builder<Book>.CreateNew()
.With(v => v.Id = 4)
_edition = Builder<Edition>.CreateNew()
.With(v => v.Id = 8)
.With(v => v.Images = new List<MediaCover.MediaCover> { new MediaCover.MediaCover(MediaCoverTypes.Cover, "") })
.With(v => v.Monitored = true)
.Build();
_book = Builder<Book>.CreateNew()
.With(v => v.Id = 4)
.With(v => v.Editions = new List<Edition> { _edition })
.Build();
_httpResponse = new HttpResponse(null, new HttpHeader(), "");
@@ -110,7 +117,7 @@ namespace NzbDrone.Core.Test.MediaCoverTests
Subject.ConvertToLocalUrls(6, MediaCoverEntity.Book, covers);
covers.Single().Url.Should().Be("/MediaCover/Albums/6/disc" + extension + "?lastWrite=1234");
covers.Single().Url.Should().Be("/MediaCover/Books/6/disc" + extension + "?lastWrite=1234");
}
[TestCase(".png")]
@@ -140,13 +147,13 @@ namespace NzbDrone.Core.Test.MediaCoverTests
Mocker.GetMock<IBookService>()
.Setup(v => v.GetBooksByAuthor(It.IsAny<int>()))
.Returns(new List<Book> { _album });
.Returns(new List<Book> { _book });
Mocker.GetMock<IDiskProvider>()
.Setup(v => v.FileExists(It.IsAny<string>()))
.Returns(true);
Subject.HandleAsync(new AuthorRefreshCompleteEvent(_artist));
Subject.HandleAsync(new AuthorRefreshCompleteEvent(_author));
Mocker.GetMock<IImageResizer>()
.Verify(v => v.Resize(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>()), Times.Exactly(2));
@@ -161,13 +168,13 @@ namespace NzbDrone.Core.Test.MediaCoverTests
Mocker.GetMock<IBookService>()
.Setup(v => v.GetBooksByAuthor(It.IsAny<int>()))
.Returns(new List<Book> { _album });
.Returns(new List<Book> { _book });
Mocker.GetMock<IDiskProvider>()
.Setup(v => v.FileExists(It.IsAny<string>()))
.Returns(false);
Subject.HandleAsync(new AuthorRefreshCompleteEvent(_artist));
Subject.HandleAsync(new AuthorRefreshCompleteEvent(_author));
Mocker.GetMock<IImageResizer>()
.Verify(v => v.Resize(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>()), Times.Exactly(2));
@@ -186,13 +193,13 @@ namespace NzbDrone.Core.Test.MediaCoverTests
Mocker.GetMock<IBookService>()
.Setup(v => v.GetBooksByAuthor(It.IsAny<int>()))
.Returns(new List<Book> { _album });
.Returns(new List<Book> { _book });
Mocker.GetMock<IDiskProvider>()
.Setup(v => v.GetFileSize(It.IsAny<string>()))
.Returns(1000);
Subject.HandleAsync(new AuthorRefreshCompleteEvent(_artist));
Subject.HandleAsync(new AuthorRefreshCompleteEvent(_author));
Mocker.GetMock<IImageResizer>()
.Verify(v => v.Resize(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>()), Times.Never());
@@ -211,13 +218,13 @@ namespace NzbDrone.Core.Test.MediaCoverTests
Mocker.GetMock<IBookService>()
.Setup(v => v.GetBooksByAuthor(It.IsAny<int>()))
.Returns(new List<Book> { _album });
.Returns(new List<Book> { _book });
Mocker.GetMock<IDiskProvider>()
.Setup(v => v.GetFileSize(It.IsAny<string>()))
.Returns(0);
Subject.HandleAsync(new AuthorRefreshCompleteEvent(_artist));
Subject.HandleAsync(new AuthorRefreshCompleteEvent(_author));
Mocker.GetMock<IImageResizer>()
.Verify(v => v.Resize(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>()), Times.Exactly(2));
@@ -236,13 +243,13 @@ namespace NzbDrone.Core.Test.MediaCoverTests
Mocker.GetMock<IBookService>()
.Setup(v => v.GetBooksByAuthor(It.IsAny<int>()))
.Returns(new List<Book> { _album });
.Returns(new List<Book> { _book });
Mocker.GetMock<IImageResizer>()
.Setup(v => v.Resize(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>()))
.Throws<ApplicationException>();
Subject.HandleAsync(new AuthorRefreshCompleteEvent(_artist));
Subject.HandleAsync(new AuthorRefreshCompleteEvent(_author));
Mocker.GetMock<IImageResizer>()
.Verify(v => v.Resize(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>()), Times.Exactly(2));

View File

@@ -315,8 +315,12 @@ namespace NzbDrone.Core.Test.MediaFiles.AudioTagServiceFixture
.With(x => x.Author = artist)
.Build();
var file = Builder<BookFile>.CreateNew()
var edition = Builder<Edition>.CreateNew()
.With(x => x.Book = album)
.Build();
var file = Builder<BookFile>.CreateNew()
.With(x => x.Edition = edition)
.With(x => x.Author = artist)
.Build();

View File

@@ -15,6 +15,7 @@ using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Profiles.Qualities;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.RootFolders;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Test.Common;
@@ -43,6 +44,14 @@ namespace NzbDrone.Core.Test.MediaFiles
.With(e => e.Author = artist)
.Build();
var edition = Builder<Edition>.CreateNew()
.With(e => e.Book = album)
.Build();
var rootFolder = Builder<RootFolder>.CreateNew()
.With(r => r.IsCalibreLibrary = false)
.Build();
_rejectedDecisions.Add(new ImportDecision<LocalBook>(new LocalBook(), new Rejection("Rejected!")));
_rejectedDecisions.Add(new ImportDecision<LocalBook>(new LocalBook(), new Rejection("Rejected!")));
_rejectedDecisions.Add(new ImportDecision<LocalBook>(new LocalBook(), new Rejection("Rejected!")));
@@ -52,6 +61,7 @@ namespace NzbDrone.Core.Test.MediaFiles
{
Author = artist,
Book = album,
Edition = edition,
Path = Path.Combine(artist.Path, "Alien Ant Farm - 01 - Pilot.mp3"),
Quality = new QualityModel(Quality.MP3_320),
FileTrackInfo = new ParsedTrackInfo
@@ -69,6 +79,10 @@ namespace NzbDrone.Core.Test.MediaFiles
Mocker.GetMock<IMediaFileService>()
.Setup(s => s.GetFilesByBook(It.IsAny<int>()))
.Returns(new List<BookFile>());
Mocker.GetMock<IRootFolderService>()
.Setup(s => s.GetBestRootFolder(It.IsAny<string>()))
.Returns(rootFolder);
}
[Test]
@@ -152,6 +166,7 @@ namespace NzbDrone.Core.Test.MediaFiles
{
Author = fileDecision.Item.Author,
Book = fileDecision.Item.Book,
Edition = fileDecision.Item.Edition,
Path = @"C:\Test\Music\Alien Ant Farm\Alien Ant Farm - 01 - Pilot.mp3".AsOsAgnostic(),
Quality = new QualityModel(Quality.MP3_320),
Size = 80.Megabytes()

View File

@@ -16,6 +16,7 @@ namespace NzbDrone.Core.Test.MediaFiles
{
private Author _artist;
private Book _album;
private Edition _edition;
[SetUp]
public void Setup()
@@ -37,12 +38,20 @@ namespace NzbDrone.Core.Test.MediaFiles
.Build();
Db.Insert(_album);
_edition = Builder<Edition>.CreateNew()
.With(a => a.Id = 0)
.With(a => a.BookId = _album.Id)
.Build();
Db.Insert(_edition);
var files = Builder<BookFile>.CreateListOfSize(10)
.All()
.With(c => c.Id = 0)
.With(c => c.Quality = new QualityModel(Quality.MP3_320))
.TheFirst(5)
.With(c => c.BookId = _album.Id)
.With(c => c.EditionId = _edition.Id)
.TheRest()
.With(c => c.EditionId = 0)
.TheFirst(1)
.With(c => c.Path = @"C:\Test\Path\Artist\somefile1.flac".AsOsAgnostic())
.TheNext(1)
@@ -109,8 +118,8 @@ namespace NzbDrone.Core.Test.MediaFiles
var file = Subject.GetFileWithPath(@"C:\Test\Path\Artist\somefile2.flac".AsOsAgnostic());
file.Should().NotBeNull();
file.Book.IsLoaded.Should().BeTrue();
file.Book.Value.Should().NotBeNull();
file.Edition.IsLoaded.Should().BeTrue();
file.Edition.Value.Should().NotBeNull();
file.Author.IsLoaded.Should().BeTrue();
file.Author.Value.Should().NotBeNull();
}
@@ -122,7 +131,7 @@ namespace NzbDrone.Core.Test.MediaFiles
var files = Subject.GetFilesByBook(_album.Id);
VerifyEagerLoaded(files);
files.Should().OnlyContain(c => c.BookId == _album.Id);
files.Should().OnlyContain(c => c.EditionId == _album.Id);
}
private void VerifyData()
@@ -136,8 +145,8 @@ namespace NzbDrone.Core.Test.MediaFiles
{
foreach (var file in files)
{
file.Book.IsLoaded.Should().BeTrue();
file.Book.Value.Should().NotBeNull();
file.Edition.IsLoaded.Should().BeTrue();
file.Edition.Value.Should().NotBeNull();
file.Author.IsLoaded.Should().BeTrue();
file.Author.Value.Should().NotBeNull();
file.Author.Value.Metadata.IsLoaded.Should().BeTrue();
@@ -149,8 +158,8 @@ namespace NzbDrone.Core.Test.MediaFiles
{
foreach (var file in files)
{
file.Book.IsLoaded.Should().BeFalse();
file.Book.Value.Should().BeNull();
file.Edition.IsLoaded.Should().BeFalse();
file.Edition.Value.Should().BeNull();
file.Author.IsLoaded.Should().BeFalse();
file.Author.Value.Should().BeNull();
}
@@ -162,7 +171,7 @@ namespace NzbDrone.Core.Test.MediaFiles
Db.Delete(_album);
Subject.DeleteFilesByBook(_album.Id);
Db.All<BookFile>().Where(x => x.BookId == _album.Id).Should().HaveCount(0);
Db.All<BookFile>().Where(x => x.EditionId == _album.Id).Should().HaveCount(0);
}
}
}

View File

@@ -213,7 +213,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaFileServiceTests
Path = "C:\\file2.avi".AsOsAgnostic(),
Size = 10,
Modified = _lastWrite,
Book = new LazyLoaded<Book>(null)
Edition = new LazyLoaded<Edition>(null)
}
});
@@ -239,7 +239,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaFileServiceTests
Path = "C:\\file2.avi".AsOsAgnostic(),
Size = 10,
Modified = _lastWrite,
Book = Builder<Book>.CreateNew().Build()
Edition = Builder<Edition>.CreateNew().Build()
}
});

View File

@@ -24,9 +24,9 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackFileMovingServiceTests
_trackFiles = Builder<BookFile>.CreateListOfSize(3)
.TheFirst(2)
.With(f => f.BookId = _album.Id)
.With(f => f.EditionId = _album.Id)
.TheNext(1)
.With(f => f.BookId = 0)
.With(f => f.EditionId = 0)
.Build().ToList();
}

View File

@@ -43,15 +43,15 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackFileMovingServiceTests
.Build();
Mocker.GetMock<IBuildFileNames>()
.Setup(s => s.BuildBookFileName(It.IsAny<Author>(), It.IsAny<Book>(), It.IsAny<BookFile>(), null, null))
.Setup(s => s.BuildBookFileName(It.IsAny<Author>(), It.IsAny<Edition>(), It.IsAny<BookFile>(), null, null))
.Returns("File Name");
Mocker.GetMock<IBuildFileNames>()
.Setup(s => s.BuildBookFilePath(It.IsAny<Author>(), It.IsAny<Book>(), It.IsAny<string>(), It.IsAny<string>()))
.Setup(s => s.BuildBookFilePath(It.IsAny<Author>(), It.IsAny<Edition>(), It.IsAny<string>(), It.IsAny<string>()))
.Returns(@"C:\Test\Music\Artist\Album\File Name.mp3".AsOsAgnostic());
Mocker.GetMock<IBuildFileNames>()
.Setup(s => s.BuildBookPath(It.IsAny<Author>(), It.IsAny<Book>()))
.Setup(s => s.BuildBookPath(It.IsAny<Author>()))
.Returns(@"C:\Test\Music\Artist\Album".AsOsAgnostic());
var rootFolder = @"C:\Test\Music\".AsOsAgnostic();

View File

@@ -15,7 +15,7 @@ namespace NzbDrone.Core.Test.MediaFiles.BookImport.Aggregation.Aggregators
[TestFixture]
public class AggregateFilenameInfoFixture : CoreTest<AggregateFilenameInfo>
{
private LocalAlbumRelease GivenTracks(List<string> files, string root)
private LocalEdition GivenTracks(List<string> files, string root)
{
var tracks = files.Select(x => new LocalBook
{
@@ -25,7 +25,7 @@ namespace NzbDrone.Core.Test.MediaFiles.BookImport.Aggregation.Aggregators
TrackNumbers = new[] { 0 },
}
}).ToList();
return new LocalAlbumRelease(tracks);
return new LocalEdition(tracks);
}
private void VerifyData(LocalBook track, string artist, string title, int trackNum, int disc)

View File

@@ -19,7 +19,7 @@ using NzbDrone.Core.MediaFiles.BookImport.Aggregation.Aggregators;
using NzbDrone.Core.MediaFiles.BookImport.Identification;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.MetadataSource;
using NzbDrone.Core.MetadataSource.SkyHook;
using NzbDrone.Core.MetadataSource.Goodreads;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Profiles.Metadata;
@@ -32,7 +32,7 @@ namespace NzbDrone.Core.Test.MediaFiles.BookImport.Identification
public class IdentificationServiceFixture : DbTest
{
private AuthorService _authorService;
private AddArtistService _addAuthorService;
private AddAuthorService _addAuthorService;
private RefreshAuthorService _refreshArtistService;
private IdentificationService _Subject;
@@ -59,10 +59,10 @@ namespace NzbDrone.Core.Test.MediaFiles.BookImport.Identification
Mocker.SetConstant<IMediaFileService>(Mocker.Resolve<MediaFileService>());
Mocker.SetConstant<IConfigService>(Mocker.Resolve<IConfigService>());
Mocker.SetConstant<IProvideAuthorInfo>(Mocker.Resolve<SkyHookProxy>());
Mocker.SetConstant<IProvideBookInfo>(Mocker.Resolve<SkyHookProxy>());
Mocker.SetConstant<IProvideAuthorInfo>(Mocker.Resolve<GoodreadsProxy>());
Mocker.SetConstant<IProvideBookInfo>(Mocker.Resolve<GoodreadsProxy>());
_addAuthorService = Mocker.Resolve<AddArtistService>();
_addAuthorService = Mocker.Resolve<AddAuthorService>();
Mocker.SetConstant<IRefreshBookService>(Mocker.Resolve<RefreshBookService>());
_refreshArtistService = Mocker.Resolve<RefreshAuthorService>();
@@ -73,11 +73,11 @@ namespace NzbDrone.Core.Test.MediaFiles.BookImport.Identification
Mocker.SetConstant<ICandidateService>(Mocker.Resolve<CandidateService>());
// set up the augmenters
List<IAggregate<LocalAlbumRelease>> aggregators = new List<IAggregate<LocalAlbumRelease>>
List<IAggregate<LocalEdition>> aggregators = new List<IAggregate<LocalEdition>>
{
Mocker.Resolve<AggregateFilenameInfo>()
};
Mocker.SetConstant<IEnumerable<IAggregate<LocalAlbumRelease>>>(aggregators);
Mocker.SetConstant<IEnumerable<IAggregate<LocalEdition>>>(aggregators);
Mocker.SetConstant<IAugmentingService>(Mocker.Resolve<AugmentingService>());
_Subject = Mocker.Resolve<IdentificationService>();

View File

@@ -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);
}
}
}

View File

@@ -28,18 +28,19 @@ namespace NzbDrone.Core.Test.MediaFiles.BookImport
private LocalBook _localTrack;
private Author _artist;
private Book _album;
private Edition _edition;
private QualityModel _quality;
private IdentificationOverrides _idOverrides;
private ImportDecisionMakerConfig _idConfig;
private Mock<IImportDecisionEngineSpecification<LocalAlbumRelease>> _albumpass1;
private Mock<IImportDecisionEngineSpecification<LocalAlbumRelease>> _albumpass2;
private Mock<IImportDecisionEngineSpecification<LocalAlbumRelease>> _albumpass3;
private Mock<IImportDecisionEngineSpecification<LocalEdition>> _albumpass1;
private Mock<IImportDecisionEngineSpecification<LocalEdition>> _albumpass2;
private Mock<IImportDecisionEngineSpecification<LocalEdition>> _albumpass3;
private Mock<IImportDecisionEngineSpecification<LocalAlbumRelease>> _albumfail1;
private Mock<IImportDecisionEngineSpecification<LocalAlbumRelease>> _albumfail2;
private Mock<IImportDecisionEngineSpecification<LocalAlbumRelease>> _albumfail3;
private Mock<IImportDecisionEngineSpecification<LocalEdition>> _albumfail1;
private Mock<IImportDecisionEngineSpecification<LocalEdition>> _albumfail2;
private Mock<IImportDecisionEngineSpecification<LocalEdition>> _albumfail3;
private Mock<IImportDecisionEngineSpecification<LocalBook>> _pass1;
private Mock<IImportDecisionEngineSpecification<LocalBook>> _pass2;
@@ -52,13 +53,13 @@ namespace NzbDrone.Core.Test.MediaFiles.BookImport
[SetUp]
public void Setup()
{
_albumpass1 = new Mock<IImportDecisionEngineSpecification<LocalAlbumRelease>>();
_albumpass2 = new Mock<IImportDecisionEngineSpecification<LocalAlbumRelease>>();
_albumpass3 = new Mock<IImportDecisionEngineSpecification<LocalAlbumRelease>>();
_albumpass1 = new Mock<IImportDecisionEngineSpecification<LocalEdition>>();
_albumpass2 = new Mock<IImportDecisionEngineSpecification<LocalEdition>>();
_albumpass3 = new Mock<IImportDecisionEngineSpecification<LocalEdition>>();
_albumfail1 = new Mock<IImportDecisionEngineSpecification<LocalAlbumRelease>>();
_albumfail2 = new Mock<IImportDecisionEngineSpecification<LocalAlbumRelease>>();
_albumfail3 = new Mock<IImportDecisionEngineSpecification<LocalAlbumRelease>>();
_albumfail1 = new Mock<IImportDecisionEngineSpecification<LocalEdition>>();
_albumfail2 = new Mock<IImportDecisionEngineSpecification<LocalEdition>>();
_albumfail3 = new Mock<IImportDecisionEngineSpecification<LocalEdition>>();
_pass1 = new Mock<IImportDecisionEngineSpecification<LocalBook>>();
_pass2 = new Mock<IImportDecisionEngineSpecification<LocalBook>>();
@@ -68,13 +69,13 @@ namespace NzbDrone.Core.Test.MediaFiles.BookImport
_fail2 = new Mock<IImportDecisionEngineSpecification<LocalBook>>();
_fail3 = new Mock<IImportDecisionEngineSpecification<LocalBook>>();
_albumpass1.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalAlbumRelease>(), It.IsAny<DownloadClientItem>())).Returns(Decision.Accept());
_albumpass2.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalAlbumRelease>(), It.IsAny<DownloadClientItem>())).Returns(Decision.Accept());
_albumpass3.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalAlbumRelease>(), It.IsAny<DownloadClientItem>())).Returns(Decision.Accept());
_albumpass1.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalEdition>(), It.IsAny<DownloadClientItem>())).Returns(Decision.Accept());
_albumpass2.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalEdition>(), It.IsAny<DownloadClientItem>())).Returns(Decision.Accept());
_albumpass3.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalEdition>(), It.IsAny<DownloadClientItem>())).Returns(Decision.Accept());
_albumfail1.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalAlbumRelease>(), It.IsAny<DownloadClientItem>())).Returns(Decision.Reject("_albumfail1"));
_albumfail2.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalAlbumRelease>(), It.IsAny<DownloadClientItem>())).Returns(Decision.Reject("_albumfail2"));
_albumfail3.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalAlbumRelease>(), It.IsAny<DownloadClientItem>())).Returns(Decision.Reject("_albumfail3"));
_albumfail1.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalEdition>(), It.IsAny<DownloadClientItem>())).Returns(Decision.Reject("_albumfail1"));
_albumfail2.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalEdition>(), It.IsAny<DownloadClientItem>())).Returns(Decision.Reject("_albumfail2"));
_albumfail3.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalEdition>(), It.IsAny<DownloadClientItem>())).Returns(Decision.Reject("_albumfail3"));
_pass1.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalBook>(), It.IsAny<DownloadClientItem>())).Returns(Decision.Accept());
_pass2.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalBook>(), It.IsAny<DownloadClientItem>())).Returns(Decision.Accept());
@@ -93,6 +94,10 @@ namespace NzbDrone.Core.Test.MediaFiles.BookImport
.With(x => x.Author = _artist)
.Build();
_edition = Builder<Edition>.CreateNew()
.With(x => x.Book = _album)
.Build();
_quality = new QualityModel(Quality.MP3_320);
_localTrack = new LocalBook
@@ -116,9 +121,9 @@ namespace NzbDrone.Core.Test.MediaFiles.BookImport
.Setup(s => s.Identify(It.IsAny<List<LocalBook>>(), It.IsAny<IdentificationOverrides>(), It.IsAny<ImportDecisionMakerConfig>()))
.Returns((List<LocalBook> tracks, IdentificationOverrides idOverrides, ImportDecisionMakerConfig config) =>
{
var ret = new LocalAlbumRelease(tracks);
ret.Book = _album;
return new List<LocalAlbumRelease> { ret };
var ret = new LocalEdition(tracks);
ret.Edition = _edition;
return new List<LocalEdition> { ret };
});
Mocker.GetMock<IMediaFileService>()
@@ -164,12 +169,12 @@ namespace NzbDrone.Core.Test.MediaFiles.BookImport
Subject.GetImportDecisions(_fileInfos, null, itemInfo, _idConfig);
_albumfail1.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalAlbumRelease>(), It.IsAny<DownloadClientItem>()), Times.Once());
_albumfail2.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalAlbumRelease>(), It.IsAny<DownloadClientItem>()), Times.Once());
_albumfail3.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalAlbumRelease>(), It.IsAny<DownloadClientItem>()), Times.Once());
_albumpass1.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalAlbumRelease>(), It.IsAny<DownloadClientItem>()), Times.Once());
_albumpass2.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalAlbumRelease>(), It.IsAny<DownloadClientItem>()), Times.Once());
_albumpass3.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalAlbumRelease>(), It.IsAny<DownloadClientItem>()), Times.Once());
_albumfail1.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalEdition>(), It.IsAny<DownloadClientItem>()), Times.Once());
_albumfail2.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalEdition>(), It.IsAny<DownloadClientItem>()), Times.Once());
_albumfail3.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalEdition>(), It.IsAny<DownloadClientItem>()), Times.Once());
_albumpass1.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalEdition>(), It.IsAny<DownloadClientItem>()), Times.Once());
_albumpass2.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalEdition>(), It.IsAny<DownloadClientItem>()), Times.Once());
_albumpass3.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalEdition>(), It.IsAny<DownloadClientItem>()), Times.Once());
}
[Test]
@@ -317,7 +322,7 @@ namespace NzbDrone.Core.Test.MediaFiles.BookImport
.Setup(s => s.Identify(It.IsAny<List<LocalBook>>(), It.IsAny<IdentificationOverrides>(), It.IsAny<ImportDecisionMakerConfig>()))
.Returns((List<LocalBook> tracks, IdentificationOverrides idOverrides, ImportDecisionMakerConfig config) =>
{
return new List<LocalAlbumRelease> { new LocalAlbumRelease(tracks) };
return new List<LocalEdition> { new LocalEdition(tracks) };
});
var decisions = Subject.GetImportDecisions(_fileInfos, _idOverrides, null, _idConfig);

View File

@@ -5,14 +5,14 @@ using Moq;
using NUnit.Framework;
using NzbDrone.Core.Books;
using NzbDrone.Core.Exceptions;
using NzbDrone.Core.MetadataSource.SkyHook;
using NzbDrone.Core.MetadataSource.Goodreads;
using NzbDrone.Core.Profiles.Metadata;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.MetadataSource.SkyHook
namespace NzbDrone.Core.Test.MetadataSource.Goodreads
{
[TestFixture]
public class SkyHookProxyFixture : CoreTest<SkyHookProxy>
public class GoodreadsProxyFixture : CoreTest<GoodreadsProxy>
{
private MetadataProfile _metadataProfile;
@@ -32,8 +32,8 @@ namespace NzbDrone.Core.Test.MetadataSource.SkyHook
.Returns(true);
}
[TestCase("amzn1.gr.author.v1.qTrNu9-PIaaBj5gYRDmN4Q", "Terry Pratchett")]
[TestCase("amzn1.gr.author.v1.afCyJgprpWE2xJU2_z3zTQ", "Robert Harris")]
[TestCase("1654", "Terry Pratchett")]
[TestCase("575", "Robert Harris")]
public void should_be_able_to_get_author_detail(string mbId, string name)
{
var details = Subject.GetAuthorInfo(mbId);
@@ -43,7 +43,7 @@ namespace NzbDrone.Core.Test.MetadataSource.SkyHook
details.Name.Should().Be(name);
}
[TestCase("amzn1.gr.book.v1.2rp8a0vJ8clGzMzZf61R9Q", "Guards! Guards!")]
[TestCase("64216", "Guards! Guards!")]
public void should_be_able_to_get_book_detail(string mbId, string name)
{
var details = Subject.GetBookInfo(mbId);
@@ -75,9 +75,6 @@ namespace NzbDrone.Core.Test.MetadataSource.SkyHook
author.Metadata.Value.Overview.Should().NotBeNullOrWhiteSpace();
author.Metadata.Value.Images.Should().NotBeEmpty();
author.ForeignAuthorId.Should().NotBeNullOrWhiteSpace();
author.Books.IsLoaded.Should().BeTrue();
author.Books.Value.Should().NotBeEmpty();
author.Books.Value.Should().OnlyContain(x => x.CleanTitle != null);
}
private void ValidateAlbums(List<Book> albums, bool idOnly = false)

View File

@@ -4,15 +4,15 @@ using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.Books;
using NzbDrone.Core.MetadataSource.SkyHook;
using NzbDrone.Core.MetadataSource.Goodreads;
using NzbDrone.Core.Profiles.Metadata;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.MetadataSource.SkyHook
namespace NzbDrone.Core.Test.MetadataSource.Goodreads
{
[TestFixture]
public class SkyHookProxySearchFixture : CoreTest<SkyHookProxy>
public class GoodreadsProxySearchFixture : CoreTest<GoodreadsProxy>
{
[SetUp]
public void Setup()
@@ -45,10 +45,10 @@ namespace NzbDrone.Core.Test.MetadataSource.SkyHook
}
[TestCase("Harry Potter and the sorcerer's stone", null, "Harry Potter and the Sorcerer's Stone")]
[TestCase("readarr:3", null, "Harry Potter and the Sorcerer's Stone")]
[TestCase("readarr: 3", null, "Harry Potter and the Sorcerer's Stone")]
[TestCase("readarrid:3", null, "Harry Potter and the Sorcerer's Stone")]
[TestCase("goodreads:3", null, "Harry Potter and the Sorcerer's Stone")]
[TestCase("readarr:3", null, "Harry Potter and the Philosopher's Stone")]
[TestCase("readarr: 3", null, "Harry Potter and the Philosopher's Stone")]
[TestCase("readarrid:3", null, "Harry Potter and the Philosopher's Stone")]
[TestCase("goodreads:3", null, "Harry Potter and the Philosopher's Stone")]
[TestCase("asin:B0192CTMYG", null, "Harry Potter and the Sorcerer's Stone")]
[TestCase("isbn:9780439554930", null, "Harry Potter and the Sorcerer's Stone")]
public void successful_album_search(string title, string artist, string expected)

View File

@@ -54,11 +54,19 @@ namespace NzbDrone.Core.Test.MusicTests
.Returns<Author, NamingConfig>((c, n) => c.Name);
}
private Book AlbumToAdd(string bookId, string authorId)
private Book AlbumToAdd(string editionId, string bookId, string authorId)
{
return new Book
{
ForeignBookId = bookId,
Editions = new List<Edition>
{
new Edition
{
ForeignEditionId = editionId,
Monitored = true
}
},
AuthorMetadata = new AuthorMetadata
{
ForeignAuthorId = authorId
@@ -69,9 +77,9 @@ namespace NzbDrone.Core.Test.MusicTests
[Test]
public void should_be_able_to_add_a_album_without_passing_in_name()
{
var newAlbum = AlbumToAdd("5537624c-3d2f-4f5c-8099-df916082c85c", "cc2c9c3c-b7bc-4b8b-84d8-4fbd8779e493");
var newAlbum = AlbumToAdd("edition", "book", "author");
GivenValidAlbum(newAlbum.ForeignBookId);
GivenValidAlbum("edition");
GivenValidPath();
var album = Subject.AddBook(newAlbum);
@@ -82,11 +90,11 @@ namespace NzbDrone.Core.Test.MusicTests
[Test]
public void should_throw_if_album_cannot_be_found()
{
var newAlbum = AlbumToAdd("5537624c-3d2f-4f5c-8099-df916082c85c", "cc2c9c3c-b7bc-4b8b-84d8-4fbd8779e493");
var newAlbum = AlbumToAdd("edition", "book", "author");
Mocker.GetMock<IProvideBookInfo>()
.Setup(s => s.GetBookInfo(newAlbum.ForeignBookId))
.Throws(new BookNotFoundException(newAlbum.ForeignBookId));
.Setup(s => s.GetBookInfo("edition"))
.Throws(new BookNotFoundException("edition"));
Assert.Throws<ValidationException>(() => Subject.AddBook(newAlbum));

View File

@@ -16,7 +16,7 @@ using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.MusicTests
{
[TestFixture]
public class AddArtistFixture : CoreTest<AddArtistService>
public class AddArtistFixture : CoreTest<AddAuthorService>
{
private Author _fakeArtist;

View File

@@ -36,7 +36,6 @@ namespace NzbDrone.Core.Test.MusicTests.AlbumRepositoryTests
{
Title = "ANThology",
ForeignBookId = "1",
ForeignWorkId = "1",
TitleSlug = "1-ANThology",
CleanTitle = "anthology",
Author = _artist,
@@ -50,7 +49,6 @@ namespace NzbDrone.Core.Test.MusicTests.AlbumRepositoryTests
{
Title = "+",
ForeignBookId = "2",
ForeignWorkId = "2",
TitleSlug = "2-_",
CleanTitle = "",
Author = _artist,

View File

@@ -143,6 +143,59 @@ namespace NzbDrone.Core.Test.MusicTests
item1.Should().Be(item2);
}
private Edition GivenEdition()
{
return _fixture.Build<Edition>()
.Without(x => x.Book)
.Without(x => x.BookFiles)
.Create();
}
[Test]
public void two_equivalent_editions_should_be_equal()
{
var item1 = GivenEdition();
var item2 = item1.JsonClone();
item1.Should().NotBeSameAs(item2);
item1.Should().Be(item2);
}
[Test]
[TestCaseSource(typeof(EqualityPropertySource<Edition>), "TestCases")]
public void two_different_editions_should_not_be_equal(PropertyInfo prop)
{
var item1 = GivenEdition();
var item2 = item1.JsonClone();
var different = GivenEdition();
// make item2 different in the property under consideration
if (prop.PropertyType == typeof(bool))
{
prop.SetValue(item2, !(bool)prop.GetValue(item1));
}
else
{
prop.SetValue(item2, prop.GetValue(different));
}
item1.Should().NotBeSameAs(item2);
item1.Should().NotBe(item2);
}
[Test]
public void metadata_and_db_fields_should_replicate_edition()
{
var item1 = GivenEdition();
var item2 = GivenEdition();
item1.Should().NotBe(item2);
item1.UseMetadataFrom(item2);
item1.UseDbFieldsFrom(item2);
item1.Should().Be(item2);
}
private Author GivenArtist()
{
return _fixture.Build<Author>()

View File

@@ -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);
}
}
}

View File

@@ -45,10 +45,12 @@ namespace NzbDrone.Core.Test.MusicTests
var metadata = Builder<AuthorMetadata>.CreateNew().Build();
var series = Builder<Series>.CreateListOfSize(1).BuildList();
var profile = Builder<MetadataProfile>.CreateNew().Build();
_artist = Builder<Author>.CreateNew()
.With(a => a.Metadata = metadata)
.With(a => a.Series = series)
.With(a => a.MetadataProfile = profile)
.Build();
Mocker.GetMock<IAuthorService>(MockBehavior.Strict)
@@ -63,7 +65,7 @@ namespace NzbDrone.Core.Test.MusicTests
.Returns(_albums);
Mocker.GetMock<IProvideAuthorInfo>()
.Setup(s => s.GetAuthorInfo(It.IsAny<string>()))
.Setup(s => s.GetAuthorAndBooks(It.IsAny<string>(), It.IsAny<double>()))
.Callback(() => { throw new AuthorNotFoundException(_artist.ForeignAuthorId); });
Mocker.GetMock<IMediaFileService>()
@@ -86,7 +88,7 @@ namespace NzbDrone.Core.Test.MusicTests
private void GivenNewArtistInfo(Author artist)
{
Mocker.GetMock<IProvideAuthorInfo>()
.Setup(s => s.GetAuthorInfo(_artist.ForeignAuthorId))
.Setup(s => s.GetAuthorAndBooks(_artist.ForeignAuthorId, It.IsAny<double>()))
.Returns(artist);
}

View File

@@ -38,7 +38,13 @@ namespace NzbDrone.Core.Test.OrganizerTests
.With(s => s.Title = "Fake: Book")
.Build();
Subject.BuildBookFilePath(fakeArtist, fakeAlbum, filename, ".mobi").Should().Be(expectedPath.AsOsAgnostic());
var fakeEdition = Builder<Edition>
.CreateNew()
.With(s => s.Title = fakeAlbum.Title)
.With(s => s.Book = fakeAlbum)
.Build();
Subject.BuildBookFilePath(fakeArtist, fakeEdition, filename, ".mobi").Should().Be(expectedPath.AsOsAgnostic());
}
}
}

View File

@@ -16,6 +16,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
{
private Author _artist;
private Book _album;
private Edition _edition;
private BookFile _trackFile;
private NamingConfig _namingConfig;
@@ -32,6 +33,12 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
.With(s => s.Title = "Hail to the King")
.Build();
_edition = Builder<Edition>
.CreateNew()
.With(s => s.Title = _album.Title)
.With(s => s.Book = _album)
.Build();
_trackFile = new BookFile { Quality = new QualityModel(Quality.MP3_320), ReleaseGroup = "ReadarrTest" };
_namingConfig = NamingConfig.Default;
@@ -68,7 +75,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
_artist.Name = name;
_namingConfig.StandardBookFormat = "{Author CleanName}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be(expected);
}
}

View File

@@ -18,6 +18,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
{
private Author _artist;
private Book _album;
private Edition _edition;
private BookFile _trackFile;
private NamingConfig _namingConfig;
@@ -37,7 +38,13 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
_album = Builder<Book>
.CreateNew()
.With(s => s.Title = "Hybrid Theory")
.Build();
_edition = Builder<Edition>
.CreateNew()
.With(s => s.Title = _album.Title)
.With(s => s.Disambiguation = "The Best Album")
.With(s => s.Book = _album)
.Build();
_namingConfig = NamingConfig.Default;
@@ -78,7 +85,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
{
_namingConfig.StandardBookFormat = "{Author Name}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("Linkin Park");
}
@@ -87,7 +94,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
{
_namingConfig.StandardBookFormat = "{Author_Name}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("Linkin_Park");
}
@@ -96,7 +103,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
{
_namingConfig.StandardBookFormat = "{Author.Name}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("Linkin.Park");
}
@@ -105,7 +112,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
{
_namingConfig.StandardBookFormat = "{Author-Name}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("Linkin-Park");
}
@@ -114,7 +121,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
{
_namingConfig.StandardBookFormat = "{AUTHOR NAME}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("LINKIN PARK");
}
@@ -123,7 +130,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
{
_namingConfig.StandardBookFormat = "{aUtHoR-nAmE}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be(_artist.Name.Replace(' ', '-'));
}
@@ -132,7 +139,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
{
_namingConfig.StandardBookFormat = "{author name}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("linkin park");
}
@@ -142,7 +149,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
_namingConfig.StandardBookFormat = "{Author.CleanName}";
_artist.Name = "Linkin Park (1997)";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("Linkin.Park.1997");
}
@@ -151,16 +158,16 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
{
_namingConfig.StandardBookFormat = "{Author Disambiguation}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("US Rock Band");
}
[Test]
public void should_replace_Album_space_Title()
public void should_replace_edition_space_Title()
{
_namingConfig.StandardBookFormat = "{Book Title}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("Hybrid Theory");
}
@@ -169,7 +176,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
{
_namingConfig.StandardBookFormat = "{Book Disambiguation}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("The Best Album");
}
@@ -178,7 +185,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
{
_namingConfig.StandardBookFormat = "{Book_Title}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("Hybrid_Theory");
}
@@ -187,7 +194,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
{
_namingConfig.StandardBookFormat = "{Book.Title}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("Hybrid.Theory");
}
@@ -196,7 +203,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
{
_namingConfig.StandardBookFormat = "{Book-Title}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("Hybrid-Theory");
}
@@ -205,7 +212,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
{
_namingConfig.StandardBookFormat = "{BOOK TITLE}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("HYBRID THEORY");
}
@@ -214,7 +221,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
{
_namingConfig.StandardBookFormat = "{bOoK-tItLE}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be(_album.Title.Replace(' ', '-'));
}
@@ -223,7 +230,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
{
_namingConfig.StandardBookFormat = "{book title}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("hybrid theory");
}
@@ -233,7 +240,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
_namingConfig.StandardBookFormat = "{Author.CleanName}";
_artist.Name = "Hybrid Theory (2000)";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("Hybrid.Theory.2000");
}
@@ -242,7 +249,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
{
_namingConfig.StandardBookFormat = "{Quality Title}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("MP3-320");
}
@@ -251,7 +258,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
{
_namingConfig.StandardBookFormat = "{MediaInfo AudioCodec}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("FLAC");
}
@@ -260,7 +267,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
{
_namingConfig.StandardBookFormat = "{MediaInfo AudioBitRate}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("320 kbps");
}
@@ -269,7 +276,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
{
_namingConfig.StandardBookFormat = "{MediaInfo AudioChannels}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("2.0");
}
@@ -278,7 +285,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
{
_namingConfig.StandardBookFormat = "{MediaInfo AudioBitsPerSample}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("16bit");
}
@@ -287,7 +294,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
{
_namingConfig.StandardBookFormat = "{MediaInfo AudioSampleRate}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("44.1kHz");
}
@@ -296,7 +303,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
{
_namingConfig.StandardBookFormat = "{Author Name} - {Book Title} - [{Quality Title}]";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("Linkin Park - Hybrid Theory - [MP3-320]");
}
@@ -306,7 +313,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
_namingConfig.RenameBooks = false;
_trackFile.Path = "Linkin Park - 06 - Test";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be(Path.GetFileNameWithoutExtension(_trackFile.Path));
}
@@ -317,7 +324,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
_trackFile.Path = "Linkin Park - 06 - Test";
_trackFile.SceneName = "SceneName";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be(Path.GetFileNameWithoutExtension(_trackFile.Path));
}
@@ -327,7 +334,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
_namingConfig.RenameBooks = false;
_trackFile.Path = @"C:\Test\Unsorted\Artist - 01 - Test";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be(Path.GetFileNameWithoutExtension(_trackFile.Path));
}
@@ -336,7 +343,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
{
_namingConfig.StandardBookFormat = "{Release Group}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be(_trackFile.ReleaseGroup);
}
@@ -349,7 +356,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
_trackFile.SceneName = "Linkin.Park.Meteora.320-LOL";
_trackFile.Path = "30 Rock - 01 - Test";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("Linkin Park - Linkin.Park.Meteora.320-LOL");
}
@@ -358,7 +365,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
{
_namingConfig.StandardBookFormat = "{Author.Name}.{Book.Title}";
Subject.BuildBookFileName(new Author { Name = "In The Woods." }, new Book { Title = "30 Rock" }, _trackFile)
Subject.BuildBookFileName(new Author { Name = "In The Woods." }, new Edition { Title = "30 Rock" }, _trackFile)
.Should().Be("In.The.Woods.30.Rock");
}
@@ -367,7 +374,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
{
_namingConfig.StandardBookFormat = "{Author.Name}.{Book.Title}";
Subject.BuildBookFileName(new Author { Name = "In The Woods..." }, new Book { Title = "30 Rock" }, _trackFile)
Subject.BuildBookFileName(new Author { Name = "In The Woods..." }, new Edition { Title = "30 Rock" }, _trackFile)
.Should().Be("In.The.Woods.30.Rock");
}
@@ -376,7 +383,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
{
_namingConfig.StandardBookFormat = "{Author.Name}{_Book.Title_}{Quality.Title}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("Linkin.Park_Hybrid.Theory_MP3-320");
}
@@ -385,7 +392,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
{
_namingConfig.StandardBookFormat = "{Author.Name}{_Book.Title_}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("Linkin.Park_Hybrid.Theory");
}
@@ -395,7 +402,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
_artist.Name = "Venture Bros.";
_namingConfig.StandardBookFormat = "{Author.Name}.{Book.Title}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("Venture.Bros.Hybrid.Theory");
}
@@ -408,7 +415,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
_trackFile.SceneName = null;
_trackFile.Path = "existing.file.mkv";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be(Path.GetFileNameWithoutExtension(_trackFile.Path));
}
@@ -421,7 +428,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
_trackFile.SceneName = "30.Rock.S01E01.xvid-LOL";
_trackFile.Path = "30 Rock - S01E01 - Test";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("30.Rock.S01E01.xvid-LOL");
}
@@ -430,7 +437,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
{
_namingConfig.StandardBookFormat = "{Quality Title} {Quality Proper}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("MP3-320");
}
@@ -439,7 +446,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
{
_namingConfig.StandardBookFormat = "{Author Name} - {Book Title} [{Quality Title}] {[Quality Proper]}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("Linkin Park - Hybrid Theory [MP3-320]");
}
@@ -448,7 +455,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
{
_namingConfig.StandardBookFormat = "{Author Name} - {Book Title} [{Quality Full}]";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("Linkin Park - Hybrid Theory [MP3-320]");
}
@@ -460,7 +467,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
{
_namingConfig.StandardBookFormat = string.Format("{{Quality{0}Title}}{0}{{Quality{0}Proper}}", separator);
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("MP3-320");
}
@@ -472,7 +479,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
{
_namingConfig.StandardBookFormat = string.Format("{{Quality{0}Title}}{0}{{Quality{0}Proper}}{0}{{Book{0}Title}}", separator);
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be(string.Format("MP3-320{0}Hybrid{0}Theory", separator));
}
@@ -485,7 +492,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
_trackFile.SceneName = "30.Rock.S01E01.xvid-LOL";
_trackFile.Path = "30 Rock - S01E01 - Test";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("30 Rock - 30 Rock - S01E01 - Test");
}
@@ -498,7 +505,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
_trackFile.SceneName = "30.Rock.S01E01.xvid-LOL";
_trackFile.Path = "30 Rock - S01E01 - Test";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("30 Rock - S01E01 - Test");
}
@@ -508,7 +515,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
_trackFile.ReleaseGroup = null;
_namingConfig.StandardBookFormat = "{Release Group}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("Readarr");
}
@@ -520,7 +527,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
_trackFile.ReleaseGroup = null;
_namingConfig.StandardBookFormat = pattern;
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be(expectedFileName);
}
@@ -532,7 +539,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
_trackFile.ReleaseGroup = releaseGroup;
_namingConfig.StandardBookFormat = "{Release Group}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be(releaseGroup);
}
}

View File

@@ -16,6 +16,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
{
private Author _artist;
private Book _album;
private Edition _edition;
private BookFile _trackFile;
private NamingConfig _namingConfig;
@@ -32,6 +33,12 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
.With(s => s.Title = "Anthology")
.Build();
_edition = Builder<Edition>
.CreateNew()
.With(s => s.Title = _album.Title)
.With(s => s.Book = _album)
.Build();
_trackFile = new BookFile { Quality = new QualityModel(Quality.MP3_320), ReleaseGroup = "ReadarrTest" };
_namingConfig = NamingConfig.Default;
@@ -62,7 +69,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
_artist.Name = name;
_namingConfig.StandardBookFormat = "{Author NameThe}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be(expected);
}
@@ -75,7 +82,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
_artist.Name = name;
_namingConfig.StandardBookFormat = "{Author NameThe}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be(name);
}
}

View File

@@ -16,7 +16,7 @@ namespace NzbDrone.Core.AuthorStats
public class AuthorStatisticsRepository : IAuthorStatisticsRepository
{
private const string _selectTemplate = "SELECT /**select**/ FROM Books /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/ /**orderby**/";
private const string _selectTemplate = "SELECT /**select**/ FROM Editions /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/ /**orderby**/";
private readonly IMainDatabase _database;
@@ -28,14 +28,22 @@ namespace NzbDrone.Core.AuthorStats
public List<BookStatistics> AuthorStatistics()
{
var time = DateTime.UtcNow;
return Query(Builder().Where<Book>(x => x.ReleaseDate < time));
var stats = Query(Builder());
#pragma warning disable CS0472
return Query(Builder().OrWhere<Book>(x => x.ReleaseDate < time)
.OrWhere<BookFile>(x => x.Id != null));
#pragma warning restore
}
public List<BookStatistics> AuthorStatistics(int authorId)
{
var time = DateTime.UtcNow;
return Query(Builder().Where<Book>(x => x.ReleaseDate < time)
#pragma warning disable CS0472
return Query(Builder().OrWhere<Book>(x => x.ReleaseDate < time)
.OrWhere<BookFile>(x => x.Id != null)
.Where<Author>(x => x.Id == authorId));
#pragma warning restore
}
private List<BookStatistics> Query(SqlBuilder builder)
@@ -56,8 +64,10 @@ namespace NzbDrone.Core.AuthorStats
SUM(CASE WHEN BookFiles.Id IS NULL THEN 0 ELSE 1 END) AS AvailableBookCount,
SUM(CASE WHEN Books.Monitored = 1 OR BookFiles.Id IS NOT NULL THEN 1 ELSE 0 END) AS BookCount,
SUM(CASE WHEN BookFiles.Id IS NULL THEN 0 ELSE 1 END) AS BookFileCount")
.Join<Edition, Book>((e, b) => e.BookId == b.Id)
.Join<Book, Author>((book, author) => book.AuthorMetadataId == author.AuthorMetadataId)
.LeftJoin<Book, BookFile>((t, f) => t.Id == f.BookId)
.LeftJoin<Edition, BookFile>((t, f) => t.Id == f.EditionId)
.Where<Edition>(x => x.Monitored == true)
.GroupBy<Author>(x => x.Id)
.GroupBy<Book>(x => x.Id);
}

View File

@@ -117,7 +117,7 @@ namespace NzbDrone.Core.Books.Calibre
public void SetFields(BookFile file, CalibreSettings settings)
{
var book = file.Book.Value;
var book = file.Edition.Value;
var cover = book.Images.FirstOrDefault(x => x.CoverType == MediaCoverTypes.Cover);
string image = null;
@@ -144,7 +144,6 @@ namespace NzbDrone.Core.Books.Calibre
rating = book.Ratings.Value * 2,
identifiers = new Dictionary<string, string>
{
{ "goodreads", book.GoodreadsId.ToString() },
{ "isbn", book.Isbn13 },
{ "asin", book.Asin }
}

View File

@@ -0,0 +1,14 @@
using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.Books.Events
{
public class EditionDeletedEvent : IEvent
{
public Edition Edition { get; private set; }
public EditionDeletedEvent(Edition edition)
{
Edition = edition;
}
}
}

View File

@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Common.Extensions;
@@ -16,13 +17,15 @@ namespace NzbDrone.Core.Books
}
public string ForeignAuthorId { get; set; }
public int GoodreadsId { get; set; }
public string TitleSlug { get; set; }
public string Name { get; set; }
public List<string> Aliases { get; set; }
public string Overview { get; set; }
public string Disambiguation { get; set; }
public string Type { get; set; }
public string Gender { get; set; }
public string Hometown { get; set; }
public DateTime? Born { get; set; }
public DateTime? Died { get; set; }
public AuthorStatusType Status { get; set; }
public List<MediaCover.MediaCover> Images { get; set; }
public List<Links> Links { get; set; }
@@ -37,13 +40,15 @@ namespace NzbDrone.Core.Books
public override void UseMetadataFrom(AuthorMetadata other)
{
ForeignAuthorId = other.ForeignAuthorId;
GoodreadsId = other.GoodreadsId;
TitleSlug = other.TitleSlug;
Name = other.Name;
Aliases = other.Aliases;
Overview = other.Overview.IsNullOrWhiteSpace() ? Overview : other.Overview;
Disambiguation = other.Disambiguation;
Type = other.Type;
Gender = other.Gender;
Hometown = other.Hometown;
Born = other.Born;
Died = other.Died;
Status = other.Status;
Images = other.Images.Any() ? other.Images : Images;
Links = other.Links;

View File

@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Equ;
using Newtonsoft.Json;
using NzbDrone.Common.Extensions;
@@ -13,8 +12,6 @@ namespace NzbDrone.Core.Books
{
public Book()
{
Overview = string.Empty;
Images = new List<MediaCover.MediaCover>();
Links = new List<Links>();
Genres = new List<string>();
Ratings = new Ratings();
@@ -26,19 +23,9 @@ namespace NzbDrone.Core.Books
// These are metadata entries
public int AuthorMetadataId { get; set; }
public string ForeignBookId { get; set; }
public string ForeignWorkId { get; set; }
public int GoodreadsId { 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 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 List<string> Genres { get; set; }
public Ratings Ratings { get; set; }
@@ -46,6 +33,7 @@ namespace NzbDrone.Core.Books
// These are Readarr generated/config
public string CleanTitle { get; set; }
public bool Monitored { get; set; }
public bool AnyEditionOk { get; set; }
public DateTime? LastInfoSync { get; set; }
public DateTime Added { get; set; }
[MemberwiseEqualityIgnore]
@@ -57,6 +45,8 @@ namespace NzbDrone.Core.Books
[MemberwiseEqualityIgnore]
public LazyLoaded<Author> Author { get; set; }
[MemberwiseEqualityIgnore]
public LazyLoaded<List<Edition>> Editions { get; set; }
[MemberwiseEqualityIgnore]
public LazyLoaded<List<BookFile>> BookFiles { get; set; }
[MemberwiseEqualityIgnore]
public LazyLoaded<List<SeriesBookLink>> SeriesLinks { get; set; }
@@ -77,19 +67,9 @@ namespace NzbDrone.Core.Books
public override void UseMetadataFrom(Book other)
{
ForeignBookId = other.ForeignBookId;
ForeignWorkId = other.ForeignWorkId;
GoodreadsId = other.GoodreadsId;
TitleSlug = other.TitleSlug;
Isbn13 = other.Isbn13;
Asin = other.Asin;
Title = other.Title;
Language = other.Language;
Overview = other.Overview.IsNullOrWhiteSpace() ? Overview : other.Overview;
Disambiguation = other.Disambiguation;
Publisher = other.Publisher;
PageCount = other.PageCount;
ReleaseDate = other.ReleaseDate;
Images = other.Images.Any() ? other.Images : Images;
Links = other.Links;
Genres = other.Genres;
Ratings = other.Ratings;
@@ -101,6 +81,7 @@ namespace NzbDrone.Core.Books
Id = other.Id;
AuthorMetadataId = other.AuthorMetadataId;
Monitored = other.Monitored;
AnyEditionOk = other.AnyEditionOk;
LastInfoSync = other.LastInfoSync;
Added = other.Added;
AddOptions = other.AddOptions;
@@ -109,9 +90,9 @@ namespace NzbDrone.Core.Books
public override void ApplyChanges(Book other)
{
ForeignBookId = other.ForeignBookId;
ForeignWorkId = other.ForeignWorkId;
AddOptions = other.AddOptions;
Monitored = other.Monitored;
AnyEditionOk = other.AnyEditionOk;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -7,5 +7,7 @@ namespace NzbDrone.Core.Books
{
public int Votes { get; set; }
public decimal Value { get; set; }
public double Popularity => (double)Value * Votes;
}
}

View File

@@ -70,7 +70,7 @@ namespace NzbDrone.Core.Books
public List<Book> GetBooksByFileIds(IEnumerable<int> fileIds)
{
return Query(new SqlBuilder()
.Join<Book, BookFile>((l, r) => l.Id == r.BookId)
.Join<Book, BookFile>((l, r) => l.Id == r.EditionId)
.Where<BookFile>(f => fileIds.Contains(f.Id)))
.DistinctBy(x => x.Id)
.ToList();
@@ -90,7 +90,7 @@ namespace NzbDrone.Core.Books
#pragma warning disable CS0472
private SqlBuilder AlbumsWithoutFilesBuilder(DateTime currentTime) => Builder()
.Join<Book, Author>((l, r) => l.AuthorMetadataId == r.AuthorMetadataId)
.LeftJoin<Book, BookFile>((t, f) => t.Id == f.BookId)
.LeftJoin<Book, BookFile>((t, f) => t.Id == f.EditionId)
.Where<BookFile>(f => f.Id == null)
.Where<Book>(a => a.ReleaseDate <= currentTime);
#pragma warning restore CS0472
@@ -107,7 +107,7 @@ namespace NzbDrone.Core.Books
private SqlBuilder AlbumsWhereCutoffUnmetBuilder(List<QualitiesBelowCutoff> qualitiesBelowCutoff) => Builder()
.Join<Book, Author>((l, r) => l.AuthorMetadataId == r.AuthorMetadataId)
.Join<Book, BookFile>((t, f) => t.Id == f.BookId)
.Join<Book, BookFile>((t, f) => t.Id == f.EditionId)
.Where(BuildQualityCutoffWhereClause(qualitiesBelowCutoff));
private string BuildQualityCutoffWhereClause(List<QualitiesBelowCutoff> qualitiesBelowCutoff)
@@ -193,7 +193,7 @@ namespace NzbDrone.Core.Books
public List<Book> GetAuthorBooksWithFiles(Author author)
{
return Query(Builder()
.Join<Book, BookFile>((t, f) => t.Id == f.BookId)
.Join<Book, BookFile>((t, f) => t.Id == f.EditionId)
.Where<Book>(x => x.AuthorMetadataId == author.AuthorMetadataId));
}
}

View File

@@ -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;
}
}
}

View File

@@ -20,7 +20,7 @@ namespace NzbDrone.Core.Books
List<Author> AddAuthors(List<Author> newAuthors, bool doRefresh = true);
}
public class AddArtistService : IAddAuthorService
public class AddAuthorService : IAddAuthorService
{
private readonly IAuthorService _authorService;
private readonly IAuthorMetadataService _authorMetadataService;
@@ -29,7 +29,7 @@ namespace NzbDrone.Core.Books
private readonly IAddAuthorValidator _addAuthorValidator;
private readonly Logger _logger;
public AddArtistService(IAuthorService authorService,
public AddAuthorService(IAuthorService authorService,
IAuthorMetadataService authorMetadataService,
IProvideAuthorInfo authorInfo,
IBuildFileNames fileNameBuilder,

View File

@@ -44,7 +44,17 @@ namespace NzbDrone.Core.Books
{
_logger.Debug($"Adding book {book}");
// we allow adding extra editions, so check if the book already exists
var dbBook = _bookService.FindById(book.ForeignBookId);
if (dbBook != null)
{
dbBook.Editions = book.Editions;
book = dbBook;
}
else
{
book = AddSkyhookData(book);
}
// Remove any import list exclusions preventing addition
_importListExclusionService.Delete(book.ForeignBookId);
@@ -98,7 +108,7 @@ namespace NzbDrone.Core.Books
Tuple<string, Book, List<AuthorMetadata>> tuple = null;
try
{
tuple = _bookInfo.GetBookInfo(newBook.ForeignBookId);
tuple = _bookInfo.GetBookInfo(newBook.Editions.Value.Single(x => x.Monitored).ForeignEditionId);
}
catch (BookNotFoundException)
{

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using NLog;
using NzbDrone.Common.Extensions;
@@ -45,21 +46,32 @@ namespace NzbDrone.Core.Books
IHandle<AuthorDeletedEvent>
{
private readonly IBookRepository _bookRepository;
private readonly IEditionService _editionService;
private readonly IEventAggregator _eventAggregator;
private readonly Logger _logger;
public BookService(IBookRepository bookRepository,
IEditionService editionService,
IEventAggregator eventAggregator,
Logger logger)
{
_bookRepository = bookRepository;
_editionService = editionService;
_eventAggregator = eventAggregator;
_logger = logger;
}
public Book AddBook(Book newBook, bool doRefresh = true)
{
_bookRepository.Insert(newBook);
var editions = newBook.Editions.Value;
editions.ForEach(x => x.Monitored = newBook.Id > 0);
_bookRepository.Upsert(newBook);
editions.ForEach(x => x.BookId = newBook.Id);
_editionService.InsertMany(editions);
_editionService.SetMonitored(editions.First());
_eventAggregator.PublishEvent(new BookAddedEvent(GetBook(newBook.Id), doRefresh));

View File

@@ -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);
}
}
}

View File

@@ -21,7 +21,12 @@ using NzbDrone.Core.RootFolders;
namespace NzbDrone.Core.Books
{
public interface IRefreshAuthorService
{
}
public class RefreshAuthorService : RefreshEntityServiceBase<Author, Book>,
IRefreshAuthorService,
IExecute<RefreshAuthorCommand>,
IExecute<BulkRefreshAuthorCommand>
{
@@ -76,11 +81,11 @@ namespace NzbDrone.Core.Books
_logger = logger;
}
private Author GetSkyhookData(string foreignId)
private Author GetSkyhookData(string foreignId, double minPopularity)
{
try
{
return _authorInfo.GetAuthorInfo(foreignId);
return _authorInfo.GetAuthorAndBooks(foreignId, minPopularity);
}
catch (AuthorNotFoundException)
{
@@ -278,7 +283,6 @@ namespace NzbDrone.Core.Books
{
// little hack - trigger the series update here
_refreshSeriesService.RefreshSeriesInfo(entity.AuthorMetadataId, entity.Series, entity, false, false, null);
_eventAggregator.PublishEvent(new AuthorRefreshCompleteEvent(entity));
}
@@ -332,7 +336,7 @@ namespace NzbDrone.Core.Books
{
try
{
var data = GetSkyhookData(author.ForeignAuthorId);
var data = GetSkyhookData(author.ForeignAuthorId, author.MetadataProfile.Value.MinPopularity);
updated |= RefreshEntityInfo(author, null, data, true, false, null);
}
catch (Exception e)
@@ -381,7 +385,7 @@ namespace NzbDrone.Core.Books
{
try
{
var data = GetSkyhookData(author.ForeignAuthorId);
var data = GetSkyhookData(author.ForeignAuthorId, author.MetadataProfile.Value.MinPopularity);
updated |= RefreshEntityInfo(author, null, data, manualTrigger, false, message.LastStartTime);
}
catch (Exception e)

View File

@@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Core.Books.Events;
using NzbDrone.Core.History;
@@ -18,12 +20,14 @@ namespace NzbDrone.Core.Books
bool RefreshBookInfo(List<Book> books, List<Book> remoteBooks, Author remoteData, bool forceBookRefresh, bool forceUpdateFileTags, DateTime? lastUpdate);
}
public class RefreshBookService : RefreshEntityServiceBase<Book, object>, IRefreshBookService
public class RefreshBookService : RefreshEntityServiceBase<Book, Edition>, IRefreshBookService
{
private readonly IBookService _bookService;
private readonly IAuthorService _authorService;
private readonly IAddAuthorService _addAuthorService;
private readonly IEditionService _editionService;
private readonly IProvideBookInfo _bookInfo;
private readonly IRefreshEditionService _refreshEditionService;
private readonly IMediaFileService _mediaFileService;
private readonly IHistoryService _historyService;
private readonly IEventAggregator _eventAggregator;
@@ -34,8 +38,10 @@ namespace NzbDrone.Core.Books
public RefreshBookService(IBookService bookService,
IAuthorService authorService,
IAddAuthorService addAuthorService,
IEditionService editionService,
IAuthorMetadataService authorMetadataService,
IProvideBookInfo bookInfo,
IRefreshEditionService refreshEditionService,
IMediaFileService mediaFileService,
IHistoryService historyService,
IEventAggregator eventAggregator,
@@ -47,7 +53,9 @@ namespace NzbDrone.Core.Books
_bookService = bookService;
_authorService = authorService;
_addAuthorService = addAuthorService;
_editionService = editionService;
_bookInfo = bookInfo;
_refreshEditionService = refreshEditionService;
_mediaFileService = mediaFileService;
_historyService = historyService;
_eventAggregator = eventAggregator;
@@ -60,7 +68,7 @@ namespace NzbDrone.Core.Books
{
var result = new RemoteData();
var book = remote.SingleOrDefault(x => x.ForeignWorkId == local.ForeignWorkId);
var book = remote.SingleOrDefault(x => x.ForeignBookId == local.ForeignBookId);
if (book == null && ShouldDelete(local))
{
@@ -69,7 +77,7 @@ namespace NzbDrone.Core.Books
if (book == null)
{
book = data.Books.Value.SingleOrDefault(x => x.ForeignWorkId == local.ForeignWorkId);
book = data.Books.Value.SingleOrDefault(x => x.ForeignBookId == local.ForeignBookId);
}
result.Entity = book;
@@ -167,7 +175,7 @@ namespace NzbDrone.Core.Books
// Update book ids for trackfiles
var files = _mediaFileService.GetFilesByBook(local.Id);
files.ForEach(x => x.BookId = target.Id);
files.ForEach(x => x.EditionId = target.Id);
_mediaFileService.Update(files);
// Update book ids for history
@@ -197,36 +205,70 @@ namespace NzbDrone.Core.Books
_bookService.DeleteBook(local.Id, true);
}
protected override List<object> GetRemoteChildren(Book local, Book remote)
protected override List<Edition> GetRemoteChildren(Book local, Book remote)
{
return new List<object>();
return remote.Editions.Value.DistinctBy(m => m.ForeignEditionId).ToList();
}
protected override List<object> GetLocalChildren(Book entity, List<object> remoteChildren)
protected override List<Edition> GetLocalChildren(Book entity, List<Edition> remoteChildren)
{
return new List<object>();
return _editionService.GetEditionsForRefresh(entity.Id, remoteChildren.Select(x => x.ForeignEditionId));
}
protected override Tuple<object, List<object>> GetMatchingExistingChildren(List<object> existingChildren, object remote)
protected override Tuple<Edition, List<Edition>> GetMatchingExistingChildren(List<Edition> existingChildren, Edition remote)
{
return null;
var existingChild = existingChildren.SingleOrDefault(x => x.ForeignEditionId == remote.ForeignEditionId);
return Tuple.Create(existingChild, new List<Edition>());
}
protected override void PrepareNewChild(object child, Book entity)
protected override void PrepareNewChild(Edition child, Book entity)
{
child.BookId = entity.Id;
child.Book = entity;
}
protected override void PrepareExistingChild(object local, object remote, Book entity)
protected override void PrepareExistingChild(Edition local, Edition remote, Book entity)
{
local.BookId = entity.Id;
local.Book = entity;
remote.UseDbFieldsFrom(local);
}
protected override void AddChildren(List<object> children)
protected override void AddChildren(List<Edition> children)
{
// hack - add the chilren in refresh children so we can control monitored status
}
protected override bool RefreshChildren(SortedChildren localChildren, List<object> remoteChildren, Author remoteData, bool forceChildRefresh, bool forceUpdateFileTags, DateTime? lastUpdate)
private void MonitorSingleEdition(List<Edition> releases)
{
return false;
var monitored = releases.Where(x => x.Monitored).ToList();
if (!monitored.Any())
{
monitored = releases;
}
var toMonitor = monitored.OrderByDescending(x => _mediaFileService.GetFilesByEdition(x.Id).Count)
.ThenByDescending(x => x.Ratings.Votes)
.First();
releases.ForEach(x => x.Monitored = false);
toMonitor.Monitored = true;
Debug.Assert(!releases.Any() || releases.Count(x => x.Monitored) == 1, "one edition monitored");
}
protected override bool RefreshChildren(SortedChildren localChildren, List<Edition> remoteChildren, Author remoteData, bool forceChildRefresh, bool forceUpdateFileTags, DateTime? lastUpdate)
{
// make sure only one of the releases ends up monitored
localChildren.Old.ForEach(x => x.Monitored = false);
MonitorSingleEdition(localChildren.Future);
localChildren.All.ForEach(x => _logger.Trace($"release: {x} monitored: {x.Monitored}"));
_editionService.InsertMany(localChildren.Added);
return _refreshEditionService.RefreshEditionInfo(localChildren.Added, localChildren.Updated, localChildren.Merged, localChildren.Deleted, localChildren.UpToDate, remoteChildren, forceUpdateFileTags);
}
protected override void PublishEntityUpdatedEvent(Book entity)

View File

@@ -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();
}
}
}

View File

@@ -129,13 +129,13 @@ namespace NzbDrone.Core.Books
var existing = existingByAuthor.Concat(existingBySeries).GroupBy(x => x.ForeignSeriesId).Select(x => x.First()).ToList();
var books = _bookService.GetBooksByAuthorMetadataId(authorMetadataId);
var bookDict = books.ToDictionary(x => x.ForeignWorkId);
var bookDict = books.ToDictionary(x => x.ForeignBookId);
var links = new List<SeriesBookLink>();
foreach (var s in remoteData.Series.Value)
{
s.LinkItems.Value.ForEach(x => x.Series = s);
links.AddRange(s.LinkItems.Value.Where(x => bookDict.ContainsKey(x.Book.Value.ForeignWorkId)));
links.AddRange(s.LinkItems.Value.Where(x => bookDict.ContainsKey(x.Book.Value.ForeignBookId)));
}
var grouped = links.GroupBy(x => x.Series.Value);

View File

@@ -53,12 +53,14 @@ namespace NzbDrone.Core.Datastore.Migration
Create.TableForModel("AuthorMetadata")
.WithColumn("ForeignAuthorId").AsString().Unique()
.WithColumn("GoodreadsId").AsInt32()
.WithColumn("TitleSlug").AsString().Unique()
.WithColumn("Name").AsString()
.WithColumn("Overview").AsString().Nullable()
.WithColumn("Disambiguation").AsString().Nullable()
.WithColumn("Type").AsString().Nullable()
.WithColumn("Gender").AsString().Nullable()
.WithColumn("Hometown").AsString().Nullable()
.WithColumn("Born").AsDateTime().Nullable()
.WithColumn("Died").AsDateTime().Nullable()
.WithColumn("Status").AsInt32()
.WithColumn("Images").AsString()
.WithColumn("Links").AsString().Nullable()
@@ -68,31 +70,43 @@ namespace NzbDrone.Core.Datastore.Migration
Create.TableForModel("Books")
.WithColumn("AuthorMetadataId").AsInt32().WithDefaultValue(0)
.WithColumn("ForeignBookId").AsString().Unique()
.WithColumn("ForeignWorkId").AsString().Indexed()
.WithColumn("GoodreadsId").AsInt32()
.WithColumn("ForeignBookId").AsString().Indexed()
.WithColumn("TitleSlug").AsString().Unique()
.WithColumn("Isbn13").AsString().Nullable()
.WithColumn("Asin").AsString().Nullable()
.WithColumn("Title").AsString()
.WithColumn("Language").AsString().Nullable()
.WithColumn("Overview").AsString().Nullable()
.WithColumn("PageCount").AsInt32().Nullable()
.WithColumn("Disambiguation").AsString().Nullable()
.WithColumn("Publisher").AsString().Nullable()
.WithColumn("ReleaseDate").AsDateTime().Nullable()
.WithColumn("Images").AsString()
.WithColumn("Links").AsString().Nullable()
.WithColumn("Genres").AsString().Nullable()
.WithColumn("Ratings").AsString().Nullable()
.WithColumn("CleanTitle").AsString().Indexed()
.WithColumn("Monitored").AsBoolean()
.WithColumn("AnyEditionOk").AsBoolean()
.WithColumn("LastInfoSync").AsDateTime().Nullable()
.WithColumn("Added").AsDateTime().Nullable()
.WithColumn("AddOptions").AsString().Nullable();
Create.TableForModel("Editions")
.WithColumn("BookId").AsInt32().WithDefaultValue(0)
.WithColumn("ForeignEditionId").AsString().Unique()
.WithColumn("Isbn13").AsString().Nullable()
.WithColumn("Asin").AsString().Nullable()
.WithColumn("Title").AsString()
.WithColumn("TitleSlug").AsString()
.WithColumn("Language").AsString().Nullable()
.WithColumn("Overview").AsString().Nullable()
.WithColumn("Format").AsString().Nullable()
.WithColumn("IsEbook").AsBoolean().Nullable()
.WithColumn("Disambiguation").AsString().Nullable()
.WithColumn("Publisher").AsString().Nullable()
.WithColumn("PageCount").AsInt32().Nullable()
.WithColumn("ReleaseDate").AsDateTime().Nullable()
.WithColumn("Images").AsString()
.WithColumn("Links").AsString().Nullable()
.WithColumn("Ratings").AsString().Nullable()
.WithColumn("Monitored").AsBoolean()
.WithColumn("ManualAdd").AsBoolean();
Create.TableForModel("BookFiles")
.WithColumn("BookId").AsInt32().Indexed()
.WithColumn("EditionId").AsInt32().Indexed()
.WithColumn("CalibreId").AsInt32()
.WithColumn("Quality").AsString()
.WithColumn("Size").AsInt64()
@@ -152,8 +166,7 @@ namespace NzbDrone.Core.Datastore.Migration
Create.TableForModel("MetadataProfiles")
.WithColumn("Name").AsString().Unique()
.WithColumn("MinRating").AsDouble()
.WithColumn("MinRatingCount").AsInt32()
.WithColumn("MinPopularity").AsDouble()
.WithColumn("SkipMissingDate").AsBoolean()
.WithColumn("SkipMissingIsbn").AsBoolean()
.WithColumn("SkipPartsAndSets").AsBoolean()

View File

@@ -123,9 +123,12 @@ namespace NzbDrone.Core.Datastore
.HasOne(r => r.AuthorMetadata, r => r.AuthorMetadataId)
.LazyLoad(x => x.BookFiles,
(db, book) => db.Query<BookFile>(new SqlBuilder()
.Join<BookFile, Book>((l, r) => l.BookId == r.Id)
.Join<BookFile, Book>((l, r) => l.EditionId == r.Id)
.Where<Book>(b => b.Id == book.Id)).ToList(),
b => b.Id > 0)
.LazyLoad(x => x.Editions,
(db, book) => db.Query<Edition>(new SqlBuilder().Where<Edition>(e => e.BookId == book.Id)).ToList(),
b => b.Id > 0)
.LazyLoad(a => a.Author,
(db, book) => AuthorRepository.Query(db,
new SqlBuilder()
@@ -133,14 +136,22 @@ namespace NzbDrone.Core.Datastore
.Where<Author>(a => a.AuthorMetadataId == book.AuthorMetadataId)).SingleOrDefault(),
a => a.AuthorMetadataId > 0);
Mapper.Entity<Edition>("Editions").RegisterModel()
.HasOne(r => r.Book, r => r.BookId)
.LazyLoad(x => x.BookFiles,
(db, book) => db.Query<BookFile>(new SqlBuilder()
.Join<BookFile, Book>((l, r) => l.EditionId == r.Id)
.Where<Book>(b => b.Id == book.Id)).ToList(),
b => b.Id > 0);
Mapper.Entity<BookFile>("BookFiles").RegisterModel()
.HasOne(f => f.Book, f => f.BookId)
.HasOne(f => f.Edition, f => f.EditionId)
.LazyLoad(x => x.Author,
(db, f) => AuthorRepository.Query(db,
new SqlBuilder()
.Join<Author, AuthorMetadata>((a, m) => a.AuthorMetadataId == m.Id)
.Join<Author, Book>((l, r) => l.AuthorMetadataId == r.AuthorMetadataId)
.Where<Book>(a => a.Id == f.BookId)).SingleOrDefault(),
.Where<Book>(a => a.Id == f.EditionId)).SingleOrDefault(),
t => t.Id > 0);
Mapper.Entity<QualityDefinition>("QualityDefinitions").RegisterModel()

View File

@@ -143,7 +143,7 @@ namespace NzbDrone.Core.Extras
public void Handle(TrackFolderCreatedEvent message)
{
var author = message.Author;
var book = _bookService.GetBook(message.BookFile.BookId);
var book = _bookService.GetBook(message.BookFile.EditionId);
foreach (var extraFileManager in _extraFileManagers)
{

View File

@@ -72,7 +72,7 @@ namespace NzbDrone.Core.Extras.Files
return new TExtraFile
{
AuthorId = author.Id,
BookId = bookFile.BookId,
BookId = bookFile.EditionId,
BookFileId = bookFile.Id,
RelativePath = author.Path.GetRelativePath(newFileName),
Extension = extension

View File

@@ -144,7 +144,7 @@ namespace NzbDrone.Core.Extras.Metadata
foreach (var filePath in distinctTrackFilePaths)
{
var metadataFilesForConsumer = GetMetadataFilesForConsumer(consumer, metadataFiles)
.Where(m => m.BookId == filePath.BookId)
.Where(m => m.BookId == filePath.EditionId)
.Where(m => m.Type == MetadataType.BookImage || m.Type == MetadataType.BookMetadata)
.ToList();
@@ -287,7 +287,7 @@ namespace NzbDrone.Core.Extras.Metadata
new MetadataFile
{
AuthorId = author.Id,
BookId = bookFile.BookId,
BookId = bookFile.EditionId,
BookFileId = bookFile.Id,
Consumer = consumer.GetType().Name,
Type = MetadataType.BookMetadata,

View File

@@ -294,7 +294,7 @@ namespace NzbDrone.Core.History
Quality = message.BookFile.Quality,
SourceTitle = message.BookFile.Path,
AuthorId = message.BookFile.Author.Value.Id,
BookId = message.BookFile.BookId
BookId = message.BookFile.EditionId
};
history.Data.Add("Reason", message.Reason.ToString());
@@ -314,7 +314,7 @@ namespace NzbDrone.Core.History
Quality = message.BookFile.Quality,
SourceTitle = message.OriginalPath,
AuthorId = message.BookFile.Author.Value.Id,
BookId = message.BookFile.BookId
BookId = message.BookFile.EditionId
};
history.Data.Add("SourcePath", sourcePath);
@@ -334,7 +334,7 @@ namespace NzbDrone.Core.History
Quality = message.BookFile.Quality,
SourceTitle = path,
AuthorId = message.BookFile.Author.Value.Id,
BookId = message.BookFile.BookId
BookId = message.BookFile.EditionId
};
history.Data.Add("TagsScrubbed", message.Scrubbed.ToString());

View File

@@ -18,12 +18,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
{
// Unlink where track no longer exists
mapper.Execute(@"UPDATE BookFiles
SET BookId = 0
SET EditionId = 0
WHERE Id IN (
SELECT BookFiles.Id FROM BookFiles
LEFT OUTER JOIN Books
ON BookFiles.BookId = Books.Id
WHERE Books.Id IS NULL)");
LEFT OUTER JOIN Editions
ON BookFiles.EditionId = Editions.Id
WHERE Editions.Id IS NULL)");
}
}
}

View File

@@ -139,7 +139,7 @@ namespace NzbDrone.Core.ImportLists
if (report.AlbumMusicBrainzId.IsNotNullOrWhiteSpace() && int.TryParse(report.AlbumMusicBrainzId, out var goodreadsId))
{
mappedAlbum = _bookSearchService.SearchByGoodreadsId(goodreadsId).FirstOrDefault(x => x.GoodreadsId == goodreadsId);
mappedAlbum = _bookSearchService.SearchByGoodreadsId(goodreadsId).FirstOrDefault(x => int.TryParse(x.ForeignBookId, out var bookId) && bookId == goodreadsId);
}
else
{

View File

@@ -72,17 +72,13 @@ namespace NzbDrone.Core.IndexerSearch
var searchSpec = Get<BookSearchCriteria>(author, new List<Book> { book }, userInvokedSearch, interactiveSearch);
searchSpec.BookTitle = book.Title;
searchSpec.BookIsbn = book.Isbn13;
// searchSpec.BookIsbn = book.Isbn13;
if (book.ReleaseDate.HasValue)
{
searchSpec.BookYear = book.ReleaseDate.Value.Year;
}
if (book.Disambiguation.IsNotNullOrWhiteSpace())
{
searchSpec.Disambiguation = book.Disambiguation;
}
return Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec);
}

View File

@@ -32,7 +32,7 @@ namespace NzbDrone.Core.Indexers.Newznab
}
}
private bool SupportsAudioSearch
private bool SupportsBookSearch
{
get
{
@@ -67,7 +67,7 @@ namespace NzbDrone.Core.Indexers.Newznab
{
var pageableRequests = new IndexerPageableRequestChain();
if (SupportsAudioSearch)
if (SupportsBookSearch)
{
AddBookPageableRequests(pageableRequests,
searchCriteria,
@@ -78,12 +78,17 @@ namespace NzbDrone.Core.Indexers.Newznab
{
pageableRequests.AddTier();
pageableRequests.Add(GetPagedRequests(MaxPages,
/* pageableRequests.Add(GetPagedRequests(MaxPages,
Settings.Categories,
"search",
NewsnabifyTitle($"&q={searchCriteria.BookIsbn}")));
pageableRequests.AddTier();
pageableRequests.AddTier();*/
pageableRequests.Add(GetPagedRequests(MaxPages,
Settings.Categories,
"search",
NewsnabifyTitle($"&q={searchCriteria.BookQuery}+{searchCriteria.AuthorQuery}")));
pageableRequests.Add(GetPagedRequests(MaxPages,
Settings.Categories,
@@ -98,7 +103,7 @@ namespace NzbDrone.Core.Indexers.Newznab
{
var pageableRequests = new IndexerPageableRequestChain();
if (SupportsAudioSearch)
if (SupportsBookSearch)
{
AddBookPageableRequests(pageableRequests,
searchCriteria,
@@ -122,7 +127,7 @@ namespace NzbDrone.Core.Indexers.Newznab
{
chain.AddTier();
chain.Add(GetPagedRequests(MaxPages, Settings.Categories, "book", $"&q={parameters}"));
chain.Add(GetPagedRequests(MaxPages, Settings.Categories, "book", $"{parameters}"));
}
private IEnumerable<IndexerRequest> GetPagedRequests(int maxPages, IEnumerable<int> categories, string searchType, string parameters)

View File

@@ -91,7 +91,7 @@ namespace NzbDrone.Core.MediaCover
if (coverEntity == MediaCoverEntity.Book)
{
mediaCover.Url = _configFileProvider.UrlBase + @"/MediaCover/Albums/" + entityId + "/" + mediaCover.CoverType.ToString().ToLower() + mediaCover.Extension;
mediaCover.Url = _configFileProvider.UrlBase + @"/MediaCover/Books/" + entityId + "/" + mediaCover.CoverType.ToString().ToLower() + mediaCover.Extension;
}
else
{
@@ -113,7 +113,7 @@ namespace NzbDrone.Core.MediaCover
private string GetAlbumCoverPath(int bookId)
{
return Path.Combine(_coverRootFolder, "Albums", bookId.ToString());
return Path.Combine(_coverRootFolder, "Books", bookId.ToString());
}
private void EnsureArtistCovers(Author author)
@@ -163,7 +163,7 @@ namespace NzbDrone.Core.MediaCover
public void EnsureAlbumCovers(Book book)
{
foreach (var cover in book.Images.Where(e => e.CoverType == MediaCoverTypes.Cover))
foreach (var cover in book.Editions.Value.Single(x => x.Monitored).Images.Where(e => e.CoverType == MediaCoverTypes.Cover))
{
var fileName = GetCoverPath(book.Id, MediaCoverEntity.Book, cover.CoverType, cover.Extension, null);
var alreadyExists = false;

View File

@@ -23,7 +23,7 @@ namespace NzbDrone.Core.MediaFiles
{
ParsedTrackInfo ReadTags(string file);
void WriteTags(BookFile trackfile, bool newDownload, bool force = false);
void SyncTags(List<Book> tracks);
void SyncTags(List<Edition> tracks);
List<RetagBookFilePreview> GetRetagPreviewsByArtist(int authorId);
List<RetagBookFilePreview> GetRetagPreviewsByAlbum(int authorId);
}
@@ -148,7 +148,7 @@ namespace NzbDrone.Core.MediaFiles
_eventAggregator.PublishEvent(new BookFileRetaggedEvent(trackfile.Author.Value, trackfile, diff, _configService.ScrubAudioTags));
}
public void SyncTags(List<Book> books)
public void SyncTags(List<Edition> editions)
{
if (_configService.WriteAudioTags != WriteAudioTagsType.Sync)
{
@@ -156,9 +156,9 @@ namespace NzbDrone.Core.MediaFiles
}
// get the tracks to update
foreach (var book in books)
foreach (var edition in editions)
{
var bookFiles = book.BookFiles.Value;
var bookFiles = edition.BookFiles.Value;
_logger.Debug($"Syncing audio tags for {bookFiles.Count} files");
@@ -166,7 +166,7 @@ namespace NzbDrone.Core.MediaFiles
{
// populate tracks (which should also have release/book/author set) because
// not all of the updates will have been committed to the database yet
file.Book = book;
file.Edition = edition;
WriteTags(file, false);
}
}
@@ -188,11 +188,11 @@ namespace NzbDrone.Core.MediaFiles
private IEnumerable<RetagBookFilePreview> GetPreviews(List<BookFile> files)
{
foreach (var f in files.OrderBy(x => x.Book.Value.Title))
foreach (var f in files.OrderBy(x => x.Edition.Value.Title))
{
var file = f;
if (f.Book.Value == null)
if (f.Edition.Value == null)
{
_logger.Warn($"File {f} is not linked to any books");
continue;
@@ -207,7 +207,7 @@ namespace NzbDrone.Core.MediaFiles
yield return new RetagBookFilePreview
{
AuthorId = file.Author.Value.Id,
BookId = file.Book.Value.Id,
BookId = file.Edition.Value.Id,
BookFileId = file.Id,
Path = file.Path,
Changes = diff

View File

@@ -19,12 +19,12 @@ namespace NzbDrone.Core.MediaFiles
public string ReleaseGroup { get; set; }
public QualityModel Quality { get; set; }
public MediaInfoModel MediaInfo { get; set; }
public int BookId { get; set; }
public int EditionId { get; set; }
public int CalibreId { get; set; }
// These are queried from the database
public LazyLoaded<Author> Author { get; set; }
public LazyLoaded<Book> Book { get; set; }
public LazyLoaded<Edition> Edition { get; set; }
public override string ToString()
{

View File

@@ -60,9 +60,9 @@ namespace NzbDrone.Core.MediaFiles
public BookFile MoveBookFile(BookFile bookFile, Author author)
{
var book = _bookService.GetBook(bookFile.BookId);
var newFileName = _buildFileNames.BuildBookFileName(author, book, bookFile);
var filePath = _buildFileNames.BuildBookFilePath(author, book, newFileName, Path.GetExtension(bookFile.Path));
var book = _bookService.GetBook(bookFile.EditionId);
var newFileName = _buildFileNames.BuildBookFileName(author, bookFile.Edition.Value, bookFile);
var filePath = _buildFileNames.BuildBookFilePath(author, bookFile.Edition.Value, newFileName, Path.GetExtension(bookFile.Path));
EnsureBookFolder(bookFile, author, book, filePath);
@@ -73,8 +73,8 @@ namespace NzbDrone.Core.MediaFiles
public BookFile MoveBookFile(BookFile bookFile, LocalBook localBook)
{
var newFileName = _buildFileNames.BuildBookFileName(localBook.Author, localBook.Book, bookFile);
var filePath = _buildFileNames.BuildBookFilePath(localBook.Author, localBook.Book, newFileName, Path.GetExtension(localBook.Path));
var newFileName = _buildFileNames.BuildBookFileName(localBook.Author, localBook.Edition, bookFile);
var filePath = _buildFileNames.BuildBookFilePath(localBook.Author, localBook.Edition, newFileName, Path.GetExtension(localBook.Path));
EnsureTrackFolder(bookFile, localBook, filePath);
@@ -85,8 +85,8 @@ namespace NzbDrone.Core.MediaFiles
public BookFile CopyBookFile(BookFile bookFile, LocalBook localBook)
{
var newFileName = _buildFileNames.BuildBookFileName(localBook.Author, localBook.Book, bookFile);
var filePath = _buildFileNames.BuildBookFilePath(localBook.Author, localBook.Book, newFileName, Path.GetExtension(localBook.Path));
var newFileName = _buildFileNames.BuildBookFileName(localBook.Author, localBook.Edition, bookFile);
var filePath = _buildFileNames.BuildBookFilePath(localBook.Author, localBook.Edition, newFileName, Path.GetExtension(localBook.Path));
EnsureTrackFolder(bookFile, localBook, filePath);
@@ -147,7 +147,7 @@ namespace NzbDrone.Core.MediaFiles
private void EnsureBookFolder(BookFile bookFile, Author author, Book book, string filePath)
{
var trackFolder = Path.GetDirectoryName(filePath);
var bookFolder = _buildFileNames.BuildBookPath(author, book);
var bookFolder = _buildFileNames.BuildBookPath(author);
var authorFolder = author.Path;
var rootFolder = new OsPath(authorFolder).Directory.FullPath;

View File

@@ -11,18 +11,18 @@ namespace NzbDrone.Core.MediaFiles.BookImport.Aggregation
public interface IAugmentingService
{
LocalBook Augment(LocalBook localTrack, bool otherFiles);
LocalAlbumRelease Augment(LocalAlbumRelease localAlbum);
LocalEdition Augment(LocalEdition localAlbum);
}
public class AugmentingService : IAugmentingService
{
private readonly IEnumerable<IAggregate<LocalBook>> _trackAugmenters;
private readonly IEnumerable<IAggregate<LocalAlbumRelease>> _albumAugmenters;
private readonly IEnumerable<IAggregate<LocalEdition>> _albumAugmenters;
private readonly IDiskProvider _diskProvider;
private readonly Logger _logger;
public AugmentingService(IEnumerable<IAggregate<LocalBook>> trackAugmenters,
IEnumerable<IAggregate<LocalAlbumRelease>> albumAugmenters,
IEnumerable<IAggregate<LocalEdition>> albumAugmenters,
IDiskProvider diskProvider,
Logger logger)
{
@@ -61,7 +61,7 @@ namespace NzbDrone.Core.MediaFiles.BookImport.Aggregation
return localTrack;
}
public LocalAlbumRelease Augment(LocalAlbumRelease localAlbum)
public LocalEdition Augment(LocalEdition localAlbum)
{
foreach (var augmenter in _albumAugmenters)
{

View File

@@ -9,7 +9,7 @@ using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.MediaFiles.BookImport.Aggregation.Aggregators
{
public class AggregateFilenameInfo : IAggregate<LocalAlbumRelease>
public class AggregateFilenameInfo : IAggregate<LocalEdition>
{
private readonly Logger _logger;
@@ -55,7 +55,7 @@ namespace NzbDrone.Core.MediaFiles.BookImport.Aggregation.Aggregators
_logger = logger;
}
public LocalAlbumRelease Aggregate(LocalAlbumRelease release, bool others)
public LocalEdition Aggregate(LocalEdition release, bool others)
{
var tracks = release.LocalBooks;
if (tracks.Count(x => x.FileTrackInfo.Title.IsNullOrWhiteSpace()) > 0

View File

@@ -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