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>
<Label size={sizes.LARGE}>
<HeartRating
rating={ratings.value}
iconSize={13}
/>
</Label>
{
authorType ?
ratings.votes > 0 ?
<Label size={sizes.LARGE}>
{authorType}
<HeartRating
rating={ratings.value}
iconSize={13}
/>
</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 @@ protected virtual void AddRequestHeaders(HttpWebRequest webRequest, HttpHeader h
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 @@ public class ArtistStatisticsFixture : DbTest<AuthorStatisticsRepository, Author
{
private Author _artist;
private Book _album;
private Edition _edition;
private BookFile _trackFile;
[SetUp]
@ -32,10 +33,16 @@ public void Setup()
.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 @@ public void Setup()
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 @@ public void should_explicit_load_everything_if_joined()
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 @@ public void Setup()
_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 @@ public void Setup()
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 @@ public void Setup()
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 @@ public void should_unlink_orphaned_track_files()
{
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 @@ public void SetUp()
.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 @@ public void Setup()
{
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 @@ public void should_convert_album_cover_urls_to_local(string extension)
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 @@ public void should_resize_covers_if_main_downloaded()
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 @@ public void should_resize_covers_if_missing()
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 @@ public void should_not_resize_covers_if_exists()
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 @@ public void should_resize_covers_if_existing_is_empty()
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 @@ public void should_log_error_if_resize_failed()
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 @@ private BookFile GivenPopulatedTrackfile(int mediumOffset)
.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.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 @@ public void Setup()
.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 @@ public void Setup()
{
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 @@ public void Setup()
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 @@ public void should_import_larger_files_first()
{
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 @@ public class MediaFileRepositoryFixture : DbTest<MediaFileRepository, BookFile>
{
private Author _artist;
private Book _album;
private Edition _edition;
[SetUp]
public void Setup()
@ -37,12 +38,20 @@ public void Setup()
.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 @@ public void get_file_by_path()
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 @@ public void get_files_by_album()
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 @@ private void VerifyEagerLoaded(List<BookFile> files)
{
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 @@ private void VerifyUnmapped(List<BookFile> files)
{
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 @@ public void delete_files_by_album_should_work_if_join_fails()
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 @@ public void filter_unmatched_should_return_existing_file_if_unmatched(FilterFile
Path = "C:\\file2.avi".AsOsAgnostic(),
Size = 10,
Modified = _lastWrite,
Book = new LazyLoaded<Book>(null)
Edition = new LazyLoaded<Edition>(null)
}
});
@ -239,7 +239,7 @@ public void filter_unmatched_should_not_return_existing_file_if_matched(FilterFi
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 @@ public void Setup()
_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 @@ public void Setup()
.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 @@ private LocalAlbumRelease GivenTracks(List<string> files, string root)
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.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 @@ public void SetUp()
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 @@ public void SetUp()
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 @@ public class ImportDecisionMakerFixture : FileSystemTest<ImportDecisionMaker>
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 @@ public class ImportDecisionMakerFixture : FileSystemTest<ImportDecisionMaker>
[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 @@ public void Setup()
_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 @@ public void Setup()
.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 @@ public void Setup()
.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 @@ public void should_call_all_album_specifications()
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 @@ public void should_not_throw_if_release_not_identified()
.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 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 @@ public void Setup()
.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 @@ public void should_be_able_to_get_author_detail(string mbId, string name)
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 @@ private void ValidateAuthor(Author author)
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 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 @@ public void successful_artist_search(string title, string expected)
}
[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 @@ private void GivenValidPath()
.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 @@ private Book AlbumToAdd(string bookId, string authorId)
[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 @@ public void should_be_able_to_add_a_album_without_passing_in_name()
[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 @@
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 @@ public void Setup()
{
Title = "ANThology",
ForeignBookId = "1",
ForeignWorkId = "1",
TitleSlug = "1-ANThology",
CleanTitle = "anthology",
Author = _artist,
@ -50,7 +49,6 @@ public void Setup()
{
Title = "+",
ForeignBookId = "2",
ForeignWorkId = "2",
TitleSlug = "2-_",
CleanTitle = "",
Author = _artist,

View file

@ -143,6 +143,59 @@ public void metadata_and_db_fields_should_replicate_album()
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 @@ public void Setup()
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,8 +65,8 @@ public void Setup()
.Returns(_albums);
Mocker.GetMock<IProvideAuthorInfo>()
.Setup(s => s.GetAuthorInfo(It.IsAny<string>()))
.Callback(() => { throw new AuthorNotFoundException(_artist.ForeignAuthorId); });
.Setup(s => s.GetAuthorAndBooks(It.IsAny<string>(), It.IsAny<double>()))
.Callback(() => { throw new AuthorNotFoundException(_artist.ForeignAuthorId); });
Mocker.GetMock<IMediaFileService>()
.Setup(x => x.GetFilesByAuthor(It.IsAny<int>()))
@ -86,8 +88,8 @@ public void Setup()
private void GivenNewArtistInfo(Author artist)
{
Mocker.GetMock<IProvideAuthorInfo>()
.Setup(s => s.GetAuthorInfo(_artist.ForeignAuthorId))
.Returns(artist);
.Setup(s => s.GetAuthorAndBooks(_artist.ForeignAuthorId, It.IsAny<double>()))
.Returns(artist);
}
private void GivenArtistFiles()

View file

@ -38,7 +38,13 @@ public void should_clean_album_folder_when_it_contains_illegal_characters_in_alb
.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 @@ public class CleanTitleFixture : CoreTest<FileNameBuilder>
{
private Author _artist;
private Book _album;
private Edition _edition;
private BookFile _trackFile;
private NamingConfig _namingConfig;
@ -32,6 +33,12 @@ public void Setup()
.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 @@ public void should_get_expected_title_back(string name, string expected)
_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 @@ public class FileNameBuilderFixture : CoreTest<FileNameBuilder>
{
private Author _artist;
private Book _album;
private Edition _edition;
private BookFile _trackFile;
private NamingConfig _namingConfig;
@ -37,7 +38,13 @@ public void Setup()
_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 @@ public void should_replace_Artist_space_Name()
{
_namingConfig.StandardBookFormat = "{Author Name}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("Linkin Park");
}
@ -87,7 +94,7 @@ public void should_replace_Artist_underscore_Name()
{
_namingConfig.StandardBookFormat = "{Author_Name}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("Linkin_Park");
}
@ -96,7 +103,7 @@ public void should_replace_Artist_dot_Name()
{
_namingConfig.StandardBookFormat = "{Author.Name}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("Linkin.Park");
}
@ -105,7 +112,7 @@ public void should_replace_Artist_dash_Name()
{
_namingConfig.StandardBookFormat = "{Author-Name}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("Linkin-Park");
}
@ -114,7 +121,7 @@ public void should_replace_ARTIST_NAME_with_all_caps()
{
_namingConfig.StandardBookFormat = "{AUTHOR NAME}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("LINKIN PARK");
}
@ -123,7 +130,7 @@ public void should_replace_ARTIST_NAME_with_random_casing_should_keep_original_c
{
_namingConfig.StandardBookFormat = "{aUtHoR-nAmE}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be(_artist.Name.Replace(' ', '-'));
}
@ -132,7 +139,7 @@ public void should_replace_artist_name_with_all_lower_case()
{
_namingConfig.StandardBookFormat = "{author name}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("linkin park");
}
@ -142,7 +149,7 @@ public void should_cleanup_Artist_Name()
_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 @@ public void should_replace_Artist_Disambiguation()
{
_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 @@ public void should_replace_Album_Disambiguation()
{
_namingConfig.StandardBookFormat = "{Book Disambiguation}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("The Best Album");
}
@ -178,7 +185,7 @@ public void should_replace_Album_underscore_Title()
{
_namingConfig.StandardBookFormat = "{Book_Title}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("Hybrid_Theory");
}
@ -187,7 +194,7 @@ public void should_replace_Album_dot_Title()
{
_namingConfig.StandardBookFormat = "{Book.Title}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("Hybrid.Theory");
}
@ -196,7 +203,7 @@ public void should_replace_Album_dash_Title()
{
_namingConfig.StandardBookFormat = "{Book-Title}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("Hybrid-Theory");
}
@ -205,7 +212,7 @@ public void should_replace_ALBUM_TITLE_with_all_caps()
{
_namingConfig.StandardBookFormat = "{BOOK TITLE}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("HYBRID THEORY");
}
@ -214,7 +221,7 @@ public void should_replace_ALBUM_TITLE_with_random_casing_should_keep_original_c
{
_namingConfig.StandardBookFormat = "{bOoK-tItLE}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be(_album.Title.Replace(' ', '-'));
}
@ -223,7 +230,7 @@ public void should_replace_album_title_with_all_lower_case()
{
_namingConfig.StandardBookFormat = "{book title}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("hybrid theory");
}
@ -233,7 +240,7 @@ public void should_cleanup_Album_Title()
_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 @@ public void should_replace_quality_title()
{
_namingConfig.StandardBookFormat = "{Quality Title}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("MP3-320");
}
@ -251,7 +258,7 @@ public void should_replace_media_info_audio_codec()
{
_namingConfig.StandardBookFormat = "{MediaInfo AudioCodec}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("FLAC");
}
@ -260,7 +267,7 @@ public void should_replace_media_info_audio_bitrate()
{
_namingConfig.StandardBookFormat = "{MediaInfo AudioBitRate}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("320 kbps");
}
@ -269,7 +276,7 @@ public void should_replace_media_info_audio_channels()
{
_namingConfig.StandardBookFormat = "{MediaInfo AudioChannels}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("2.0");
}
@ -278,7 +285,7 @@ public void should_replace_media_info_bits_per_sample()
{
_namingConfig.StandardBookFormat = "{MediaInfo AudioBitsPerSample}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("16bit");
}
@ -287,7 +294,7 @@ public void should_replace_media_info_sample_rate()
{
_namingConfig.StandardBookFormat = "{MediaInfo AudioSampleRate}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("44.1kHz");
}
@ -296,7 +303,7 @@ public void should_replace_all_contents_in_pattern()
{
_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 @@ public void use_file_name_when_sceneName_is_null()
_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 @@ public void use_file_name_when_sceneName_is_not_null()
_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 @@ public void use_path_when_sceneName_and_relative_path_are_null()
_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 @@ public void should_should_replace_release_group()
{
_namingConfig.StandardBookFormat = "{Release Group}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be(_trackFile.ReleaseGroup);
}
@ -349,7 +356,7 @@ public void should_be_able_to_use_original_title()
_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 @@ public void should_replace_double_period_with_single_period()
{
_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 @@ public void should_replace_triple_period_with_single_period()
{
_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 @@ public void should_include_affixes_if_value_not_empty()
{
_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 @@ public void should_not_include_affixes_if_value_empty()
{
_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 @@ public void should_remove_duplicate_non_word_characters()
_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 @@ public void should_use_existing_filename_when_scene_name_is_not_available()
_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 @@ public void should_be_able_to_use_only_original_title()
_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 @@ public void should_not_include_quality_proper_when_release_is_not_a_proper()
{
_namingConfig.StandardBookFormat = "{Quality Title} {Quality Proper}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("MP3-320");
}
@ -439,7 +446,7 @@ public void should_not_wrap_proper_in_square_brackets_when_not_a_proper()
{
_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 @@ public void should_replace_quality_full_with_quality_title_only_when_not_a_prope
{
_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 @@ public void should_trim_extra_separators_from_end_when_quality_proper_is_not_inc
{
_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 @@ public void should_trim_extra_separators_from_middle_when_quality_proper_is_not_
{
_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 @@ public void should_be_able_to_use_original_filename()
_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 @@ public void should_be_able_to_use_original_filename_only()
_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 @@ public void should_use_Readarr_as_release_group_when_not_available()
_trackFile.ReleaseGroup = null;
_namingConfig.StandardBookFormat = "{Release Group}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be("Readarr");
}
@ -520,7 +527,7 @@ public void should_not_use_Readarr_as_release_group_if_pattern_has_separator(str
_trackFile.ReleaseGroup = null;
_namingConfig.StandardBookFormat = pattern;
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be(expectedFileName);
}
@ -532,7 +539,7 @@ public void should_use_existing_casing_for_release_group(string releaseGroup)
_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 @@ public class TitleTheFixture : CoreTest<FileNameBuilder>
{
private Author _artist;
private Book _album;
private Edition _edition;
private BookFile _trackFile;
private NamingConfig _namingConfig;
@ -32,6 +33,12 @@ public void Setup()
.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 @@ public void should_get_expected_title_back(string name, string expected)
_artist.Name = name;
_namingConfig.StandardBookFormat = "{Author NameThe}";
Subject.BuildBookFileName(_artist, _album, _trackFile)
Subject.BuildBookFileName(_artist, _edition, _trackFile)
.Should().Be(expected);
}
@ -75,7 +82,7 @@ public void should_not_change_title(string name)
_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 @@ public interface IAuthorStatisticsRepository
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 @@ public AuthorStatisticsRepository(IMainDatabase database)
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 @@ private List<BookStatistics> Query(SqlBuilder builder)
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 @@ public void RemoveFormats(int calibreId, IEnumerable<string> formats, CalibreSet
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 @@ public void SetFields(BookFile file, CalibreSettings settings)
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 @@ public AuthorMetadata()
}
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 @@ public override string ToString()
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 @@ public class Book : Entity<Book>
{
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 @@ public Book()
// 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 @@ public Book()
// 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 @@ public Book()
[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 @@ public override string ToString()
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 @@ public override void UseDbFieldsFrom(Book other)
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 @@ public override void UseDbFieldsFrom(Book other)
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 @@ public class Ratings : MemberwiseEquatable<Ratings>, IEmbeddedDocument
{
public int Votes { get; set; }
public decimal Value { get; set; }
public double Popularity => (double)Value * Votes;
}
}

View file

@ -70,7 +70,7 @@ public List<Book> GetBooksForRefresh(int authorMetadataId, IEnumerable<string> f
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 @@ public Book FindBySlug(string titleSlug)
#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 @@ public PagingSpec<Book> BooksWithoutFiles(PagingSpec<Book> pagingSpec)
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 @@ public Book FindByTitle(int authorMetadataId, string title)
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 @@ public interface IAddAuthorService
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 @@ public class AddArtistService : IAddAuthorService
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 @@ public Book AddBook(Book book, bool doRefresh = true)
{
_logger.Debug($"Adding book {book}");
book = AddSkyhookData(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 @@ private Book AddSkyhookData(Book newBook)
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 @@ public class BookService : IBookService,
IHandle<AuthorDeletedEvent>
{
private readonly IBookRepository _bookRepository;
private readonly IEditionService _editionService;
private readonly IEventAggregator _eventAggregator;
private readonly Logger _logger;
public BookService(IBookRepository bookRepository,
IEventAggregator eventAggregator,
Logger logger)
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 @@
namespace NzbDrone.Core.Books
{
public interface IRefreshAuthorService
{
}
public class RefreshAuthorService : RefreshEntityServiceBase<Author, Book>,
IRefreshAuthorService,
IExecute<RefreshAuthorCommand>,
IExecute<BulkRefreshAuthorCommand>
{
@ -76,11 +81,11 @@ public RefreshAuthorService(IProvideAuthorInfo authorInfo,
_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 @@ protected override void PublishRefreshCompleteEvent(Author entity)
{
// 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 @@ private void RefreshSelectedArtists(List<int> authorIds, bool isNew, CommandTrig
{
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 @@ public void Execute(RefreshAuthorCommand message)
{
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 @@ public interface IRefreshBookService
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;
@ -32,22 +36,26 @@ public class RefreshBookService : RefreshEntityServiceBase<Book, object>, IRefre
private readonly Logger _logger;
public RefreshBookService(IBookService bookService,
IAuthorService authorService,
IAddAuthorService addAuthorService,
IAuthorMetadataService authorMetadataService,
IProvideBookInfo bookInfo,
IMediaFileService mediaFileService,
IHistoryService historyService,
IEventAggregator eventAggregator,
ICheckIfBookShouldBeRefreshed checkIfBookShouldBeRefreshed,
IMapCoversToLocal mediaCoverService,
Logger logger)
IAuthorService authorService,
IAddAuthorService addAuthorService,
IEditionService editionService,
IAuthorMetadataService authorMetadataService,
IProvideBookInfo bookInfo,
IRefreshEditionService refreshEditionService,
IMediaFileService mediaFileService,
IHistoryService historyService,
IEventAggregator eventAggregator,
ICheckIfBookShouldBeRefreshed checkIfBookShouldBeRefreshed,
IMapCoversToLocal mediaCoverService,
Logger logger)
: base(logger, authorMetadataService)
{
_bookService = bookService;
_authorService = authorService;
_addAuthorService = addAuthorService;
_editionService = editionService;
_bookInfo = bookInfo;
_refreshEditionService = refreshEditionService;
_mediaFileService = mediaFileService;
_historyService = historyService;
_eventAggregator = eventAggregator;
@ -60,7 +68,7 @@ protected override RemoteData GetRemoteData(Book local, List<Book> remote, Autho
{
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 @@ protected override RemoteData GetRemoteData(Book local, List<Book> remote, Autho
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 @@ protected override UpdateResult MergeEntity(Book local, Book target, Book remote
// 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 @@ protected override void DeleteEntity(Book local, bool deleteFiles)
_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 @@ public bool RefreshSeriesInfo(int authorMetadataId, List<Series> remoteSeries, A
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 @@ protected override void MainDbUpgrade()
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 @@ protected override void MainDbUpgrade()
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 @@ protected override void MainDbUpgrade()
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 @@ public static void Map()
.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 @@ public static void Map()
.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 @@ public void Handle(MediaCoversUpdatedEvent message)
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 @@ protected TExtraFile ImportFile(Author author, BookFile bookFile, string path, b
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 @@ public override IEnumerable<ExtraFile> MoveFilesAfterRename(Author author, List<
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 @@ private MetadataFile ProcessBookMetadata(IMetadata consumer, Author author, Book
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 @@ public void Handle(BookFileDeletedEvent message)
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 @@ public void Handle(BookFileRenamedEvent message)
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 @@ public void Handle(BookFileRetaggedEvent message)
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 @@ public void Clean()
{
// 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 @@ private void MapAlbumReport(ImportListItemInfo report)
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 @@ public List<DownloadDecision> BookSearch(Book book, bool missingOnly, bool userI
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 @@ private bool SupportsSearch
}
}
private bool SupportsAudioSearch
private bool SupportsBookSearch
{
get
{
@ -67,7 +67,7 @@ public virtual IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria
{
var pageableRequests = new IndexerPageableRequestChain();
if (SupportsAudioSearch)
if (SupportsBookSearch)
{
AddBookPageableRequests(pageableRequests,
searchCriteria,
@ -78,12 +78,17 @@ public virtual IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria
{
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 @@ public virtual IndexerPageableRequestChain GetSearchRequests(AuthorSearchCriteri
{
var pageableRequests = new IndexerPageableRequestChain();
if (SupportsAudioSearch)
if (SupportsBookSearch)
{
AddBookPageableRequests(pageableRequests,
searchCriteria,
@ -122,7 +127,7 @@ private void AddBookPageableRequests(IndexerPageableRequestChain chain, SearchCr
{
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 @@ public void ConvertToLocalUrls(int entityId, MediaCoverEntity coverEntity, IEnum
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 @@ private string GetArtistCoverPath(int authorId)
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 @@ private void EnsureArtistCovers(Author author)
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 @@ public interface IAudioTagService
{
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 @@ public void WriteTags(BookFile trackfile, bool newDownload, bool force = false)
_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 @@ public void SyncTags(List<Book> books)
}
// 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 @@ public void SyncTags(List<Book> books)
{
// 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 @@ public List<RetagBookFilePreview> GetRetagPreviewsByAlbum(int bookId)
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 @@ private IEnumerable<RetagBookFilePreview> GetPreviews(List<BookFile> files)
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 @@ public class BookFile : ModelBase
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 @@ public BookFileMovingService(IBookService bookService,
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 @@ public BookFile MoveBookFile(BookFile bookFile, Author author)
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 @@ public BookFile MoveBookFile(BookFile bookFile, LocalBook localBook)
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 @@ private void EnsureTrackFolder(BookFile bookFile, LocalBook localBook, string fi
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 @@ public LocalBook Augment(LocalBook localTrack, bool otherFiles)
return localTrack;
}
public LocalAlbumRelease Augment(LocalAlbumRelease localAlbum)
public LocalEdition Augment(LocalEdition localAlbum)
{
foreach (var augmenter in _albumAugmenters)
{

View file

@ -9,7 +9,7 @@
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 @@ public AggregateFilenameInfo(Logger logger)
_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