New: Manually Edit/Override Album Release (#181)

* New: Manually Edit/Override Album Release

* !fixup for comments, loading all albums instead of only artist albums
* fixup! UI Cleanup lint issues
* fixup! Remove AddAlbum service for now, fix refresh override selected release
* fixup! Last one... to fix updating albums with custom release set

Closes #109 
Closes #129 
Closes #128
pull/190/head
Qstick 6 years ago committed by GitHub
parent 74f433d4f0
commit 26ef43f302
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -68,7 +68,7 @@ class BlacklistRow extends Component {
return ( return (
<TableRowCell key={name}> <TableRowCell key={name}>
<ArtistNameLink <ArtistNameLink
nameSlug={artist.nameSlug} foreignArtistId={artist.foreignArtistId}
artistName={artist.artistName} artistName={artist.artistName}
/> />
</TableRowCell> </TableRowCell>

@ -103,7 +103,7 @@ class HistoryRow extends Component {
return ( return (
<TableRowCell key={name}> <TableRowCell key={name}>
<ArtistNameLink <ArtistNameLink
nameSlug={artist.nameSlug} foreignArtistId={artist.foreignArtistId}
artistName={artist.artistName} artistName={artist.artistName}
/> />
</TableRowCell> </TableRowCell>

@ -134,7 +134,7 @@ class QueueRow extends Component {
return ( return (
<TableRowCell key={name}> <TableRowCell key={name}>
<ArtistNameLink <ArtistNameLink
nameSlug={artist.nameSlug} foreignArtistId={artist.foreignArtistId}
artistName={artist.artistName} artistName={artist.artistName}
/> />
</TableRowCell> </TableRowCell>
@ -145,7 +145,7 @@ class QueueRow extends Component {
return ( return (
<TableRowCell key={name}> <TableRowCell key={name}>
<ArtistNameLink <ArtistNameLink
nameSlug={artist.nameSlug} foreignArtistId={artist.foreignArtistId}
artistName={artist.artistName} artistName={artist.artistName}
/> />
</TableRowCell> </TableRowCell>

@ -99,7 +99,7 @@ class AddNewArtist extends Component {
className={styles.searchInput} className={styles.searchInput}
name="artistLookup" name="artistLookup"
value={term} value={term}
placeholder="eg. Breaking Benjamin, lidarr:####" placeholder="eg. Breaking Benjamin, lidarr:854a1807-025b-42a8-ba8c-2a39717f1d25"
onChange={this.onSearchInputChange} onChange={this.onSearchInputChange}
/> />
@ -144,7 +144,7 @@ class AddNewArtist extends Component {
!isFetching && !error && !items.length && !!term && !isFetching && !error && !items.length && !!term &&
<div className={styles.message}> <div className={styles.message}>
<div className={styles.noResults}>Couldn't find any results for '{term}'</div> <div className={styles.noResults}>Couldn't find any results for '{term}'</div>
<div>You can also search using MusicBrainz ID of a show. eg. lidarr:cc197bad-dc9c-440d-a5b5-d52ba2e14234</div> <div>You can also search using MusicBrainz ID of an artist. eg. lidarr:cc197bad-dc9c-440d-a5b5-d52ba2e14234</div>
<div> <div>
<Link to="https://github.com/Lidarr/Lidarr/wiki/FAQ#why-cant-i-add-a-new-artist-when-i-know-the-musicbrainz-id"> <Link to="https://github.com/Lidarr/Lidarr/wiki/FAQ#why-cant-i-add-a-new-artist-when-i-know-the-musicbrainz-id">
Why can't I find my artist? Why can't I find my artist?
@ -157,7 +157,7 @@ class AddNewArtist extends Component {
!term && !term &&
<div className={styles.message}> <div className={styles.message}>
<div className={styles.helpText}>It's easy to add a new artist, just start typing the name the artist you want to add.</div> <div className={styles.helpText}>It's easy to add a new artist, just start typing the name the artist you want to add.</div>
<div>You can also search using MusicBrainz ID of a show. eg. lidarr:cc197bad-dc9c-440d-a5b5-d52ba2e14234</div> <div>You can also search using MusicBrainz ID of an artist. eg. lidarr:cc197bad-dc9c-440d-a5b5-d52ba2e14234</div>
</div> </div>
} }

@ -0,0 +1,168 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import LazyLoad from 'react-lazyload';
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=';
function findCover(images) {
return _.find(images, { coverType: 'cover' });
}
function getCoverUrl(cover, size) {
if (cover) {
if (cover.url.contains('lastWrite=') || (/^https?:/).test(cover.url)) {
// Remove protocol
let url = cover.url.replace(/^https?:/, '');
url = url.replace('cover.jpg', `cover-${size}.jpg`);
return url;
}
}
}
class AlbumCover extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
const pixelRatio = Math.floor(window.devicePixelRatio);
const {
images,
size
} = props;
const cover = findCover(images);
this.state = {
pixelRatio,
cover,
coverUrl: getCoverUrl(cover, pixelRatio * size),
isLoaded: false,
hasError: false
};
}
componentDidUpdate(prevProps) {
const {
images,
size
} = this.props;
const {
cover,
pixelRatio
} = this.state;
const nextcover = findCover(images);
if (nextcover && (!cover || nextcover.url !== cover.url)) {
this.setState({
cover: nextcover,
coverUrl: getCoverUrl(nextcover, pixelRatio * size),
hasError: false
// Don't reset isLoaded, as we want to immediately try to
// show the new image, whether an image was shown previously
// or the placeholder was shown.
});
}
}
//
// Listeners
onError = () => {
this.setState({ hasError: true });
}
onLoad = () => {
this.setState({
isLoaded: true,
hasError: false
});
}
//
// Render
render() {
const {
className,
style,
size,
lazy,
overflow
} = this.props;
const {
coverUrl,
hasError,
isLoaded
} = this.state;
if (hasError || !coverUrl) {
return (
<img
className={className}
style={style}
src={coverPlaceholder}
/>
);
}
if (lazy) {
return (
<LazyLoad
height={size}
offset={100}
overflow={overflow}
placeholder={
<img
className={className}
style={style}
src={coverPlaceholder}
/>
}
>
<img
className={className}
style={style}
src={coverUrl}
onError={this.onError}
/>
</LazyLoad>
);
}
return (
<img
className={className}
style={style}
src={isLoaded ? coverUrl : coverPlaceholder}
onError={this.onError}
onLoad={this.onLoad}
/>
);
}
}
AlbumCover.propTypes = {
className: PropTypes.string,
style: PropTypes.object,
images: PropTypes.arrayOf(PropTypes.object).isRequired,
size: PropTypes.number.isRequired,
lazy: PropTypes.bool.isRequired,
overflow: PropTypes.bool.isRequired
};
AlbumCover.defaultProps = {
size: 250,
lazy: true,
overflow: false
};
export default AlbumCover;

@ -37,7 +37,7 @@
margin-top: 20px; margin-top: 20px;
} }
.openSeriesButton { .openArtistButton {
composes: button from 'Components/Link/Button.css'; composes: button from 'Components/Link/Button.css';
margin-right: auto; margin-right: auto;

@ -8,13 +8,11 @@ import ModalHeader from 'Components/Modal/ModalHeader';
import ModalBody from 'Components/Modal/ModalBody'; import ModalBody from 'Components/Modal/ModalBody';
import ModalFooter from 'Components/Modal/ModalFooter'; import ModalFooter from 'Components/Modal/ModalFooter';
import MonitorToggleButton from 'Components/MonitorToggleButton'; import MonitorToggleButton from 'Components/MonitorToggleButton';
import AlbumSummaryConnector from './Summary/AlbumSummaryConnector';
import AlbumHistoryConnector from './History/AlbumHistoryConnector'; import AlbumHistoryConnector from './History/AlbumHistoryConnector';
import AlbumSearchConnector from './Search/AlbumSearchConnector'; import AlbumSearchConnector from './Search/AlbumSearchConnector';
import styles from './AlbumDetailsModalContent.css'; import styles from './AlbumDetailsModalContent.css';
const tabs = [ const tabs = [
'details',
'history', 'history',
'search' 'search'
]; ];
@ -45,14 +43,10 @@ class AlbumDetailsModalContent extends Component {
render() { render() {
const { const {
albumId, albumId,
albumEntity,
artistId,
artistName, artistName,
nameSlug, foreignArtistId,
albumLabel,
artistMonitored, artistMonitored,
albumTitle, albumTitle,
releaseDate,
monitored, monitored,
isSaving, isSaving,
showOpenArtistButton, showOpenArtistButton,
@ -61,7 +55,7 @@ class AlbumDetailsModalContent extends Component {
onModalClose onModalClose
} = this.props; } = this.props;
const artistLink = `/artist/${nameSlug}`; const artistLink = `/artist/${foreignArtistId}`;
return ( return (
<ModalContent <ModalContent
@ -96,13 +90,6 @@ class AlbumDetailsModalContent extends Component {
<TabList <TabList
className={styles.tabList} className={styles.tabList}
> >
<Tab
className={styles.tab}
selectedClassName={styles.selectedTab}
>
Details
</Tab>
<Tab <Tab
className={styles.tab} className={styles.tab}
selectedClassName={styles.selectedTab} selectedClassName={styles.selectedTab}
@ -118,16 +105,6 @@ class AlbumDetailsModalContent extends Component {
</Tab> </Tab>
</TabList> </TabList>
<TabPanel className={styles.tabPanel}>
<AlbumSummaryConnector
albumId={albumId}
albumEntity={albumEntity}
releaseDate={releaseDate}
albumLabel={albumLabel}
artistId={artistId}
/>
</TabPanel>
<TabPanel className={styles.tabPanel}> <TabPanel className={styles.tabPanel}>
<AlbumHistoryConnector <AlbumHistoryConnector
albumId={albumId} albumId={albumId}
@ -148,7 +125,7 @@ class AlbumDetailsModalContent extends Component {
{ {
showOpenArtistButton && showOpenArtistButton &&
<Button <Button
className={styles.openSeriesButton} className={styles.openArtistButton}
to={artistLink} to={artistLink}
onPress={onModalClose} onPress={onModalClose}
> >
@ -172,7 +149,7 @@ AlbumDetailsModalContent.propTypes = {
albumEntity: PropTypes.string.isRequired, albumEntity: PropTypes.string.isRequired,
artistId: PropTypes.number.isRequired, artistId: PropTypes.number.isRequired,
artistName: PropTypes.string.isRequired, artistName: PropTypes.string.isRequired,
nameSlug: PropTypes.string.isRequired, foreignArtistId: PropTypes.string.isRequired,
artistMonitored: PropTypes.bool.isRequired, artistMonitored: PropTypes.bool.isRequired,
releaseDate: PropTypes.string.isRequired, releaseDate: PropTypes.string.isRequired,
albumLabel: PropTypes.arrayOf(PropTypes.string).isRequired, albumLabel: PropTypes.arrayOf(PropTypes.string).isRequired,
@ -187,7 +164,7 @@ AlbumDetailsModalContent.propTypes = {
}; };
AlbumDetailsModalContent.defaultProps = { AlbumDetailsModalContent.defaultProps = {
selectedTab: 'details', selectedTab: 'history',
albumLabel: ['Unknown'], albumLabel: ['Unknown'],
albumEntity: albumEntities.ALBUMS, albumEntity: albumEntities.ALBUMS,
startInteractiveSearch: false startInteractiveSearch: false

@ -17,13 +17,13 @@ function createMapStateToProps() {
(album, artist) => { (album, artist) => {
const { const {
artistName, artistName,
nameSlug, foreignArtistId,
monitored: artistMonitored monitored: artistMonitored
} = artist; } = artist;
return { return {
artistName, artistName,
nameSlug, foreignArtistId,
artistMonitored, artistMonitored,
...album ...album
}; };

@ -5,6 +5,7 @@ import IconButton from 'Components/Link/IconButton';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell';
import AlbumDetailsModal from './AlbumDetailsModal'; import AlbumDetailsModal from './AlbumDetailsModal';
import EditAlbumModalConnector from './Edit/EditAlbumModalConnector';
import styles from './AlbumSearchCell.css'; import styles from './AlbumSearchCell.css';
class AlbumSearchCell extends Component { class AlbumSearchCell extends Component {
@ -16,7 +17,8 @@ class AlbumSearchCell extends Component {
super(props, context); super(props, context);
this.state = { this.state = {
isDetailsModalOpen: false isDetailsModalOpen: false,
isEditAlbumModalOpen: false
}; };
} }
@ -31,6 +33,14 @@ class AlbumSearchCell extends Component {
this.setState({ isDetailsModalOpen: false }); this.setState({ isDetailsModalOpen: false });
} }
onEditAlbumPress = () => {
this.setState({ isEditAlbumModalOpen: true });
}
onEditAlbumModalClose = () => {
this.setState({ isEditAlbumModalOpen: false });
}
// //
// Render // Render
@ -57,6 +67,12 @@ class AlbumSearchCell extends Component {
onPress={this.onManualSearchPress} onPress={this.onManualSearchPress}
/> />
<IconButton
name={icons.EDIT}
title="Edit Album"
onPress={this.onEditAlbumPress}
/>
<AlbumDetailsModal <AlbumDetailsModal
isOpen={this.state.isDetailsModalOpen} isOpen={this.state.isDetailsModalOpen}
albumId={albumId} albumId={albumId}
@ -67,6 +83,13 @@ class AlbumSearchCell extends Component {
onModalClose={this.onDetailsModalClose} onModalClose={this.onDetailsModalClose}
{...otherProps} {...otherProps}
/> />
<EditAlbumModalConnector
isOpen={this.state.isEditAlbumModalOpen}
albumId={albumId}
artistId={artistId}
onModalClose={this.onEditAlbumModalClose}
/>
</TableRowCell> </TableRowCell>
); );
} }

@ -0,0 +1,20 @@
import PropTypes from 'prop-types';
import React from 'react';
import Link from 'Components/Link/Link';
function AlbumTitleDetailLink({ foreignAlbumId, title }) {
const link = `/album/${foreignAlbumId}`;
return (
<Link to={link}>
{title}
</Link>
);
}
AlbumTitleDetailLink.propTypes = {
foreignAlbumId: PropTypes.string.isRequired,
title: PropTypes.string.isRequired
};
export default AlbumTitleDetailLink;

@ -0,0 +1,137 @@
.innerContentBody {
padding: 0;
}
.header {
position: relative;
width: 100%;
height: 310px;
}
.backdrop {
position: absolute;
z-index: -1;
width: 100%;
height: 100%;
background-size: cover;
}
.backdropOverlay {
position: absolute;
width: 100%;
height: 100%;
background: $black;
opacity: 0.7;
}
.headerContent {
display: flex;
padding: 30px;
width: 100%;
height: 100%;
color: $white;
}
.logo {
flex-shrink: 0;
margin-right: 35px;
width: 250px;
height: 97px;
}
.cover {
flex-shrink: 0;
margin-right: 35px;
width: 250px;
height: 250px;
}
.info {
display: flex;
flex-direction: column;
flex-grow: 1;
overflow: hidden;
}
.titleRow {
display: flex;
justify-content: space-between;
flex: 0 0 auto;
}
.titleContainer {
display: flex;
justify-content: space-between;
}
.title {
margin-bottom: 5px;
font-weight: 300;
font-size: 50px;
line-height: 50px;
}
.alternateTitlesIconContainer {
margin-left: 20px;
line-height: 50px;
}
.artistNavigationButtons {
white-space: no-wrap;
}
.artistNavigationButton {
composes: button from 'Components/Link/IconButton.css';
margin-left: 5px;
color: #e1e2e3;
white-space: nowrap;
}
.details {
font-weight: 300;
font-size: 20px;
}
.runtime {
margin-right: 15px;
}
.detailsLabel {
composes: label from 'Components/Label.css';
margin: 5px 10px 5px 0;
}
.sizeOnDisk,
.qualityProfileName,
.tags {
margin-left: 8px;
font-weight: 300;
font-size: 17px;
}
.overview {
flex: 1 0 auto;
min-height: 0;
}
.contentContainer {
padding: 20px;
}
@media only screen and (max-width: $breakpointSmall) {
.contentContainer {
padding: 20px 0;
}
.headerContent {
padding: 15px;
}
}
@media only screen and (max-width: $breakpointLarge) {
.cover {
display: none;
}
}

@ -0,0 +1,422 @@
import _ from 'lodash';
import moment from 'moment';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import formatBytes from 'Utilities/Number/formatBytes';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import { align, icons, sizes } from 'Helpers/Props';
import HeartRating from 'Components/HeartRating';
import Icon from 'Components/Icon';
import IconButton from 'Components/Link/IconButton';
import Label from 'Components/Label';
import AlbumCover from 'Album/AlbumCover';
import EditAlbumModalConnector from 'Album/Edit/EditAlbumModalConnector';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import AlbumDetailsMediumConnector from './AlbumDetailsMediumConnector';
import ArtistHistoryModal from 'Artist/History/ArtistHistoryModal';
import TrackFileEditorModal from 'TrackFile/Editor/TrackFileEditorModal';
import styles from './AlbumDetails.css';
function getFanartUrl(images) {
const fanartImage = _.find(images, { coverType: 'fanart' });
if (fanartImage) {
// Remove protocol
return fanartImage.url.replace(/^https?:/, '');
}
}
function getExpandedState(newState) {
return {
allExpanded: newState.allSelected,
allCollapsed: newState.allUnselected,
expandedState: newState.selectedState
};
}
class AlbumDetails extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isArtistHistoryModalOpen: false,
isManageTracksOpen: false,
isEditAlbumModalOpen: false,
allExpanded: false,
allCollapsed: false,
expandedState: {}
};
}
//
// Listeners
onEditAlbumPress = () => {
this.setState({ isEditAlbumModalOpen: true });
}
onEditAlbumModalClose = () => {
this.setState({ isEditAlbumModalOpen: false });
}
onManageTracksPress = () => {
this.setState({ isManageTracksOpen: true });
}
onManageTracksModalClose = () => {
this.setState({ isManageTracksOpen: false });
}
onArtistHistoryPress = () => {
this.setState({ isArtistHistoryModalOpen: true });
}
onArtistHistoryModalClose = () => {
this.setState({ isArtistHistoryModalOpen: false });
}
onExpandAllPress = () => {
const {
allExpanded,
expandedState
} = this.state;
this.setState(getExpandedState(selectAll(expandedState, !allExpanded)));
}
onExpandPress = (albumId, isExpanded) => {
this.setState((state) => {
const convertedState = {
allSelected: state.allExpanded,
allUnselected: state.allCollapsed,
selectedState: state.expandedState
};
const newState = toggleSelected(convertedState, [], albumId, isExpanded, false);
return getExpandedState(newState);
});
}
//
// Render
render() {
const {
id,
title,
albumType,
statistics,
monitored,
releaseDate,
images,
media,
isFetching,
isPopulated,
albumsError,
trackFilesError,
shortDateFormat,
artist,
previousAlbum,
nextAlbum,
isSearching,
onSearchPress
} = this.props;
const {
isArtistHistoryModalOpen,
isEditAlbumModalOpen,
isManageTracksOpen,
allExpanded,
allCollapsed,
expandedState
} = this.state;
let expandIcon = icons.EXPAND_INDETERMINATE;
if (allExpanded) {
expandIcon = icons.COLLAPSE;
} else if (allCollapsed) {
expandIcon = icons.EXPAND;
}
return (
<PageContent title={title}>
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label="Search Album"
iconName={icons.SEARCH}
isSpinning={isSearching}
onPress={onSearchPress}
/>
<PageToolbarSeparator />
<PageToolbarButton
label="Manage Tracks"
iconName={icons.TRACK_FILE}
onPress={this.onManageTracksPress}
/>
<PageToolbarButton
label="History"
iconName={icons.HISTORY}
onPress={this.onArtistHistoryPress}
/>
<PageToolbarSeparator />
<PageToolbarButton
label="Edit"
iconName={icons.EDIT}
onPress={this.onEditAlbumPress}
/>
</PageToolbarSection>
<PageToolbarSection alignContent={align.RIGHT}>
<PageToolbarButton
label={allExpanded ? 'Collapse All' : 'Expand All'}
iconName={expandIcon}
onPress={this.onExpandAllPress}
/>
</PageToolbarSection>
</PageToolbar>
<PageContentBodyConnector innerClassName={styles.innerContentBody}>
<div className={styles.header}>
<div
className={styles.backdrop}
style={{
backgroundImage: `url(${getFanartUrl(images)})`
}}
>
<div className={styles.backdropOverlay} />
</div>
<div className={styles.headerContent}>
<AlbumCover
className={styles.cover}
images={images}
size={500}
lazy={false}
/>
<div className={styles.info}>
<div className={styles.titleContainer}>
<div className={styles.title}>
{title}
</div>
<div className={styles.artistNavigationButtons}>
<IconButton
className={styles.artistNavigationButton}
name={icons.ARROW_LEFT}
size={30}
title={`Go to ${previousAlbum.title}`}
to={`/album/${previousAlbum.foreignAlbumId}`}
/>
<IconButton
className={styles.artistNavigationButton}
name={icons.ARROW_UP}
size={30}
title={`Go to ${artist.artistName}`}
to={`/artist/${artist.foreignArtistId}`}
/>
<IconButton
className={styles.artistNavigationButton}
name={icons.ARROW_RIGHT}
size={30}
title={`Go to ${nextAlbum.title}`}
to={`/album/${nextAlbum.foreignAlbumId}`}
/>
</div>
</div>
{/* <div className={styles.details}>
<div>
<HeartRating
rating={ratings.value}
iconSize={20}
/>
</div>
</div> */}
<div className={styles.detailsLabels}>
<Label
className={styles.detailsLabel}
size={sizes.LARGE}
>
<Icon
name={icons.CALENDAR}
size={17}
/>
<span className={styles.sizeOnDisk}>
{
moment(releaseDate).format(shortDateFormat)
}
</span>
</Label>
<Label
className={styles.detailsLabel}
size={sizes.LARGE}
>
<Icon
name={icons.DRIVE}
size={17}
/>
<span className={styles.sizeOnDisk}>
{
formatBytes(statistics.sizeOnDisk)
}
</span>
</Label>
<Label
className={styles.detailsLabel}
size={sizes.LARGE}
>
<Icon
name={monitored ? icons.MONITORED : icons.UNMONITORED}
size={17}
/>
<span className={styles.qualityProfileName}>
{monitored ? 'Monitored' : 'Unmonitored'}
</span>
</Label>
{
!!albumType &&
<Label
className={styles.detailsLabel}
title="Type"
size={sizes.LARGE}
>
<Icon
name={icons.INFO}
size={17}
/>
<span className={styles.qualityProfileName}>
{albumType}
</span>
</Label>
}
</div>
</div>
</div>
</div>
<div className={styles.contentContainer}>
{
!isPopulated && !albumsError && !trackFilesError &&
<LoadingIndicator />
}
{
!isFetching && albumsError &&
<div>Loading albums failed</div>
}
{
!isFetching && trackFilesError &&
<div>Loading track files failed</div>
}
{
isPopulated && !!media.length &&
<div>
{
media.slice(0).map((medium) => {
return (
<AlbumDetailsMediumConnector
key={medium.mediumNumber}
albumId={id}
{...medium}
isExpanded={expandedState[medium.mediumNumber]}
onExpandPress={this.onExpandPress}
/>
);
})
}
</div>
}
</div>
<TrackFileEditorModal
isOpen={isManageTracksOpen}
artistId={artist.id}
albumId={id}
onModalClose={this.onManageTracksModalClose}
/>
<ArtistHistoryModal
isOpen={isArtistHistoryModalOpen}
artistId={artist.id}
albumId={id}
onModalClose={this.onArtistHistoryModalClose}
/>
<EditAlbumModalConnector
isOpen={isEditAlbumModalOpen}
albumId={id}
artistId={artist.id}
onModalClose={this.onEditAlbumModalClose}
/>
</PageContentBodyConnector>
</PageContent>
);
}
}
AlbumDetails.propTypes = {
id: PropTypes.number.isRequired,
foreignAlbumId: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
albumType: PropTypes.string.isRequired,
statistics: PropTypes.object.isRequired,
releaseDate: PropTypes.string.isRequired,
images: PropTypes.arrayOf(PropTypes.object).isRequired,
media: PropTypes.arrayOf(PropTypes.object).isRequired,
monitored: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
isSearching: PropTypes.bool,
isFetching: PropTypes.bool,
isPopulated: PropTypes.bool,
albumsError: PropTypes.object,
tracksError: PropTypes.object,
trackFilesError: PropTypes.object,
artist: PropTypes.object,
previousAlbum: PropTypes.object,
nextAlbum: PropTypes.object,
onRefreshPress: PropTypes.func,
onSearchPress: PropTypes.func.isRequired
};
AlbumDetails.defaultProps = {
isSaving: false
};
export default AlbumDetails;

@ -0,0 +1,146 @@
/* eslint max-params: 0 */
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { findCommand } from 'Utilities/Command';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
import { fetchTracks, clearTracks } from 'Store/Actions/trackActions';
import { fetchTrackFiles, clearTrackFiles } from 'Store/Actions/trackFileActions';
import { executeCommand } from 'Store/Actions/commandActions';
import * as commandNames from 'Commands/commandNames';
import AlbumDetails from './AlbumDetails';
import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
function createMapStateToProps() {
return createSelector(
(state, { foreignAlbumId }) => foreignAlbumId,
(state) => state.tracks,
(state) => state.trackFiles,
(state) => state.albums,
createAllArtistSelector(),
createCommandsSelector(),
createUISettingsSelector(),
(foreignAlbumId, tracks, trackFiles, albums, artists, commands, uiSettings) => {
const sortedAlbums = _.orderBy(albums.items, 'releaseDate');
const albumIndex = _.findIndex(sortedAlbums, { foreignAlbumId });
const album = sortedAlbums[albumIndex];
const artist = _.find(artists, { id: album.artistId });
if (!album) {
return {};
}
const previousAlbum = sortedAlbums[albumIndex - 1] || _.last(sortedAlbums);
const nextAlbum = sortedAlbums[albumIndex + 1] || _.first(sortedAlbums);
const isSearching = !!findCommand(commands, { name: commandNames.ALBUM_SEARCH });
const isFetching = tracks.isFetching || trackFiles.isFetching;
const isPopulated = tracks.isPopulated && trackFiles.isPopulated;
const tracksError = tracks.error;
const trackFilesError = trackFiles.error;
return {
...album,
shortDateFormat: uiSettings.shortDateFormat,
artist,
isSearching,
isFetching,
isPopulated,
tracksError,
trackFilesError,
previousAlbum,
nextAlbum
};
}
);
}
const mapDispatchToProps = {
executeCommand,
fetchTracks,
clearTracks,
fetchTrackFiles,
clearTrackFiles
};
class AlbumDetailsConnector extends Component {
componentDidMount() {
registerPagePopulator(this.populate);
this.populate();
}
componentDidUpdate(prevProps) {
const {
id
} = this.props;
// If the id has changed we need to clear the tracks/track
// files and fetch from the server.
if (prevProps.id !== id) {
this.unpopulate();
this.populate();
}
}
componentWillUnmount() {
unregisterPagePopulator(this.populate);
this.unpopulate();
}
//
// Control
populate = () => {
const albumId = this.props.id;
this.props.fetchTracks({ albumId });
this.props.fetchTrackFiles({ albumId });
}
unpopulate = () => {
this.props.clearTracks();
this.props.clearTrackFiles();
}
//
// Listeners
onSearchPress = () => {
this.props.executeCommand({
name: commandNames.ALBUM_SEARCH,
albumIds: [this.props.id]
});
}
//
// Render
render() {
return (
<AlbumDetails
{...this.props}
onSearchPress={this.onSearchPress}
/>
);
}
}
AlbumDetailsConnector.propTypes = {
id: PropTypes.number,
isAlbumFetching: PropTypes.bool,
isAlbumPopulated: PropTypes.bool,
foreignAlbumId: PropTypes.string.isRequired,
fetchTracks: PropTypes.func.isRequired,
clearTracks: PropTypes.func.isRequired,
fetchTrackFiles: PropTypes.func.isRequired,
clearTrackFiles: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(AlbumDetailsConnector);

@ -0,0 +1,114 @@
.medium {
margin-bottom: 20px;
border: 1px solid $borderColor;
border-radius: 4px;
background-color: $white;
&:last-of-type {
margin-bottom: 0;
}
}
.header {
position: relative;
display: flex;
align-items: center;
width: 100%;
font-size: 24px;
}
.mediumNumber {
margin-right: 10px;
margin-left: 5px;
}
.mediumFormat {
color: #8895aa;
font-style: italic;
font-size: 18px;
}
.expandButton {
composes: link from 'Components/Link/Link.css';
flex-grow: 1;
margin: 0 20px;
text-align: center;
}
.left {
display: flex;
align-items: center;
flex: 0 1 300px;
}
.left,
.actions {
padding: 15px 10px;
}
.actionsMenu {
composes: menu from 'Components/Menu/Menu.css';
flex: 0 0 45px;
}
.actionsMenuContent {
composes: menuContent from 'Components/Menu/MenuContent.css';
white-space: nowrap;
font-size: 14px;
}
.actionMenuIcon {
margin-right: 8px;
}
.actionButton {
composes: button from 'Components/Link/IconButton.css';
width: 30px;
}
.tracks {
padding-top: 15px;
border-top: 1px solid $borderColor;
}
.collapseButtonContainer {
padding: 10px 15px;
width: 100%;
border-top: 1px solid $borderColor;
border-bottom-right-radius: 4px;
border-bottom-left-radius: 4px;
background-color: #fafafa;
text-align: center;
}
.expandButtonIcon {
composes: actionButton;
position: absolute;
top: 50%;
left: 50%;
margin-top: -12px;
margin-left: -15px;
}
.noTracks {
margin-bottom: 15px;
text-align: center;
}
@media only screen and (max-width: $breakpointSmall) {
.medium {
border-right: 0;
border-left: 0;
border-radius: 0;
}
.expandButtonIcon {
position: static;
margin: 0;
}
}

@ -0,0 +1,211 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import isAfter from 'Utilities/Date/isAfter';
import isBefore from 'Utilities/Date/isBefore';
import { icons, kinds, sizes } from 'Helpers/Props';
import Icon from 'Components/Icon';
import IconButton from 'Components/Link/IconButton';
import Label from 'Components/Label';
import Link from 'Components/Link/Link';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import TrackRowConnector from './TrackRowConnector';
import styles from './AlbumDetailsMedium.css';
function getMediumStatistics(tracks) {
let trackCount = 0;
let trackFileCount = 0;
let totalTrackCount = 0;
tracks.forEach((track) => {
if (track.trackFileId) {
trackCount++;
trackFileCount++;
}
totalTrackCount++;
});
return {
trackCount,
trackFileCount,
totalTrackCount
};
}
function getTrackCountKind(trackFileCount, trackCount) {
if (trackFileCount === trackCount && trackCount > 0) {
return kinds.SUCCESS;
}
return kinds.DANGER;
}
class AlbumDetailsMedium extends Component {
//
// Lifecycle
componentDidMount() {
this._expandByDefault();
}
componentDidUpdate(prevProps) {
if (prevProps.albumId !== this.props.albumId) {
this._expandByDefault();
}
}
//
// Control
_expandByDefault() {
const {
mediumNumber,
onExpandPress,
items
} = this.props;
const expand = _.some(items, (item) => {
return isAfter(item.airDateUtc) ||
isAfter(item.airDateUtc, { days: -30 });
});
onExpandPress(mediumNumber, expand && mediumNumber > 0);
}
//
// Listeners
onExpandPress = () => {
const {
mediumNumber,
isExpanded
} = this.props;
this.props.onExpandPress(mediumNumber, !isExpanded);
}
//
// Render
render() {
const {
mediumNumber,
mediumFormat,
items,
columns,
onTableOptionChange,
isExpanded,
isSmallScreen
} = this.props;
const {
trackCount,
trackFileCount,
totalTrackCount
} = getMediumStatistics(items);
return (
<div
className={styles.medium}
>
<div className={styles.header}>
<div className={styles.left}>
{
<div>
<span className={styles.mediumNumber}>
{mediumFormat} {mediumNumber}
</span>
</div>
}
<Label
title={`${totalTrackCount} tracks total. ${trackFileCount} tracks with files.`}
kind={getTrackCountKind(trackFileCount, trackCount)}
size={sizes.LARGE}
>
{
<span>{totalTrackCount} / {trackCount}</span>
}
</Label>
</div>
<Link
className={styles.expandButton}
onPress={this.onExpandPress}
>
<Icon
className={styles.expandButtonIcon}
name={isExpanded ? icons.COLLAPSE : icons.EXPAND}
title={isExpanded ? 'Hide tracks' : 'Show tracks'}
size={24}
/>
{
!isSmallScreen &&
<span>&nbsp;</span>
}
</Link>
</div>
<div>
{
isExpanded &&
<div className={styles.tracks}>
{
items.length ?
<Table
columns={columns}
onTableOptionChange={onTableOptionChange}
>
<TableBody>
{
items.map((item) => {
return (
<TrackRowConnector
key={item.id}
columns={columns}
{...item}
/>
);
})
}
</TableBody>
</Table> :
<div className={styles.noTracks}>
No tracks in this medium
</div>
}
<div className={styles.collapseButtonContainer}>
<IconButton
name={icons.COLLAPSE}
size={20}
title="Hide tracks"
onPress={this.onExpandPress}
/>
</div>
</div>
}
</div>
</div>
);
}
}
AlbumDetailsMedium.propTypes = {
albumId: PropTypes.number.isRequired,
mediumNumber: PropTypes.number.isRequired,
mediumFormat: PropTypes.string.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
isSaving: PropTypes.bool,
isExpanded: PropTypes.bool,
isSmallScreen: PropTypes.bool.isRequired,
onTableOptionChange: PropTypes.func.isRequired,
onExpandPress: PropTypes.func.isRequired
};
export default AlbumDetailsMedium;

@ -0,0 +1,64 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import { setTracksTableOption } from 'Store/Actions/trackActions';
import { executeCommand } from 'Store/Actions/commandActions';
import AlbumDetailsMedium from './AlbumDetailsMedium';
function createMapStateToProps() {
return createSelector(
(state, { mediumNumber }) => mediumNumber,
(state) => state.tracks,
createDimensionsSelector(),
(mediumNumber, tracks, dimensions) => {
const tracksInMedium = _.filter(tracks.items, { mediumNumber });
const sortedTracks = _.orderBy(tracksInMedium, ['absoluteTrackNumber'], ['asc']);
return {
items: sortedTracks,
columns: tracks.columns,
isSmallScreen: dimensions.isSmallScreen
};
}
);
}
const mapDispatchToProps = {
setTracksTableOption,
executeCommand
};
class AlbumDetailsMediumConnector extends Component {
//
// Listeners
onTableOptionChange = (payload) => {
this.props.setTracksTableOption(payload);
}
//
// Render
render() {
return (
<AlbumDetailsMedium
{...this.props}
onTableOptionChange={this.onTableOptionChange}
/>
);
}
}
AlbumDetailsMediumConnector.propTypes = {
albumId: PropTypes.number.isRequired,
mediumNumber: PropTypes.number.isRequired,
setTracksTableOption: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(AlbumDetailsMediumConnector);

@ -0,0 +1,110 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { push } from 'react-router-redux';
import NotFound from 'Components/NotFound';
import { fetchAlbums, clearAlbums } from 'Store/Actions/albumActions';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import AlbumDetailsConnector from './AlbumDetailsConnector';
function createMapStateToProps() {
return createSelector(
(state, { match }) => match,
(state) => state.albums,
(match, albums) => {
const foreignAlbumId = match.params.foreignAlbumId;
const isAlbumsFetching = albums.isFetching;
const isAlbumsPopulated = albums.isPopulated;
return {
foreignAlbumId,
isAlbumsFetching,
isAlbumsPopulated
};
}
);
}
const mapDispatchToProps = {
push,
fetchAlbums,
clearAlbums
};
class AlbumDetailsPageConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.populate();
}
componentWillUnmount() {
this.unpopulate();
}
//
// Control
populate = () => {
const foreignAlbumId = this.props.foreignAlbumId;
this.props.fetchAlbums({ foreignAlbumId });
}
unpopulate = () => {
this.props.clearAlbums();
}
//
// Render
render() {
const {
foreignAlbumId,
isAlbumsFetching,
isAlbumsPopulated
} = this.props;
if (!foreignAlbumId) {
return (
<NotFound
message="Sorry, that album cannot be found."
/>
);
}
if (isAlbumsFetching) {
return (
<LoadingIndicator />
);
}
if (!isAlbumsFetching && !isAlbumsPopulated) {
return (
<LoadingIndicator />
);
}
if (!isAlbumsFetching && isAlbumsPopulated) {
return (
<AlbumDetailsConnector
foreignAlbumId={foreignAlbumId}
/>
);
}
}
}
AlbumDetailsPageConnector.propTypes = {
foreignAlbumId: PropTypes.string,
match: PropTypes.shape({ params: PropTypes.shape({ foreignAlbumId: PropTypes.string.isRequired }).isRequired }).isRequired,
push: PropTypes.func.isRequired,
fetchAlbums: PropTypes.func.isRequired,
clearAlbums: PropTypes.func.isRequired,
isAlbumsFetching: PropTypes.bool.isRequired,
isAlbumsPopulated: PropTypes.bool.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(AlbumDetailsPageConnector);

@ -0,0 +1,31 @@
.title {
composes: cell from 'Components/Table/Cells/TableRowCell.css';
white-space: nowrap;
}
.monitored {
composes: cell from 'Components/Table/Cells/TableRowCell.css';
width: 42px;
}
.trackNumber {
composes: cell from 'Components/Table/Cells/TableRowCell.css';
width: 50px;
}
.audio {
composes: cell from 'Components/Table/Cells/TableRowCell.css';
width: 200px;
}
.language,
.duration,
.status {
composes: cell from 'Components/Table/Cells/TableRowCell.css';
width: 100px;
}

@ -0,0 +1,194 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import TableRow from 'Components/Table/TableRow';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
import EpisodeStatusConnector from 'Album/EpisodeStatusConnector';
import TrackFileLanguageConnector from 'TrackFile/TrackFileLanguageConnector';
import MediaInfoConnector from 'TrackFile/MediaInfoConnector';
import * as mediaInfoTypes from 'TrackFile/mediaInfoTypes';
import styles from './TrackRow.css';
class TrackRow extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isDetailsModalOpen: false
};
}
//
// Listeners
onDetailsModalClose = () => {
this.setState({ isDetailsModalOpen: false });
}
//
// Render
render() {
const {
id,
mediumNumber,
trackFileId,
absoluteTrackNumber,
title,
duration,
trackFilePath,
trackFileRelativePath,
columns
} = this.props;
return (
<TableRow>
{
columns.map((column) => {
const {
name,
isVisible
} = column;
if (!isVisible) {
return null;
}
if (name === 'medium') {
return (
<TableRowCell
key={name}
className={styles.trackNumber}
>
{mediumNumber}
</TableRowCell>
);
}
if (name === 'absoluteTrackNumber') {
return (
<TableRowCell
key={name}
className={styles.trackNumber}
>
{absoluteTrackNumber}
</TableRowCell>
);
}
if (name === 'title') {
return (
<TableRowCell
key={name}
className={styles.title}
>
{title}
</TableRowCell>
);
}
if (name === 'path') {
return (
<TableRowCell key={name}>
{
trackFilePath
}
</TableRowCell>
);
}
if (name === 'relativePath') {
return (
<TableRowCell key={name}>
{
trackFileRelativePath
}
</TableRowCell>
);
}
if (name === 'duration') {
return (
<TableRowCell
key={name}
className={styles.duration}
>
{
formatTimeSpan(duration)
}
</TableRowCell>
);
}
if (name === 'language') {
return (
<TableRowCell
key={name}
className={styles.language}
>
<TrackFileLanguageConnector
episodeFileId={trackFileId}
/>
</TableRowCell>
);
}
if (name === 'audioInfo') {
return (
<TableRowCell
key={name}
className={styles.audio}
>
<MediaInfoConnector
type={mediaInfoTypes.AUDIO}
trackFileId={trackFileId}
/>
</TableRowCell>
);
}
if (name === 'status') {
return (
<TableRowCell
key={name}
className={styles.status}
>
<EpisodeStatusConnector
albumId={id}
trackFileId={trackFileId}
/>
</TableRowCell>
);
}
return null;
})
}
</TableRow>
);
}
}
TrackRow.propTypes = {
id: PropTypes.number.isRequired,
albumId: PropTypes.number.isRequired,
trackFileId: PropTypes.number,
monitored: PropTypes.bool.isRequired,
mediumNumber: PropTypes.number.isRequired,
trackNumber: PropTypes.string.isRequired,
absoluteTrackNumber: PropTypes.number,
title: PropTypes.string.isRequired,
duration: PropTypes.number.isRequired,
isSaving: PropTypes.bool,
trackFilePath: PropTypes.string,
trackFileRelativePath: PropTypes.string,
mediaInfo: PropTypes.object,
columns: PropTypes.arrayOf(PropTypes.object).isRequired
};
export default TrackRow;

@ -0,0 +1,22 @@
/* eslint max-params: 0 */
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createTrackFileSelector from 'Store/Selectors/createTrackFileSelector';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
import TrackRow from './TrackRow';
function createMapStateToProps() {
return createSelector(
(state, { id }) => id,
(state, { mediumNumber }) => mediumNumber,
createTrackFileSelector(),
createCommandsSelector(),
(id, mediumNumber, trackFile, commands) => {
return {
trackFilePath: trackFile ? trackFile.path : null,
trackFileRelativePath: trackFile ? trackFile.relativePath : null
};
}
);
}
export default connect(createMapStateToProps)(TrackRow);

@ -0,0 +1,25 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import EditAlbumModalContentConnector from './EditAlbumModalContentConnector';
function EditAlbumModal({ isOpen, onModalClose, ...otherProps }) {
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<EditAlbumModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
EditAlbumModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default EditAlbumModal;

@ -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 EditAlbumModal from './EditAlbumModal';
const mapDispatchToProps = {
clearPendingChanges
};
class EditAlbumModalConnector extends Component {
//
// Listeners
onModalClose = () => {
this.props.clearPendingChanges({ section: 'albums' });
this.props.onModalClose();
}
//
// Render
render() {
return (
<EditAlbumModal
{...this.props}
onModalClose={this.onModalClose}
/>
);
}
}
EditAlbumModalConnector.propTypes = {
onModalClose: PropTypes.func.isRequired,
clearPendingChanges: PropTypes.func.isRequired
};
export default connect(undefined, mapDispatchToProps)(EditAlbumModalConnector);

@ -0,0 +1,119 @@
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 EditAlbumModalContent extends Component {
//
// Listeners
onSavePress = () => {
const {
onSavePress
} = this.props;
onSavePress(false);
}
//
// Render
render() {
const {
title,
artistName,
albumType,
item,
isSaving,
onInputChange,
onModalClose,
...otherProps
} = this.props;
const {
monitored,
currentRelease,
releases
} = item;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Edit - {artistName} - {title} [{albumType}]
</ModalHeader>
<ModalBody>
<Form
{...otherProps}
>
<FormGroup>
<FormLabel>Monitored</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="monitored"
helpText="Lidarr will search for and download album"
{...monitored}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel> Release</FormLabel>
<FormInputGroup
type={inputTypes.ALBUM_RELEASE_SELECT}
name="currentRelease"
helpText="Change release for this album"
albumReleases={releases}
selectedRelease={currentRelease}
onChange={onInputChange}
/>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
<Button
onPress={onModalClose}
>
Cancel
</Button>
<SpinnerButton
isSpinning={isSaving}
onPress={this.onSavePress}
>
Save
</SpinnerButton>
</ModalFooter>
</ModalContent>
);
}
}
EditAlbumModalContent.propTypes = {
albumId: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
artistName: PropTypes.string.isRequired,
albumType: PropTypes.string.isRequired,
item: PropTypes.object.isRequired,
isSaving: PropTypes.bool.isRequired,
onInputChange: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default EditAlbumModalContent;

@ -0,0 +1,97 @@
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 createAlbumSelector from 'Store/Selectors/createAlbumSelector';
import createArtistSelector from 'Store/Selectors/createArtistSelector';
import { setAlbumValue, saveAlbum } from 'Store/Actions/albumActions';
import EditAlbumModalContent from './EditAlbumModalContent';
function createMapStateToProps() {
return createSelector(
(state) => state.albums,
createAlbumSelector(),
createArtistSelector(),
(albumState, album, artist) => {
const {
isSaving,
saveError,
pendingChanges
} = albumState;
const albumSettings = _.pick(album, [
'monitored',
'currentRelease',
'releases'
]);
const settings = selectSettings(albumSettings, pendingChanges, saveError);
return {
title: album.title,
artistName: artist.artistName,
albumType: album.albumType,
isSaving,
saveError,
item: settings.settings,
...settings
};
}
);
}
const mapDispatchToProps = {
dispatchSetAlbumValue: setAlbumValue,
dispatchSaveAlbum: saveAlbum
};
class EditAlbumModalContentConnector extends Component {
//
// Lifecycle
componentDidUpdate(prevProps, prevState) {
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
this.props.onModalClose();
}
}
//
// Listeners
onInputChange = ({ name, value }) => {
this.props.dispatchSetAlbumValue({ name, value });
}
onSavePress = () => {
this.props.dispatchSaveAlbum({
id: this.props.albumId
});
}
//
// Render
render() {
return (
<EditAlbumModalContent
{...this.props}
onInputChange={this.onInputChange}
onSavePress={this.onSavePress}
/>
);
}
}
EditAlbumModalContentConnector.propTypes = {
albumId: PropTypes.number,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
dispatchSetAlbumValue: PropTypes.func.isRequired,
dispatchSaveAlbum: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(EditAlbumModalContentConnector);

@ -20,6 +20,7 @@ class AlbumStudioRow extends Component {
artistId, artistId,
status, status,
nameSlug, nameSlug,
foreignArtistId,
artistName, artistName,
monitored, monitored,
albums, albums,
@ -49,7 +50,7 @@ class AlbumStudioRow extends Component {
<TableRowCell className={styles.title}> <TableRowCell className={styles.title}>
<ArtistNameLink <ArtistNameLink
nameSlug={nameSlug} foreignArtistId={foreignArtistId}
artistName={artistName} artistName={artistName}
/> />
</TableRowCell> </TableRowCell>
@ -84,6 +85,7 @@ AlbumStudioRow.propTypes = {
artistId: PropTypes.number.isRequired, artistId: PropTypes.number.isRequired,
status: PropTypes.string.isRequired, status: PropTypes.string.isRequired,
nameSlug: PropTypes.string.isRequired, nameSlug: PropTypes.string.isRequired,
foreignArtistId: PropTypes.string.isRequired,
artistName: PropTypes.string.isRequired, artistName: PropTypes.string.isRequired,
monitored: PropTypes.bool.isRequired, monitored: PropTypes.bool.isRequired,
albums: PropTypes.arrayOf(PropTypes.object).isRequired, albums: PropTypes.arrayOf(PropTypes.object).isRequired,

@ -5,7 +5,7 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import createArtistSelector from 'Store/Selectors/createArtistSelector'; import createArtistSelector from 'Store/Selectors/createArtistSelector';
import { toggleArtistMonitored } from 'Store/Actions/artistActions'; import { toggleArtistMonitored } from 'Store/Actions/artistActions';
import { toggleAlbumMonitored } from 'Store/Actions/albumActions'; import { toggleAlbumsMonitored } from 'Store/Actions/albumActions';
import AlbumStudioRow from './AlbumStudioRow'; import AlbumStudioRow from './AlbumStudioRow';
function createMapStateToProps() { function createMapStateToProps() {
@ -32,7 +32,7 @@ function createMapStateToProps() {
const mapDispatchToProps = { const mapDispatchToProps = {
toggleArtistMonitored, toggleArtistMonitored,
toggleAlbumMonitored toggleAlbumsMonitored
}; };
class AlbumStudioRowConnector extends Component { class AlbumStudioRowConnector extends Component {
@ -53,8 +53,9 @@ class AlbumStudioRowConnector extends Component {
} }
onAlbumMonitoredPress = (albumId, monitored) => { onAlbumMonitoredPress = (albumId, monitored) => {
this.props.toggleAlbumMonitored({ const albumIds = [albumId];
albumId, this.props.toggleAlbumsMonitored({
albumIds,
monitored monitored
}); });
} }
@ -77,7 +78,7 @@ AlbumStudioRowConnector.propTypes = {
artistId: PropTypes.number.isRequired, artistId: PropTypes.number.isRequired,
monitored: PropTypes.bool.isRequired, monitored: PropTypes.bool.isRequired,
toggleArtistMonitored: PropTypes.func.isRequired, toggleArtistMonitored: PropTypes.func.isRequired,
toggleAlbumMonitored: PropTypes.func.isRequired toggleAlbumsMonitored: PropTypes.func.isRequired
}; };
export default connect(createMapStateToProps, mapDispatchToProps)(AlbumStudioRowConnector); export default connect(createMapStateToProps, mapDispatchToProps)(AlbumStudioRowConnector);

@ -14,6 +14,7 @@ import ImportArtist from 'AddArtist/ImportArtist/ImportArtist';
import ArtistEditorConnector from 'Artist/Editor/ArtistEditorConnector'; import ArtistEditorConnector from 'Artist/Editor/ArtistEditorConnector';
import AlbumStudioConnector from 'AlbumStudio/AlbumStudioConnector'; import AlbumStudioConnector from 'AlbumStudio/AlbumStudioConnector';
import ArtistDetailsPageConnector from 'Artist/Details/ArtistDetailsPageConnector'; import ArtistDetailsPageConnector from 'Artist/Details/ArtistDetailsPageConnector';
import AlbumDetailsPageConnector from 'Album/Details/AlbumDetailsPageConnector';
import CalendarPageConnector from 'Calendar/CalendarPageConnector'; import CalendarPageConnector from 'Calendar/CalendarPageConnector';
import HistoryConnector from 'Activity/History/HistoryConnector'; import HistoryConnector from 'Activity/History/HistoryConnector';
import QueueConnector from 'Activity/Queue/QueueConnector'; import QueueConnector from 'Activity/Queue/QueueConnector';
@ -92,10 +93,15 @@ function App({ store, history }) {
/> />
<Route <Route
path="/artist/:nameSlug" path="/artist/:foreignArtistId"
component={ArtistDetailsPageConnector} component={ArtistDetailsPageConnector}
/> />
<Route
path="/album/:foreignAlbumId"
component={AlbumDetailsPageConnector}
/>
{/* {/*
Calendar Calendar
*/} */}

@ -2,8 +2,8 @@ import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
function ArtistNameLink({ nameSlug, artistName }) { function ArtistNameLink({ foreignArtistId, artistName }) {
const link = `/artist/${nameSlug}`; const link = `/artist/${foreignArtistId}`;
return ( return (
<Link to={link}> <Link to={link}>
@ -13,7 +13,7 @@ function ArtistNameLink({ nameSlug, artistName }) {
} }
ArtistNameLink.propTypes = { ArtistNameLink.propTypes = {
nameSlug: PropTypes.string.isRequired, foreignArtistId: PropTypes.string.isRequired,
artistName: PropTypes.string.isRequired artistName: PropTypes.string.isRequired
}; };

@ -8,8 +8,7 @@ import Label from 'Components/Label';
import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell';
import formatTimeSpan from 'Utilities/Date/formatTimeSpan'; import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
import AlbumSearchCellConnector from 'Album/AlbumSearchCellConnector'; import AlbumSearchCellConnector from 'Album/AlbumSearchCellConnector';
import AlbumTitleLink from 'Album/AlbumTitleLink'; import AlbumTitleDetailLink from 'Album/AlbumTitleDetailLink';
import styles from './AlbumRow.css'; import styles from './AlbumRow.css';
function getTrackCountKind(monitored, trackFileCount, trackCount) { function getTrackCountKind(monitored, trackFileCount, trackCount) {
@ -33,7 +32,8 @@ class AlbumRow extends Component {
super(props, context); super(props, context);
this.state = { this.state = {
isDetailsModalOpen: false isDetailsModalOpen: false,
isEditAlbumModalOpen: false
}; };
} }
@ -48,6 +48,14 @@ class AlbumRow extends Component {
this.setState({ isDetailsModalOpen: false }); this.setState({ isDetailsModalOpen: false });
} }
onEditAlbumPress = () => {
this.setState({ isEditAlbumModalOpen: true });
}
onEditAlbumModalClose = () => {
this.setState({ isEditAlbumModalOpen: false });
}
onMonitorAlbumPress = (monitored, options) => { onMonitorAlbumPress = (monitored, options) => {
this.props.onMonitorAlbumPress(this.props.id, monitored, options); this.props.onMonitorAlbumPress(this.props.id, monitored, options);
} }
@ -64,9 +72,11 @@ class AlbumRow extends Component {
duration, duration,
releaseDate, releaseDate,
mediumCount, mediumCount,
secondaryTypes,
title, title,
isSaving, isSaving,
artistMonitored, artistMonitored,
foreignAlbumId,
columns columns
} = this.props; } = this.props;
@ -111,11 +121,9 @@ class AlbumRow extends Component {
key={name} key={name}
className={styles.title} className={styles.title}
> >
<AlbumTitleLink <AlbumTitleDetailLink
albumId={id} title={title}
artistId={artistId} foreignAlbumId={foreignAlbumId}
albumTitle={title}
showOpenArtistButton={false}
/> />
</TableRowCell> </TableRowCell>
); );
@ -131,6 +139,16 @@ class AlbumRow extends Component {
); );
} }
if (name === 'secondaryTypes') {
return (
<TableRowCell key={name}>
{
secondaryTypes
}
</TableRowCell>
);
}
if (name === 'trackCount') { if (name === 'trackCount') {
return ( return (
<TableRowCell key={name}> <TableRowCell key={name}>
@ -189,7 +207,6 @@ class AlbumRow extends Component {
/> />
); );
} }
return null; return null;
}) })
} }
@ -206,6 +223,8 @@ AlbumRow.propTypes = {
mediumCount: PropTypes.number.isRequired, mediumCount: PropTypes.number.isRequired,
duration: PropTypes.number.isRequired, duration: PropTypes.number.isRequired,
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired,
secondaryTypes: PropTypes.arrayOf(PropTypes.string).isRequired,
foreignAlbumId: PropTypes.string.isRequired,
isSaving: PropTypes.bool, isSaving: PropTypes.bool,
unverifiedSceneNumbering: PropTypes.bool, unverifiedSceneNumbering: PropTypes.bool,
artistMonitored: PropTypes.bool.isRequired, artistMonitored: PropTypes.bool.isRequired,

@ -15,6 +15,7 @@ function createMapStateToProps() {
createCommandsSelector(), createCommandsSelector(),
(id, sceneSeasonNumber, artist, trackFile, commands) => { (id, sceneSeasonNumber, artist, trackFile, commands) => {
return { return {
foreignArtistId: artist.foreignArtistId,
artistMonitored: artist.monitored, artistMonitored: artist.monitored,
trackFilePath: trackFile ? trackFile.path : null, trackFilePath: trackFile ? trackFile.path : null,
trackFileRelativePath: trackFile ? trackFile.relativePath : null trackFileRelativePath: trackFile ? trackFile.relativePath : null

@ -34,34 +34,6 @@ import ArtistDetailsLinks from './ArtistDetailsLinks';
import styles from './ArtistDetails.css'; import styles from './ArtistDetails.css';
import InteractiveImportModal from '../../InteractiveImport/InteractiveImportModal'; import InteractiveImportModal from '../../InteractiveImport/InteractiveImportModal';
const albumTypes = [
{
name: 'album',
label: 'Album',
isVisible: true
},
{
name: 'ep',
label: 'EP',
isVisible: true
},
{
name: 'single',
label: 'Single',
isVisible: true
},
{
name: 'broadcast',
label: 'Broadcast',
isVisible: true
},
{
name: 'other',
label: 'Other',
isVisible: true
}
];
const defaultFontSize = parseInt(fonts.defaultFontSize); const defaultFontSize = parseInt(fonts.defaultFontSize);
const lineHeight = parseFloat(fonts.lineHeight); const lineHeight = parseFloat(fonts.lineHeight);
@ -193,6 +165,7 @@ class ArtistDetails extends Component {
trackFileCount, trackFileCount,
qualityProfileId, qualityProfileId,
monitored, monitored,
albumTypes,
status, status,
overview, overview,
links, links,
@ -359,7 +332,7 @@ class ArtistDetails extends Component {
name={icons.ARROW_LEFT} name={icons.ARROW_LEFT}
size={30} size={30}
title={`Go to ${previousArtist.artistName}`} title={`Go to ${previousArtist.artistName}`}
to={`/artist/${previousArtist.nameSlug}`} to={`/artist/${previousArtist.foreignArtistId}`}
/> />
<IconButton <IconButton
@ -367,7 +340,7 @@ class ArtistDetails extends Component {
name={icons.ARROW_RIGHT} name={icons.ARROW_RIGHT}
size={30} size={30}
title={`Go to ${nextArtist.artistName}`} title={`Go to ${nextArtist.artistName}`}
to={`/artist/${nextArtist.nameSlug}`} to={`/artist/${nextArtist.foreignArtistId}`}
/> />
</div> </div>
</div> </div>
@ -545,12 +518,12 @@ class ArtistDetails extends Component {
albumTypes.slice(0).map((albumType) => { albumTypes.slice(0).map((albumType) => {
return ( return (
<ArtistDetailsSeasonConnector <ArtistDetailsSeasonConnector
key={albumType.name} key={albumType}
artistId={id} artistId={id}
name={albumType.name} name={albumType}
label={albumType.label} label={albumType}
{...albumType} {...albumType}
isExpanded={expandedState[albumType.name]} isExpanded={expandedState[albumType]}
onExpandPress={this.onExpandPress} onExpandPress={this.onExpandPress}
/> />
); );
@ -614,6 +587,7 @@ ArtistDetails.propTypes = {
trackFileCount: PropTypes.number, trackFileCount: PropTypes.number,
qualityProfileId: PropTypes.number.isRequired, qualityProfileId: PropTypes.number.isRequired,
monitored: PropTypes.bool.isRequired, monitored: PropTypes.bool.isRequired,
albumTypes: PropTypes.arrayOf(PropTypes.string),
status: PropTypes.string.isRequired, status: PropTypes.string.isRequired,
overview: PropTypes.string.isRequired, overview: PropTypes.string.isRequired,
links: PropTypes.arrayOf(PropTypes.object).isRequired, links: PropTypes.arrayOf(PropTypes.object).isRequired,

@ -17,20 +17,30 @@ import ArtistDetails from './ArtistDetails';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state, { nameSlug }) => nameSlug, (state, { foreignArtistId }) => foreignArtistId,
(state) => state.albums, (state) => state.albums,
(state) => state.trackFiles, (state) => state.trackFiles,
(state) => state.settings.metadataProfiles,
createAllArtistSelector(), createAllArtistSelector(),
createCommandsSelector(), createCommandsSelector(),
(nameSlug, albums, trackFiles, allArtists, commands) => { (foreignArtistId, albums, trackFiles, metadataProfiles, allArtists, commands) => {
const sortedArtist = _.orderBy(allArtists, 'sortName'); const sortedArtist = _.orderBy(allArtists, 'sortName');
const artistIndex = _.findIndex(sortedArtist, { nameSlug }); const artistIndex = _.findIndex(sortedArtist, { foreignArtistId });
const artist = sortedArtist[artistIndex]; const artist = sortedArtist[artistIndex];
const metadataProfile = _.find(metadataProfiles.items, { id: artist.metadataProfileId });
const albumTypes = _.reduce(metadataProfile.primaryAlbumTypes, (acc, primaryType) => {
if (primaryType.allowed) {
acc.push(primaryType.albumType.name);
}
return acc;
}, []);
if (!artist) { if (!artist) {
return {}; return {};
} }
const sortedAlbumTypes = _.orderBy(albumTypes);
const previousArtist = sortedArtist[artistIndex - 1] || _.last(sortedArtist); const previousArtist = sortedArtist[artistIndex - 1] || _.last(sortedArtist);
const nextArtist = sortedArtist[artistIndex + 1] || _.first(sortedArtist); const nextArtist = sortedArtist[artistIndex + 1] || _.first(sortedArtist);
const isArtistRefreshing = !!findCommand(commands, { name: commandNames.REFRESH_ARTIST, artistId: artist.id }); const isArtistRefreshing = !!findCommand(commands, { name: commandNames.REFRESH_ARTIST, artistId: artist.id });
@ -56,6 +66,7 @@ function createMapStateToProps() {
return { return {
...artist, ...artist,
albumTypes: sortedAlbumTypes,
alternateTitles, alternateTitles,
isArtistRefreshing, isArtistRefreshing,
allArtistRefreshing, allArtistRefreshing,
@ -176,7 +187,7 @@ class ArtistDetailsConnector extends Component {
ArtistDetailsConnector.propTypes = { ArtistDetailsConnector.propTypes = {
id: PropTypes.number.isRequired, id: PropTypes.number.isRequired,
nameSlug: PropTypes.string.isRequired, foreignArtistId: PropTypes.string.isRequired,
isArtistRefreshing: PropTypes.bool.isRequired, isArtistRefreshing: PropTypes.bool.isRequired,
allArtistRefreshing: PropTypes.bool.isRequired, allArtistRefreshing: PropTypes.bool.isRequired,
isRefreshing: PropTypes.bool.isRequired, isRefreshing: PropTypes.bool.isRequired,

@ -13,12 +13,12 @@ function createMapStateToProps() {
(state, { match }) => match, (state, { match }) => match,
createAllArtistSelector(), createAllArtistSelector(),
(match, allArtists) => { (match, allArtists) => {
const nameSlug = match.params.nameSlug; const foreignArtistId = match.params.foreignArtistId;
const artistIndex = _.findIndex(allArtists, { nameSlug }); const artistIndex = _.findIndex(allArtists, { foreignArtistId });
if (artistIndex > -1) { if (artistIndex > -1) {
return { return {
nameSlug foreignArtistId
}; };
} }
@ -37,7 +37,7 @@ class ArtistDetailsPageConnector extends Component {
// Lifecycle // Lifecycle
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
if (!this.props.nameSlug) { if (!this.props.foreignArtistId) {
this.props.push(`${window.Sonarr.urlBase}/`); this.props.push(`${window.Sonarr.urlBase}/`);
return; return;
} }
@ -48,10 +48,10 @@ class ArtistDetailsPageConnector extends Component {
render() { render() {
const { const {
nameSlug foreignArtistId
} = this.props; } = this.props;
if (!nameSlug) { if (!foreignArtistId) {
return ( return (
<NotFound <NotFound
message="Sorry, that artist cannot be found." message="Sorry, that artist cannot be found."
@ -61,15 +61,15 @@ class ArtistDetailsPageConnector extends Component {
return ( return (
<ArtistDetailsConnector <ArtistDetailsConnector
nameSlug={nameSlug} foreignArtistId={foreignArtistId}
/> />
); );
} }
} }
ArtistDetailsPageConnector.propTypes = { ArtistDetailsPageConnector.propTypes = {
nameSlug: PropTypes.string, foreignArtistId: PropTypes.string,
match: PropTypes.shape({ params: PropTypes.shape({ nameSlug: PropTypes.string.isRequired }).isRequired }).isRequired, match: PropTypes.shape({ params: PropTypes.shape({ foreignArtistId: PropTypes.string.isRequired }).isRequired }).isRequired,
push: PropTypes.func.isRequired push: PropTypes.func.isRequired
}; };

@ -106,7 +106,7 @@
} }
@media only screen and (max-width: $breakpointSmall) { @media only screen and (max-width: $breakpointSmall) {
.season { .albumType {
border-right: 0; border-right: 0;
border-left: 0; border-left: 0;
border-radius: 0; border-radius: 0;

@ -27,7 +27,7 @@ class ArtistEditorRow extends Component {
const { const {
id, id,
status, status,
nameSlug, foreignArtistId,
artistName, artistName,
monitored, monitored,
languageProfile, languageProfile,
@ -56,7 +56,7 @@ class ArtistEditorRow extends Component {
<TableRowCell className={styles.title}> <TableRowCell className={styles.title}>
<ArtistNameLink <ArtistNameLink
nameSlug={nameSlug} foreignArtistId={foreignArtistId}
artistName={artistName} artistName={artistName}
/> />
</TableRowCell> </TableRowCell>
@ -105,7 +105,7 @@ class ArtistEditorRow extends Component {
ArtistEditorRow.propTypes = { ArtistEditorRow.propTypes = {
id: PropTypes.number.isRequired, id: PropTypes.number.isRequired,
status: PropTypes.string.isRequired, status: PropTypes.string.isRequired,
nameSlug: PropTypes.string.isRequired, foreignArtistId: PropTypes.string.isRequired,
artistName: PropTypes.string.isRequired, artistName: PropTypes.string.isRequired,
monitored: PropTypes.bool.isRequired, monitored: PropTypes.bool.isRequired,
languageProfile: PropTypes.object.isRequired, languageProfile: PropTypes.object.isRequired,

@ -59,7 +59,7 @@ class ArtistIndexBanner extends Component {
artistName, artistName,
monitored, monitored,
status, status,
nameSlug, foreignArtistId,
nextAiring, nextAiring,
trackCount, trackCount,
trackFileCount, trackFileCount,
@ -84,7 +84,7 @@ class ArtistIndexBanner extends Component {
isDeleteArtistModalOpen isDeleteArtistModalOpen
} = this.state; } = this.state;
const link = `/artist/${nameSlug}`; const link = `/artist/${foreignArtistId}`;
const elementStyle = { const elementStyle = {
width: `${bannerWidth}px`, width: `${bannerWidth}px`,
@ -213,7 +213,7 @@ ArtistIndexBanner.propTypes = {
artistName: PropTypes.string.isRequired, artistName: PropTypes.string.isRequired,
monitored: PropTypes.bool.isRequired, monitored: PropTypes.bool.isRequired,
status: PropTypes.string.isRequired, status: PropTypes.string.isRequired,
nameSlug: PropTypes.string.isRequired, foreignArtistId: PropTypes.string.isRequired,
nextAiring: PropTypes.string, nextAiring: PropTypes.string,
trackCount: PropTypes.number, trackCount: PropTypes.number,
trackFileCount: PropTypes.number, trackFileCount: PropTypes.number,

@ -77,7 +77,7 @@ class ArtistIndexOverview extends Component {
overview, overview,
monitored, monitored,
status, status,
nameSlug, foreignArtistId,
nextAiring, nextAiring,
trackCount, trackCount,
trackFileCount, trackFileCount,
@ -101,7 +101,7 @@ class ArtistIndexOverview extends Component {
isDeleteArtistModalOpen isDeleteArtistModalOpen
} = this.state; } = this.state;
const link = `/artist/${nameSlug}`; const link = `/artist/${foreignArtistId}`;
const elementStyle = { const elementStyle = {
width: `${posterWidth}px`, width: `${posterWidth}px`,
@ -226,7 +226,7 @@ ArtistIndexOverview.propTypes = {
overview: PropTypes.string.isRequired, overview: PropTypes.string.isRequired,
monitored: PropTypes.bool.isRequired, monitored: PropTypes.bool.isRequired,
status: PropTypes.string.isRequired, status: PropTypes.string.isRequired,
nameSlug: PropTypes.string.isRequired, foreignArtistId: PropTypes.string.isRequired,
nextAiring: PropTypes.string, nextAiring: PropTypes.string,
trackCount: PropTypes.number, trackCount: PropTypes.number,
trackFileCount: PropTypes.number, trackFileCount: PropTypes.number,

@ -58,8 +58,8 @@ class ArtistIndexPoster extends Component {
id, id,
artistName, artistName,
monitored, monitored,
foreignArtistId,
status, status,
nameSlug,
nextAiring, nextAiring,
trackCount, trackCount,
trackFileCount, trackFileCount,
@ -84,7 +84,7 @@ class ArtistIndexPoster extends Component {
isDeleteArtistModalOpen isDeleteArtistModalOpen
} = this.state; } = this.state;
const link = `/artist/${nameSlug}`; const link = `/artist/${foreignArtistId}`;
const elementStyle = { const elementStyle = {
width: `${posterWidth}px`, width: `${posterWidth}px`,
@ -213,7 +213,7 @@ ArtistIndexPoster.propTypes = {
artistName: PropTypes.string.isRequired, artistName: PropTypes.string.isRequired,
monitored: PropTypes.bool.isRequired, monitored: PropTypes.bool.isRequired,
status: PropTypes.string.isRequired, status: PropTypes.string.isRequired,
nameSlug: PropTypes.string.isRequired, foreignArtistId: PropTypes.string.isRequired,
nextAiring: PropTypes.string, nextAiring: PropTypes.string,
trackCount: PropTypes.number, trackCount: PropTypes.number,
trackFileCount: PropTypes.number, trackFileCount: PropTypes.number,

@ -18,8 +18,8 @@
flex: 1 0 125px; flex: 1 0 125px;
} }
.nextAiring, .nextAlbum,
.previousAiring, .lastAlbum,
.added { .added {
composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css'; composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css';

@ -18,8 +18,8 @@
flex: 1 0 125px; flex: 1 0 125px;
} }
.nextAiring, .nextAlbum,
.previousAiring, .lastAlbum,
.added { .added {
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css'; composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
@ -32,8 +32,7 @@
flex: 0 0 100px; flex: 0 0 100px;
} }
.trackProgress, .trackProgress {
.latestAlbum {
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css'; composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
display: flex; display: flex;

@ -12,6 +12,7 @@ import VirtualTableRow from 'Components/Table/VirtualTableRow';
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell'; import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import ArtistNameLink from 'Artist/ArtistNameLink'; import ArtistNameLink from 'Artist/ArtistNameLink';
import AlbumTitleDetailLink from 'Album/AlbumTitleDetailLink';
import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector'; import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector';
import DeleteArtistModal from 'Artist/Delete/DeleteArtistModal'; import DeleteArtistModal from 'Artist/Delete/DeleteArtistModal';
import ArtistStatusCell from './ArtistStatusCell'; import ArtistStatusCell from './ArtistStatusCell';
@ -66,12 +67,13 @@ class ArtistIndexRow extends Component {
status, status,
artistName, artistName,
nameSlug, nameSlug,
foreignArtistId,
artistType, artistType,
qualityProfile, qualityProfile,
languageProfile, languageProfile,
metadataProfile, metadataProfile,
nextAiring, nextAlbum,
previousAiring, lastAlbum,
added, added,
albumCount, albumCount,
trackCount, trackCount,
@ -81,7 +83,6 @@ class ArtistIndexRow extends Component {
path, path,
sizeOnDisk, sizeOnDisk,
tags, tags,
// useSceneNumbering,
columns, columns,
isRefreshingArtist, isRefreshingArtist,
onRefreshArtistPress onRefreshArtistPress
@ -124,7 +125,7 @@ class ArtistIndexRow extends Component {
className={styles[name]} className={styles[name]}
> >
<ArtistNameLink <ArtistNameLink
nameSlug={nameSlug} foreignArtistId={foreignArtistId}
artistName={artistName} artistName={artistName}
/> />
</VirtualTableRowCell> </VirtualTableRowCell>
@ -175,25 +176,51 @@ class ArtistIndexRow extends Component {
); );
} }
if (name === 'nextAiring') { if (name === 'nextAlbum') {
if (nextAlbum) {
return (
<VirtualTableRowCell
key={name}
className={styles[name]}
>
<AlbumTitleDetailLink
title={nextAlbum.title}
foreignAlbumId={nextAlbum.foreignAlbumId}
/>
</VirtualTableRowCell>
);
}
return ( return (
<RelativeDateCellConnector <VirtualTableRowCell
key={name} key={name}
className={styles[name]} className={styles[name]}
date={nextAiring} >
component={VirtualTableRowCell} None
/> </VirtualTableRowCell>
); );
} }
if (name === 'previousAiring') { if (name === 'lastAlbum') {
if (lastAlbum) {
return (
<VirtualTableRowCell
key={name}
className={styles[name]}
>
<AlbumTitleDetailLink
title={lastAlbum.title}
foreignAlbumId={lastAlbum.foreignAlbumId}
/>
</VirtualTableRowCell>
);
}
return ( return (
<RelativeDateCellConnector <VirtualTableRowCell
key={name} key={name}
className={styles[name]} className={styles[name]}
date={previousAiring} >
component={VirtualTableRowCell} None
/> </VirtualTableRowCell>
); );
} }
@ -239,27 +266,6 @@ class ArtistIndexRow extends Component {
); );
} }
if (name === 'latestAlbum') {
const albumStatistics = latestAlbum.statistics;
const progress = albumStatistics.trackCount ? albumStatistics.trackFileCount / albumStatistics.trackCount * 100 : 100;
return (
<VirtualTableRowCell
key={name}
className={styles[name]}
>
<ProgressBar
progress={progress}
kind={getProgressBarKind(status, monitored, progress)}
showText={true}
text={`${albumStatistics.trackFileCount} / ${albumStatistics.trackCount}`}
title={`${albumStatistics.trackFileCount} / ${albumStatistics.trackCount} (Total: ${albumStatistics.totalTrackCount})`}
width={125}
/>
</VirtualTableRowCell>
);
}
if (name === 'trackCount') { if (name === 'trackCount') {
return ( return (
<VirtualTableRowCell <VirtualTableRowCell
@ -356,14 +362,15 @@ ArtistIndexRow.propTypes = {
status: PropTypes.string.isRequired, status: PropTypes.string.isRequired,
artistName: PropTypes.string.isRequired, artistName: PropTypes.string.isRequired,
nameSlug: PropTypes.string.isRequired, nameSlug: PropTypes.string.isRequired,
foreignArtistId: PropTypes.string.isRequired,
artistType: PropTypes.string, artistType: PropTypes.string,
qualityProfile: PropTypes.object.isRequired, qualityProfile: PropTypes.object.isRequired,
languageProfile: PropTypes.object.isRequired, languageProfile: PropTypes.object.isRequired,
metadataProfile: PropTypes.object.isRequired, metadataProfile: PropTypes.object.isRequired,
nextAiring: PropTypes.string, nextAlbum: PropTypes.object,
previousAiring: PropTypes.string, lastAlbum: PropTypes.object,
added: PropTypes.string, added: PropTypes.string,
albumCount: PropTypes.number.isRequired, albumCount: PropTypes.number,
trackCount: PropTypes.number, trackCount: PropTypes.number,
trackFileCount: PropTypes.number, trackFileCount: PropTypes.number,
totalTrackCount: PropTypes.number, totalTrackCount: PropTypes.number,

@ -23,6 +23,7 @@ export default function artistIndexCellRenderers(cellProps) {
status, status,
name, name,
nameSlug, nameSlug,
foreignArtistId,
qualityProfileId, qualityProfileId,
nextAiring, nextAiring,
previousAiring, previousAiring,
@ -52,7 +53,7 @@ export default function artistIndexCellRenderers(cellProps) {
{...otherProps} {...otherProps}
> >
<ArtistNameLink <ArtistNameLink
nameSlug={nameSlug} foreignArtistId={foreignArtistId}
name={name} name={name}
/> />
</VirtualTableRowCell> </VirtualTableRowCell>

@ -0,0 +1,65 @@
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, { albumReleases }) => albumReleases,
(state, { selectedRelease }) => selectedRelease,
(albumReleases, selectedRelease) => {
const values = _.map(albumReleases.value, (albumRelease) => {
return {
key: albumRelease.id,
value: `${albumRelease.mediaCount} med, ${albumRelease.trackCount} tracks` +
`${albumRelease.country.length > 0 ? ', ' : ''}${albumRelease.country}` +
`${albumRelease.disambiguation ? ', ' : ''}${titleCase(albumRelease.disambiguation)}` +
`${albumRelease.format ? ', [' : ''}${albumRelease.format}${albumRelease.format ? ']' : ''}`
};
});
const value = selectedRelease.value.id;
return {
values,
value
};
}
);
}
class AlbumReleaseSelectInputConnector extends Component {
//
// Listeners
onChange = ({ name, value }) => {
const {
albumReleases
} = this.props;
this.props.onChange({ name, value: _.find(albumReleases.value, { id: value }) });
}
render() {
return (
<SelectInput
{...this.props}
onChange={this.onChange}
/>
);
}
}
AlbumReleaseSelectInputConnector.propTypes = {
name: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
albumReleases: PropTypes.object
};
export default connect(createMapStateToProps)(AlbumReleaseSelectInputConnector);

@ -12,6 +12,7 @@ import PathInputConnector from './PathInputConnector';
import QualityProfileSelectInputConnector from './QualityProfileSelectInputConnector'; import QualityProfileSelectInputConnector from './QualityProfileSelectInputConnector';
import LanguageProfileSelectInputConnector from './LanguageProfileSelectInputConnector'; import LanguageProfileSelectInputConnector from './LanguageProfileSelectInputConnector';
import MetadataProfileSelectInputConnector from './MetadataProfileSelectInputConnector'; import MetadataProfileSelectInputConnector from './MetadataProfileSelectInputConnector';
import AlbumReleaseSelectInputConnector from './AlbumReleaseSelectInputConnector';
import RootFolderSelectInputConnector from './RootFolderSelectInputConnector'; import RootFolderSelectInputConnector from './RootFolderSelectInputConnector';
import SeriesTypeSelectInput from './SeriesTypeSelectInput'; import SeriesTypeSelectInput from './SeriesTypeSelectInput';
import SelectInput from './SelectInput'; import SelectInput from './SelectInput';
@ -53,6 +54,9 @@ function getComponent(type) {
case inputTypes.METADATA_PROFILE_SELECT: case inputTypes.METADATA_PROFILE_SELECT:
return MetadataProfileSelectInputConnector; return MetadataProfileSelectInputConnector;
case inputTypes.ALBUM_RELEASE_SELECT:
return AlbumReleaseSelectInputConnector;
case inputTypes.ROOT_FOLDER_SELECT: case inputTypes.ROOT_FOLDER_SELECT:
return RootFolderSelectInputConnector; return RootFolderSelectInputConnector;

@ -218,12 +218,16 @@ class SignalRConnector extends Component {
} }
handleTrack = (body) => { handleTrack = (body) => {
if (body.action === 'updated') { const action = body.action;
const section = 'tracks';
if (action === 'updated') {
this.props.updateItem({ this.props.updateItem({
section: 'tracks', section,
updateOnly: true,
...body.resource ...body.resource
}); });
} else if (action === 'deleted') {
this.props.removeItem({ section, id: body.resource.id });
} }
} }

@ -3,6 +3,7 @@ export const ACTIVITY = 'fa fa-clock-o';
export const ADD = 'fa fa-plus'; export const ADD = 'fa fa-plus';
export const ALTERNATE_TITLES = 'fa fa-clone'; export const ALTERNATE_TITLES = 'fa fa-clone';
export const ADVANCED_SETTINGS = 'fa fa-cog'; export const ADVANCED_SETTINGS = 'fa fa-cog';
export const ARROW_UP = 'fa fa-arrow-circle-up';
export const ARROW_LEFT = 'fa fa-arrow-circle-left'; export const ARROW_LEFT = 'fa fa-arrow-circle-left';
export const ARROW_RIGHT = 'fa fa-arrow-circle-right'; export const ARROW_RIGHT = 'fa fa-arrow-circle-right';
export const BACKUP = 'fa fa-file-archive-o'; export const BACKUP = 'fa fa-file-archive-o';

@ -8,6 +8,7 @@ export const PATH = 'path';
export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect'; export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect';
export const LANGUAGE_PROFILE_SELECT = 'languageProfileSelect'; export const LANGUAGE_PROFILE_SELECT = 'languageProfileSelect';
export const METADATA_PROFILE_SELECT = 'metadataProfileSelect'; export const METADATA_PROFILE_SELECT = 'metadataProfileSelect';
export const ALBUM_RELEASE_SELECT = 'albumReleaseSelect';
export const ROOT_FOLDER_SELECT = 'rootFolderSelect'; export const ROOT_FOLDER_SELECT = 'rootFolderSelect';
export const SELECT = 'select'; export const SELECT = 'select';
export const SERIES_TYPE_SELECT = 'artistTypeSelect'; export const SERIES_TYPE_SELECT = 'artistTypeSelect';
@ -26,6 +27,7 @@ export const all = [
QUALITY_PROFILE_SELECT, QUALITY_PROFILE_SELECT,
LANGUAGE_PROFILE_SELECT, LANGUAGE_PROFILE_SELECT,
METADATA_PROFILE_SELECT, METADATA_PROFILE_SELECT,
ALBUM_RELEASE_SELECT,
ROOT_FOLDER_SELECT, ROOT_FOLDER_SELECT,
SELECT, SELECT,
SERIES_TYPE_SELECT, SERIES_TYPE_SELECT,

@ -5,7 +5,9 @@ import { batchActions } from 'redux-batched-actions';
import { sortDirections } from 'Helpers/Props'; import { sortDirections } from 'Helpers/Props';
import { createThunk, handleThunks } from 'Store/thunks'; import { createThunk, handleThunks } from 'Store/thunks';
import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer'; import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
import createSetSettingValueReducer from './Creators/Reducers/createSetSettingValueReducer';
import createSetTableOptionReducer from './Creators/Reducers/createSetTableOptionReducer'; import createSetTableOptionReducer from './Creators/Reducers/createSetTableOptionReducer';
import createSaveProviderHandler from './Creators/createSaveProviderHandler';
import albumEntities from 'Album/albumEntities'; import albumEntities from 'Album/albumEntities';
import createFetchHandler from './Creators/createFetchHandler'; import createFetchHandler from './Creators/createFetchHandler';
import createHandleActions from './Creators/createHandleActions'; import createHandleActions from './Creators/createHandleActions';
@ -23,9 +25,12 @@ export const defaultState = {
isFetching: false, isFetching: false,
isPopulated: false, isPopulated: false,
error: null, error: null,
isSaving: false,
saveError: null,
sortKey: 'releaseDate', sortKey: 'releaseDate',
sortDirection: sortDirections.DESCENDING, sortDirection: sortDirections.DESCENDING,
items: [], items: [],
pendingChanges: {},
columns: [ columns: [
{ {
@ -44,6 +49,11 @@ export const defaultState = {
label: 'Release Date', label: 'Release Date',
isVisible: true isVisible: true
}, },
{
name: 'secondaryTypes',
label: 'Secondary Types',
isVisible: false
},
{ {
name: 'mediumCount', name: 'mediumCount',
label: 'Media Count', label: 'Media Count',
@ -84,6 +94,8 @@ export const FETCH_ALBUMS = 'albums/fetchAlbums';
export const SET_ALBUMS_SORT = 'albums/setAlbumsSort'; export const SET_ALBUMS_SORT = 'albums/setAlbumsSort';
export const SET_ALBUMS_TABLE_OPTION = 'albums/setAlbumsTableOption'; export const SET_ALBUMS_TABLE_OPTION = 'albums/setAlbumsTableOption';
export const CLEAR_ALBUMS = 'albums/clearAlbums'; export const CLEAR_ALBUMS = 'albums/clearAlbums';
export const SET_ALBUM_VALUE = 'albums/setAlbumValue';
export const SAVE_ALBUM = 'albums/saveAlbum';
export const TOGGLE_ALBUM_MONITORED = 'albums/toggleAlbumMonitored'; export const TOGGLE_ALBUM_MONITORED = 'albums/toggleAlbumMonitored';
export const TOGGLE_ALBUMS_MONITORED = 'albums/toggleAlbumsMonitored'; export const TOGGLE_ALBUMS_MONITORED = 'albums/toggleAlbumsMonitored';
@ -97,11 +109,21 @@ export const clearAlbums = createAction(CLEAR_ALBUMS);
export const toggleAlbumMonitored = createThunk(TOGGLE_ALBUM_MONITORED); export const toggleAlbumMonitored = createThunk(TOGGLE_ALBUM_MONITORED);
export const toggleAlbumsMonitored = createThunk(TOGGLE_ALBUMS_MONITORED); export const toggleAlbumsMonitored = createThunk(TOGGLE_ALBUMS_MONITORED);
export const saveAlbum = createThunk(SAVE_ALBUM);
export const setAlbumValue = createAction(SET_ALBUM_VALUE, (payload) => {
return {
section: 'albums',
...payload
};
});
// //
// Action Handlers // Action Handlers
export const actionHandlers = handleThunks({ export const actionHandlers = handleThunks({
[FETCH_ALBUMS]: createFetchHandler(section, '/album'), [FETCH_ALBUMS]: createFetchHandler(section, '/album'),
[SAVE_ALBUM]: createSaveProviderHandler(section, '/album'),
[TOGGLE_ALBUM_MONITORED]: function(getState, payload, dispatch) { [TOGGLE_ALBUM_MONITORED]: function(getState, payload, dispatch) {
const { const {
@ -203,6 +225,8 @@ export const reducers = createHandleActions({
[SET_ALBUMS_TABLE_OPTION]: createSetTableOptionReducer(section), [SET_ALBUMS_TABLE_OPTION]: createSetTableOptionReducer(section),
[SET_ALBUM_VALUE]: createSetSettingValueReducer(section),
[CLEAR_ALBUMS]: (state) => { [CLEAR_ALBUMS]: (state) => {
return Object.assign({}, state, { return Object.assign({}, state, {
isFetching: false, isFetching: false,

@ -93,14 +93,14 @@ export const defaultState = {
isVisible: false isVisible: false
}, },
{ {
name: 'nextAiring', name: 'nextAlbum',
label: 'Next Airing', label: 'Next Album',
isSortable: true, isSortable: true,
isVisible: true isVisible: true
}, },
{ {
name: 'previousAiring', name: 'lastAlbum',
label: 'Previous Airing', label: 'Last Album',
isSortable: true, isSortable: true,
isVisible: false isVisible: false
}, },
@ -128,12 +128,6 @@ export const defaultState = {
isSortable: true, isSortable: true,
isVisible: false isVisible: false
}, },
{
name: 'latestAlbum',
label: 'Latest Album',
isSortable: true,
isVisible: false
},
{ {
name: 'path', name: 'path',
label: 'Path', label: 'Path',

@ -20,13 +20,15 @@ export const defaultState = {
error: null, error: null,
sortKey: 'mediumNumber', sortKey: 'mediumNumber',
sortDirection: sortDirections.DESCENDING, sortDirection: sortDirections.DESCENDING,
secondarySortKey: 'absoluteTrackNumber',
secondarySortDirection: sortDirections.ASCENDING,
items: [], items: [],
columns: [ columns: [
{ {
name: 'medium', name: 'medium',
label: 'Medium', label: 'Medium',
isVisible: true isVisible: false
}, },
{ {
name: 'absoluteTrackNumber', name: 'absoluteTrackNumber',
@ -38,11 +40,26 @@ export const defaultState = {
label: 'Title', label: 'Title',
isVisible: true isVisible: true
}, },
{
name: 'path',
label: 'Path',
isVisible: false
},
{
name: 'relativePath',
label: 'Relative Path',
isVisible: false
},
{ {
name: 'duration', name: 'duration',
label: 'Duration', label: 'Duration',
isVisible: true isVisible: true
}, },
{
name: 'language',
label: 'Language',
isVisible: false
},
{ {
name: 'audioInfo', name: 'audioInfo',
label: 'Audio Info', label: 'Audio Info',
@ -63,6 +80,8 @@ export const defaultState = {
}; };
export const persistState = [ export const persistState = [
'tracks.sortKey',
'tracks.sortDirection',
'tracks.columns' 'tracks.columns'
]; ];

@ -102,8 +102,9 @@ class TrackFileEditorModalContentConnector extends Component {
componentDidMount() { componentDidMount() {
const artistId = this.props.artistId; const artistId = this.props.artistId;
const albumId = this.props.albumId;
this.props.dispatchFetchTracks({ artistId }); this.props.dispatchFetchTracks({ artistId, albumId });
this.props.dispatchFetchLanguageProfileSchema(); this.props.dispatchFetchLanguageProfileSchema();
this.props.dispatchFetchQualityProfileSchema(); this.props.dispatchFetchQualityProfileSchema();

@ -48,7 +48,7 @@ function CutoffUnmetRow(props) {
return ( return (
<TableRowCell key={name}> <TableRowCell key={name}>
<ArtistNameLink <ArtistNameLink
nameSlug={artist.nameSlug} foreignArtistId={artist.foreignArtistId}
artistName={artist.artistName} artistName={artist.artistName}
/> />
</TableRowCell> </TableRowCell>

@ -47,7 +47,7 @@ function MissingRow(props) {
return ( return (
<TableRowCell key={name}> <TableRowCell key={name}>
<ArtistNameLink <ArtistNameLink
nameSlug={artist.nameSlug} foreignArtistId={artist.foreignArtistId}
artistName={artist.artistName} artistName={artist.artistName}
/> />
</TableRowCell> </TableRowCell>

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Nancy; using Nancy;
using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Music; using NzbDrone.Core.Music;
using NzbDrone.SignalR; using NzbDrone.SignalR;
@ -21,7 +22,7 @@ namespace Lidarr.Api.V1.Albums
: base(albumService, artistStatisticsService, artistService, upgradableSpecification, signalRBroadcaster) : base(albumService, artistStatisticsService, artistService, upgradableSpecification, signalRBroadcaster)
{ {
GetResourceAll = GetAlbums; GetResourceAll = GetAlbums;
Put[@"/(?<id>[\d]{1,10})"] = x => SetAlbumMonitored(x.Id); UpdateResource = UpdateAlbum;
Put["/monitor"] = x => SetAlbumsMonitored(); Put["/monitor"] = x => SetAlbumsMonitored();
} }
@ -29,8 +30,9 @@ namespace Lidarr.Api.V1.Albums
{ {
var artistIdQuery = Request.Query.ArtistId; var artistIdQuery = Request.Query.ArtistId;
var albumIdsQuery = Request.Query.AlbumIds; var albumIdsQuery = Request.Query.AlbumIds;
var foreignIdQuery = Request.Query.ForeignAlbumId;
if (!Request.Query.ArtistId.HasValue && !albumIdsQuery.HasValue) if (!Request.Query.ArtistId.HasValue && !albumIdsQuery.HasValue && !foreignIdQuery.HasValue)
{ {
return MapToResource(_albumService.GetAllAlbums(), false); return MapToResource(_albumService.GetAllAlbums(), false);
} }
@ -42,6 +44,13 @@ namespace Lidarr.Api.V1.Albums
return MapToResource(_albumService.GetAlbumsByArtist(artistId), false); return MapToResource(_albumService.GetAlbumsByArtist(artistId), false);
} }
if (foreignIdQuery.HasValue)
{
int artistId = _albumService.FindById(foreignIdQuery.Value).ArtistId;
return MapToResource(_albumService.GetAlbumsByArtist(artistId), false);
}
string albumIdsValue = albumIdsQuery.Value.ToString(); string albumIdsValue = albumIdsQuery.Value.ToString();
var albumIds = albumIdsValue.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) var albumIds = albumIdsValue.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
@ -51,12 +60,15 @@ namespace Lidarr.Api.V1.Albums
return MapToResource(_albumService.GetAlbums(albumIds), false); return MapToResource(_albumService.GetAlbums(albumIds), false);
} }
private Response SetAlbumMonitored(int id) private void UpdateAlbum(AlbumResource albumResource)
{ {
var resource = Request.Body.FromJson<AlbumResource>(); var album = _albumService.GetAlbum(albumResource.Id);
_albumService.SetAlbumMonitored(id, resource.Monitored);
var model = albumResource.ToModel(album);
_albumService.UpdateAlbum(model);
return MapToResource(_albumService.GetAlbum(id), false).AsResponse(HttpStatusCode.Accepted); BroadcastResourceChange(ModelAction.Updated, albumResource);
} }
private Response SetAlbumsMonitored() private Response SetAlbumsMonitored()

@ -2,6 +2,8 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using FluentValidation; using FluentValidation;
using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using Lidarr.Api.V1.Artist; using Lidarr.Api.V1.Artist;
using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.DecisionEngine;
@ -9,10 +11,16 @@ using NzbDrone.Core.Music;
using NzbDrone.Core.ArtistStats; using NzbDrone.Core.ArtistStats;
using NzbDrone.SignalR; using NzbDrone.SignalR;
using Lidarr.Http; using Lidarr.Http;
using NzbDrone.Core.Download;
using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Music.Events;
namespace Lidarr.Api.V1.Albums namespace Lidarr.Api.V1.Albums
{ {
public abstract class AlbumModuleWithSignalR : LidarrRestModuleWithSignalR<AlbumResource, Album> public abstract class AlbumModuleWithSignalR : LidarrRestModuleWithSignalR<AlbumResource, Album>,
IHandle<AlbumGrabbedEvent>,
IHandle<AlbumEditedEvent>,
IHandle<TrackImportedEvent>
{ {
protected readonly IAlbumService _albumService; protected readonly IAlbumService _albumService;
protected readonly IArtistStatisticsService _artistStatisticsService; protected readonly IArtistStatisticsService _artistStatisticsService;
@ -130,18 +138,26 @@ namespace Lidarr.Api.V1.Albums
} }
} }
//TODO: Implement Track or Album Grabbed/Dowloaded Events public void Handle(AlbumGrabbedEvent message)
{
foreach (var album in message.Album.Albums)
{
var resource = album.ToResource();
resource.Grabbed = true;
//public void Handle(TrackGrabbedEvent message) BroadcastResourceChange(ModelAction.Updated, resource);
//{ }
// foreach (var track in message.Track.Tracks) }
// {
// var resource = track.ToResource(); public void Handle(AlbumEditedEvent message)
// resource.Grabbed = true; {
BroadcastResourceChange(ModelAction.Updated, message.Album.Id);
}
// BroadcastResourceChange(ModelAction.Updated, resource); public void Handle(TrackImportedEvent message)
// } {
//} BroadcastResourceChange(ModelAction.Updated, message.ImportedTrack.AlbumId);
}
//public void Handle(TrackDownloadedEvent message) //public void Handle(TrackDownloadedEvent message)
//{ //{

@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Core.Music;
namespace Lidarr.Api.V1.Albums
{
public class AlbumReleaseResource
{
public string Id { get; set; }
public DateTime? ReleaseDate { get; set; }
public int TrackCount { get; set; }
public int MediaCount { get; set; }
public string Disambiguation { get; set; }
public List<string> Country { get; set; }
public List<string> Label { get; set; }
public string Format { get; set; }
}
public static class AlbumReleaseResourceMapper
{
public static AlbumReleaseResource ToResource(this AlbumRelease model)
{
if (model == null)
{
return null;
}
return new AlbumReleaseResource
{
Id = model.Id,
ReleaseDate = model.ReleaseDate,
TrackCount = model.TrackCount,
MediaCount = model.MediaCount,
Disambiguation = model.Disambiguation,
Country = model.Country,
Label = model.Label,
Format = model.Format
};
}
public static AlbumRelease ToModel(this AlbumReleaseResource resource)
{
if (resource == null)
{
return null;
}
return new AlbumRelease
{
Id = resource.Id,
ReleaseDate = resource.ReleaseDate,
TrackCount = resource.TrackCount,
MediaCount = resource.MediaCount,
Disambiguation = resource.Disambiguation,
Country = resource.Country,
Label = resource.Label,
Format = resource.Format
};
}
public static List<AlbumReleaseResource> ToResource(this IEnumerable<AlbumRelease> models)
{
return models.Select(ToResource).ToList();
}
public static List<AlbumRelease> ToModel(this IEnumerable<AlbumReleaseResource> resources)
{
return resources.Select(ToModel).ToList();
}
}
}

@ -19,6 +19,7 @@ namespace Lidarr.Api.V1.Albums
public int ProfileId { get; set; } public int ProfileId { get; set; }
public int Duration { get; set; } public int Duration { get; set; }
public string AlbumType { get; set; } public string AlbumType { get; set; }
public List<string> SecondaryTypes { get; set; }
public int MediumCount public int MediumCount
{ {
get get
@ -33,6 +34,8 @@ namespace Lidarr.Api.V1.Albums
} }
public Ratings Ratings { get; set; } public Ratings Ratings { get; set; }
public DateTime? ReleaseDate { get; set; } public DateTime? ReleaseDate { get; set; }
public AlbumRelease CurrentRelease { get; set; }
public List<AlbumReleaseResource> Releases { get; set; }
public List<string> Genres { get; set; } public List<string> Genres { get; set; }
public List<MediumResource> Media { get; set; } public List<MediumResource> Media { get; set; }
public ArtistResource Artist { get; set; } public ArtistResource Artist { get; set; }
@ -65,15 +68,45 @@ namespace Lidarr.Api.V1.Albums
Ratings = model.Ratings, Ratings = model.Ratings,
Duration = model.Duration, Duration = model.Duration,
AlbumType = model.AlbumType, AlbumType = model.AlbumType,
SecondaryTypes = model.SecondaryTypes.Select(s => s.Name).ToList(),
Media = model.Media.ToResource(), Media = model.Media.ToResource(),
CurrentRelease = model.CurrentRelease,
Releases = model.Releases.ToResource(),
}; };
} }
public static Album ToModel(this AlbumResource resource)
{
if (resource == null) return null;
return new Album
{
Id = resource.Id,
ForeignAlbumId = resource.ForeignAlbumId,
Title = resource.Title,
Images = resource.Images,
Monitored = resource.Monitored,
CurrentRelease = resource.CurrentRelease
};
}
public static Album ToModel(this AlbumResource resource, Album album)
{
var updatedAlbum = resource.ToModel();
album.ApplyChanges(updatedAlbum);
return album;
}
public static List<AlbumResource> ToResource(this IEnumerable<Album> models) public static List<AlbumResource> ToResource(this IEnumerable<Album> models)
{ {
if (models == null) return null; return models?.Select(ToResource).ToList();
}
return models.Select(ToResource).ToList(); public static List<Album> ToModel(this IEnumerable<AlbumResource> resources)
{
return resources.Select(ToModel).ToList();
} }
} }
} }

@ -11,7 +11,7 @@ namespace Lidarr.Api.V1.Albums
public string MediumFormat { get; set; } public string MediumFormat { get; set; }
} }
public static class SeasonResourceMapper public static class MediumResourceMapper
{ {
public static MediumResource ToResource(this Medium model) public static MediumResource ToResource(this Medium model)
{ {

@ -34,6 +34,7 @@ namespace Lidarr.Api.V1.Artist
{ {
private readonly IArtistService _artistService; private readonly IArtistService _artistService;
private readonly IAlbumService _albumService;
private readonly IAddArtistService _addArtistService; private readonly IAddArtistService _addArtistService;
private readonly IArtistStatisticsService _artistStatisticsService; private readonly IArtistStatisticsService _artistStatisticsService;
private readonly IMapCoversToLocal _coverMapper; private readonly IMapCoversToLocal _coverMapper;
@ -41,6 +42,7 @@ namespace Lidarr.Api.V1.Artist
public ArtistModule(IBroadcastSignalRMessage signalRBroadcaster, public ArtistModule(IBroadcastSignalRMessage signalRBroadcaster,
IArtistService artistService, IArtistService artistService,
IAlbumService albumService,
IAddArtistService addArtistService, IAddArtistService addArtistService,
IArtistStatisticsService artistStatisticsService, IArtistStatisticsService artistStatisticsService,
IMapCoversToLocal coverMapper, IMapCoversToLocal coverMapper,
@ -57,6 +59,7 @@ namespace Lidarr.Api.V1.Artist
: base(signalRBroadcaster) : base(signalRBroadcaster)
{ {
_artistService = artistService; _artistService = artistService;
_albumService = albumService;
_addArtistService = addArtistService; _addArtistService = addArtistService;
_artistStatisticsService = artistStatisticsService; _artistStatisticsService = artistStatisticsService;
@ -105,6 +108,7 @@ namespace Lidarr.Api.V1.Artist
var resource = artist.ToResource(); var resource = artist.ToResource();
MapCoversToLocal(resource); MapCoversToLocal(resource);
FetchAndLinkArtistStatistics(resource); FetchAndLinkArtistStatistics(resource);
LinkNextPreviousAlbums(resource);
//PopulateAlternateTitles(resource); //PopulateAlternateTitles(resource);
return resource; return resource;
@ -116,7 +120,7 @@ namespace Lidarr.Api.V1.Artist
var artistsResources = _artistService.GetAllArtists().ToResource(); var artistsResources = _artistService.GetAllArtists().ToResource();
MapCoversToLocal(artistsResources.ToArray()); MapCoversToLocal(artistsResources.ToArray());
//MapAlbums(artistsResources.ToArray()); LinkNextPreviousAlbums(artistsResources.ToArray());
LinkArtistStatistics(artistsResources, artistStats); LinkArtistStatistics(artistsResources, artistStats);
//PopulateAlternateTitles(seriesResources); //PopulateAlternateTitles(seriesResources);
@ -171,6 +175,16 @@ namespace Lidarr.Api.V1.Artist
} }
} }
private void LinkNextPreviousAlbums(params ArtistResource[] artists)
{
foreach (var artistResource in artists)
{
var artistAlbums = _albumService.GetAlbumsByArtist(artistResource.Id).OrderBy(s=>s.ReleaseDate);
artistResource.NextAlbum = artistAlbums.Where(s => s.ReleaseDate >= DateTime.UtcNow && s.Monitored).FirstOrDefault();
artistResource.LastAlbum = artistAlbums.Where(s => s.ReleaseDate <= DateTime.UtcNow && s.Monitored).LastOrDefault();
}
}
private void FetchAndLinkArtistStatistics(ArtistResource resource) private void FetchAndLinkArtistStatistics(ArtistResource resource)
{ {
LinkArtistStatistics(resource, _artistStatisticsService.ArtistStatistics(resource.Id)); LinkArtistStatistics(resource, _artistStatisticsService.ArtistStatistics(resource.Id));
@ -195,13 +209,6 @@ namespace Lidarr.Api.V1.Artist
resource.SizeOnDisk = artistStatistics.SizeOnDisk; resource.SizeOnDisk = artistStatistics.SizeOnDisk;
resource.AlbumCount = artistStatistics.AlbumCount; resource.AlbumCount = artistStatistics.AlbumCount;
if (artistStatistics.AlbumStatistics != null)
{
foreach (var album in resource.Albums)
{
album.Statistics = artistStatistics.AlbumStatistics.SingleOrDefault(s => s.AlbumId == album.Id).ToResource();
}
}
} }
//private void PopulateAlternateTitles(List<ArtistResource> resources) //private void PopulateAlternateTitles(List<ArtistResource> resources)

@ -36,13 +36,13 @@ namespace Lidarr.Api.V1.Artist
public int? TrackCount { get; set; } public int? TrackCount { get; set; }
public int? TrackFileCount { get; set; } public int? TrackFileCount { get; set; }
public long? SizeOnDisk { get; set; } public long? SizeOnDisk { get; set; }
//public SeriesStatusType Status { get; set; } public Album NextAlbum { get; set; }
public Album LastAlbum { get; set; }
public List<MediaCover> Images { get; set; } public List<MediaCover> Images { get; set; }
public List<Member> Members { get; set; } public List<Member> Members { get; set; }
public string RemotePoster { get; set; } public string RemotePoster { get; set; }
public List<AlbumResource> Albums { get; set; }
//View & Edit //View & Edit
@ -89,8 +89,6 @@ namespace Lidarr.Api.V1.Artist
Images = model.Images, Images = model.Images,
Albums = model.Albums.ToResource(),
Path = model.Path, Path = model.Path,
QualityProfileId = model.ProfileId, QualityProfileId = model.ProfileId,
LanguageProfileId = model.LanguageProfileId, LanguageProfileId = model.LanguageProfileId,
@ -128,8 +126,6 @@ namespace Lidarr.Api.V1.Artist
Status = resource.Status, Status = resource.Status,
Overview = resource.Overview, Overview = resource.Overview,
//NextAiring
//PreviousAiring
Images = resource.Images, Images = resource.Images,

@ -82,6 +82,7 @@
</Compile> </Compile>
<Compile Include="Albums\AlbumModule.cs" /> <Compile Include="Albums\AlbumModule.cs" />
<Compile Include="Albums\AlbumModuleWithSignalR.cs" /> <Compile Include="Albums\AlbumModuleWithSignalR.cs" />
<Compile Include="Albums\AlbumReleaseResource.cs" />
<Compile Include="Albums\AlbumResource.cs" /> <Compile Include="Albums\AlbumResource.cs" />
<Compile Include="Albums\AlbumsMonitoredResource.cs" /> <Compile Include="Albums\AlbumsMonitoredResource.cs" />
<Compile Include="Albums\AlbumStatisticsResource.cs" /> <Compile Include="Albums\AlbumStatisticsResource.cs" />

@ -12,7 +12,7 @@ namespace Lidarr.Api.V1.TrackFiles
public static class MediaInfoResourceMapper public static class MediaInfoResourceMapper
{ {
public static MediaInfoResource ToResource(this MediaInfoModel model, string sceneName) public static MediaInfoResource ToResource(this MediaInfoModel model)
{ {
if (model == null) if (model == null)
{ {

@ -80,8 +80,9 @@ namespace Lidarr.Api.V1.TrackFiles
{ {
int albumId = Convert.ToInt32(albumIdQuery.Value); int albumId = Convert.ToInt32(albumIdQuery.Value);
var album = _albumService.GetAlbum(albumId); var album = _albumService.GetAlbum(albumId);
var albumArtist = _artistService.GetArtist(album.ArtistId);
return _mediaFileService.GetFilesByAlbum(album.ArtistId, album.Id).ConvertAll(f => f.ToResource(album.Artist, _upgradableSpecification)); return _mediaFileService.GetFilesByAlbum(album.Id).ConvertAll(f => f.ToResource(albumArtist, _upgradableSpecification));
} }
else else

@ -45,7 +45,7 @@ namespace Lidarr.Api.V1.TrackFiles
// SceneName = model.SceneName, // SceneName = model.SceneName,
Language = model.Language, Language = model.Language,
Quality = model.Quality, Quality = model.Quality,
MediaInfo = model.MediaInfo.ToResource(model.SceneName) MediaInfo = model.MediaInfo.ToResource()
//QualityCutoffNotMet //QualityCutoffNotMet
}; };
@ -68,7 +68,7 @@ namespace Lidarr.Api.V1.TrackFiles
//SceneName = model.SceneName, //SceneName = model.SceneName,
Language = model.Language, Language = model.Language,
Quality = model.Quality, Quality = model.Quality,
MediaInfo = model.MediaInfo.ToResource(model.SceneName), MediaInfo = model.MediaInfo.ToResource(),
QualityCutoffNotMet = upgradableSpecification.QualityCutoffNotMet(artist.Profile.Value, model.Quality), QualityCutoffNotMet = upgradableSpecification.QualityCutoffNotMet(artist.Profile.Value, model.Quality),
LanguageCutoffNotMet = upgradableSpecification.LanguageCutoffNotMet(artist.LanguageProfile.Value, model.Language) LanguageCutoffNotMet = upgradableSpecification.LanguageCutoffNotMet(artist.LanguageProfile.Value, model.Language)

@ -2,20 +2,20 @@ using System.Collections.Generic;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download;
using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Music; using NzbDrone.Core.Music;
using NzbDrone.Core.Music.Events;
using NzbDrone.SignalR; using NzbDrone.SignalR;
using Lidarr.Api.V1.TrackFiles; using Lidarr.Api.V1.TrackFiles;
using Lidarr.Api.V1.Artist; using Lidarr.Api.V1.Artist;
using Lidarr.Http; using Lidarr.Http;
using NzbDrone.Core.MediaFiles.Events;
namespace Lidarr.Api.V1.Tracks namespace Lidarr.Api.V1.Tracks
{ {
public abstract class TrackModuleWithSignalR : LidarrRestModuleWithSignalR<TrackResource, Track> public abstract class TrackModuleWithSignalR : LidarrRestModuleWithSignalR<TrackResource, Track>,
//IHandle<EpisodeGrabbedEvent>, IHandle<TrackInfoRefreshedEvent>,
//IHandle<EpisodeImportedEvent> IHandle<TrackImportedEvent>
{ {
protected readonly ITrackService _trackService; protected readonly ITrackService _trackService;
protected readonly IArtistService _artistService; protected readonly IArtistService _artistService;
@ -31,24 +31,24 @@ namespace Lidarr.Api.V1.Tracks
_artistService = artistService; _artistService = artistService;
_upgradableSpecification = upgradableSpecification; _upgradableSpecification = upgradableSpecification;
GetResourceById = GetEpisode; GetResourceById = GetTrack;
} }
protected TrackModuleWithSignalR(ITrackService episodeService, protected TrackModuleWithSignalR(ITrackService trackService,
IArtistService seriesService, IArtistService artistService,
IUpgradableSpecification upgradableSpecification, IUpgradableSpecification upgradableSpecification,
IBroadcastSignalRMessage signalRBroadcaster, IBroadcastSignalRMessage signalRBroadcaster,
string resource) string resource)
: base(signalRBroadcaster, resource) : base(signalRBroadcaster, resource)
{ {
_trackService = episodeService; _trackService = trackService;
_artistService = seriesService; _artistService = artistService;
_upgradableSpecification = upgradableSpecification; _upgradableSpecification = upgradableSpecification;
GetResourceById = GetEpisode; GetResourceById = GetTrack;
} }
protected TrackResource GetEpisode(int id) protected TrackResource GetTrack(int id)
{ {
var episode = _trackService.GetTrack(id); var episode = _trackService.GetTrack(id);
var resource = MapToResource(episode, true, true); var resource = MapToResource(episode, true, true);
@ -76,28 +76,28 @@ namespace Lidarr.Api.V1.Tracks
return resource; return resource;
} }
protected List<TrackResource> MapToResource(List<Track> episodes, bool includeSeries, bool includeEpisodeFile) protected List<TrackResource> MapToResource(List<Track> tracks, bool includeArtist, bool includeTrackFile)
{ {
var result = episodes.ToResource(); var result = tracks.ToResource();
if (includeSeries || includeEpisodeFile) if (includeArtist || includeTrackFile)
{ {
var seriesDict = new Dictionary<int, NzbDrone.Core.Music.Artist>(); var artistDict = new Dictionary<int, NzbDrone.Core.Music.Artist>();
for (var i = 0; i < episodes.Count; i++) for (var i = 0; i < tracks.Count; i++)
{ {
var episode = episodes[i]; var track = tracks[i];
var resource = result[i]; var resource = result[i];
var series = episode.Artist ?? seriesDict.GetValueOrDefault(episodes[i].ArtistId) ?? _artistService.GetArtist(episodes[i].ArtistId); var series = track.Artist ?? artistDict.GetValueOrDefault(tracks[i].ArtistId) ?? _artistService.GetArtist(tracks[i].ArtistId);
seriesDict[series.Id] = series; artistDict[series.Id] = series;
if (includeSeries) if (includeArtist)
{ {
resource.Artist = series.ToResource(); resource.Artist = series.ToResource();
} }
if (includeEpisodeFile && episodes[i].TrackFileId != 0) if (includeTrackFile && tracks[i].TrackFileId != 0)
{ {
resource.TrackFile = episodes[i].TrackFile.Value.ToResource(series, _upgradableSpecification); resource.TrackFile = tracks[i].TrackFile.Value.ToResource(series, _upgradableSpecification);
} }
} }
} }
@ -105,28 +105,31 @@ namespace Lidarr.Api.V1.Tracks
return result; return result;
} }
//public void Handle(EpisodeGrabbedEvent message) public void Handle(TrackInfoRefreshedEvent message)
//{ {
// foreach (var episode in message.Episode.Episodes) foreach (var track in message.Removed)
// { {
// var resource = episode.ToResource(); BroadcastResourceChange(ModelAction.Deleted, track.ToResource());
// resource.Grabbed = true; }
// BroadcastResourceChange(ModelAction.Updated, resource); foreach (var track in message.Added)
// } {
//} BroadcastResourceChange(ModelAction.Updated, track.ToResource());
}
//public void Handle(EpisodeImportedEvent message)
//{ foreach (var track in message.Updated)
// if (!message.NewDownload) {
// { BroadcastResourceChange(ModelAction.Updated, track.Id);
// return; }
// } }
// foreach (var episode in message.EpisodeInfo.Episodes) public void Handle(TrackImportedEvent message)
// { {
// BroadcastResourceChange(ModelAction.Updated, episode.Id); foreach (var track in message.TrackInfo.Tracks)
// } {
//} BroadcastResourceChange(ModelAction.Updated, track.Id);
}
}
} }
} }

@ -16,7 +16,7 @@ namespace NzbDrone.Common.Cloud
Services = new HttpRequestBuilder("https://services.lidarr.audio/v1/") Services = new HttpRequestBuilder("https://services.lidarr.audio/v1/")
.CreateFactory(); .CreateFactory();
Search = new HttpRequestBuilder("https://api.lidarr.audio/api/v0/{route}/") // TODO: Add {version} once LidarrAPI.Metadata is released. Search = new HttpRequestBuilder("https://api.lidarr.audio/api/v0.3/{route}")
.CreateFactory(); .CreateFactory();
} }

@ -71,7 +71,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
_remoteAlbum.Albums = Builder<Album>.CreateListOfSize(1).Build().ToList(); _remoteAlbum.Albums = Builder<Album>.CreateListOfSize(1).Build().ToList();
Mocker.GetMock<IMediaFileService>() Mocker.GetMock<IMediaFileService>()
.Setup(s => s.GetFilesByAlbum(It.IsAny<int>(), It.IsAny<int>())) .Setup(s => s.GetFilesByAlbum(It.IsAny<int>()))
.Returns(new List<TrackFile> { }); .Returns(new List<TrackFile> { });
Mocker.GetMock<IDelayProfileService>() Mocker.GetMock<IDelayProfileService>()
@ -86,7 +86,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
private void GivenExistingFile(QualityModel quality, Language language) private void GivenExistingFile(QualityModel quality, Language language)
{ {
Mocker.GetMock<IMediaFileService>() Mocker.GetMock<IMediaFileService>()
.Setup(s => s.GetFilesByAlbum(It.IsAny<int>(), It.IsAny<int>())) .Setup(s => s.GetFilesByAlbum(It.IsAny<int>()))
.Returns(new List<TrackFile> { new TrackFile { .Returns(new List<TrackFile> { new TrackFile {
Quality = quality, Quality = quality,
Language = language Language = language

@ -91,7 +91,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
private void SetupMediaFile(List<TrackFile> files) private void SetupMediaFile(List<TrackFile> files)
{ {
Mocker.GetMock<IMediaFileService>() Mocker.GetMock<IMediaFileService>()
.Setup(v => v.GetFilesByAlbum(It.IsAny<int>(), It.IsAny<int>())) .Setup(v => v.GetFilesByAlbum(It.IsAny<int>()))
.Returns(files); .Returns(files);
} }

@ -44,7 +44,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
.Build(); .Build();
Mocker.GetMock<IMediaFileService>() Mocker.GetMock<IMediaFileService>()
.Setup(c => c.GetFilesByAlbum(It.IsAny<int>(), It.IsAny<int>())) .Setup(c => c.GetFilesByAlbum(It.IsAny<int>()))
.Returns(new List<TrackFile> { _firstFile, _secondFile }); .Returns(new List<TrackFile> { _firstFile, _secondFile });
_parseResultMulti = new RemoteAlbum _parseResultMulti = new RemoteAlbum

@ -45,7 +45,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
.Build(); .Build();
Mocker.GetMock<IMediaFileService>() Mocker.GetMock<IMediaFileService>()
.Setup(c => c.GetFilesByAlbum(It.IsAny<int>(), It.IsAny<int>())) .Setup(c => c.GetFilesByAlbum(It.IsAny<int>()))
.Returns(new List<TrackFile> { _firstFile, _secondFile }); .Returns(new List<TrackFile> { _firstFile, _secondFile });
_parseResultMulti = new RemoteAlbum _parseResultMulti = new RemoteAlbum
@ -78,7 +78,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
public void should_return_true_if_album_has_no_existing_file() public void should_return_true_if_album_has_no_existing_file()
{ {
Mocker.GetMock<IMediaFileService>() Mocker.GetMock<IMediaFileService>()
.Setup(c => c.GetFilesByAlbum(It.IsAny<int>(), It.IsAny<int>())) .Setup(c => c.GetFilesByAlbum(It.IsAny<int>()))
.Returns(new List<TrackFile> { }); .Returns(new List<TrackFile> { });
Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeTrue(); Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeTrue();

@ -0,0 +1,15 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(9)]
public class album_releases : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Alter.Table("Albums").AddColumn("Releases").AsString().WithDefaultValue("").Nullable();
Alter.Table("Albums").AddColumn("CurrentRelease").AsString().WithDefaultValue("").Nullable();
}
}
}

@ -30,7 +30,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
foreach (var album in subject.Albums) foreach (var album in subject.Albums)
{ {
var trackFiles = _mediaFileService.GetFilesByAlbum(album.ArtistId, album.Id); var trackFiles = _mediaFileService.GetFilesByAlbum(album.Id);
if (trackFiles.Any()) if (trackFiles.Any())
{ {

@ -61,7 +61,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync
{ {
foreach (var album in subject.Albums) foreach (var album in subject.Albums)
{ {
var trackFiles = _mediaFileService.GetFilesByAlbum(album.ArtistId, album.Id); var trackFiles = _mediaFileService.GetFilesByAlbum(album.Id);
if (trackFiles.Any()) if (trackFiles.Any())
{ {

@ -46,7 +46,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync
} }
var missingTrackFiles = subject.Albums var missingTrackFiles = subject.Albums
.SelectMany(v => _albumService.GetFilesByAlbum(v.ArtistId, v.Id)) .SelectMany(v => _albumService.GetFilesByAlbum(v.Id))
.DistinctBy(v => v.Id) .DistinctBy(v => v.Id)
.Where(v => IsTrackFileMissing(subject.Artist, v)) .Where(v => IsTrackFileMissing(subject.Artist, v))
.ToArray(); .ToArray();

@ -35,7 +35,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync
foreach (var album in subject.Albums) foreach (var album in subject.Albums)
{ {
var trackFiles = _mediaFileService.GetFilesByAlbum(album.ArtistId, album.Id); var trackFiles = _mediaFileService.GetFilesByAlbum(album.Id);
if (trackFiles.Any()) if (trackFiles.Any())
{ {

@ -28,7 +28,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
foreach (var album in subject.Albums) foreach (var album in subject.Albums)
{ {
var trackFiles = _mediaFileService.GetFilesByAlbum(album.ArtistId, album.Id); var trackFiles = _mediaFileService.GetFilesByAlbum(album.Id);
if (trackFiles.Any()) if (trackFiles.Any())
{ {

@ -0,0 +1,31 @@
using NzbDrone.Common.Exceptions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace NzbDrone.Core.Exceptions
{
public class AlbumNotFoundException : NzbDroneException
{
public string MusicBrainzId { get; set; }
public AlbumNotFoundException(string musicbrainzId)
: base(string.Format("Album with MusicBrainz {0} was not found, it may have been removed from MusicBrainz.", musicbrainzId))
{
MusicBrainzId = musicbrainzId;
}
public AlbumNotFoundException(string musicbrainzId, string message, params object[] args)
: base(message, args)
{
MusicBrainzId = musicbrainzId;
}
public AlbumNotFoundException(string musicbrainzId, string message)
: base(message)
{
MusicBrainzId = musicbrainzId;
}
}
}

@ -18,7 +18,7 @@ namespace NzbDrone.Core.MediaFiles
void Update(List<TrackFile> trackFile); void Update(List<TrackFile> trackFile);
void Delete(TrackFile trackFile, DeleteMediaFileReason reason); void Delete(TrackFile trackFile, DeleteMediaFileReason reason);
List<TrackFile> GetFilesByArtist(int artistId); List<TrackFile> GetFilesByArtist(int artistId);
List<TrackFile> GetFilesByAlbum(int artistId, int albumId); List<TrackFile> GetFilesByAlbum(int albumId);
List<TrackFile> GetFiles(IEnumerable<int> ids); List<TrackFile> GetFiles(IEnumerable<int> ids);
List<TrackFile> GetFilesWithoutMediaInfo(); List<TrackFile> GetFilesWithoutMediaInfo();
List<string> FilterExistingFiles(List<string> files, Artist artist); List<string> FilterExistingFiles(List<string> files, Artist artist);
@ -27,7 +27,7 @@ namespace NzbDrone.Core.MediaFiles
} }
public class MediaFileService : IMediaFileService, IHandleAsync<ArtistDeletedEvent> public class MediaFileService : IMediaFileService, IHandleAsync<ArtistDeletedEvent>, IHandleAsync<AlbumDeletedEvent>
{ {
private readonly IEventAggregator _eventAggregator; private readonly IEventAggregator _eventAggregator;
private readonly IMediaFileRepository _mediaFileRepository; private readonly IMediaFileRepository _mediaFileRepository;
@ -104,12 +104,18 @@ namespace NzbDrone.Core.MediaFiles
_mediaFileRepository.DeleteMany(files); _mediaFileRepository.DeleteMany(files);
} }
public void HandleAsync(AlbumDeletedEvent message)
{
var files = GetFilesByAlbum(message.Album.Id);
_mediaFileRepository.DeleteMany(files);
}
public List<TrackFile> GetFilesByArtist(int artistId) public List<TrackFile> GetFilesByArtist(int artistId)
{ {
return _mediaFileRepository.GetFilesByArtist(artistId); return _mediaFileRepository.GetFilesByArtist(artistId);
} }
public List<TrackFile> GetFilesByAlbum(int artistId, int albumId) public List<TrackFile> GetFilesByAlbum(int albumId)
{ {
return _mediaFileRepository.GetFilesByAlbum(albumId); return _mediaFileRepository.GetFilesByAlbum(albumId);
} }

@ -72,7 +72,7 @@ namespace NzbDrone.Core.MediaFiles
var artist = _artistService.GetArtist(artistId); var artist = _artistService.GetArtist(artistId);
var tracks = _trackService.GetTracksByAlbum(albumId); var tracks = _trackService.GetTracksByAlbum(albumId);
var files = _mediaFileService.GetFilesByAlbum(artistId, albumId); var files = _mediaFileService.GetFilesByAlbum(albumId);
return GetPreviews(artist, tracks, files) return GetPreviews(artist, tracks, files)
.OrderByDescending(e => e.TrackNumbers.First()).ToList(); .OrderByDescending(e => e.TrackNumbers.First()).ToList();

@ -0,0 +1,11 @@
using NzbDrone.Core.Music;
using System;
using System.Collections.Generic;
namespace NzbDrone.Core.MetadataSource
{
public interface IProvideAlbumInfo
{
Tuple<Album, List<Track>> GetAlbumInfo(string lidarrId, string releaseId);
}
}

@ -25,6 +25,8 @@ namespace NzbDrone.Core.MetadataSource.SkyHook.Resource
public List<string> SecondaryTypes { get; set; } public List<string> SecondaryTypes { get; set; }
public List<MediumResource> Media { get; set; } public List<MediumResource> Media { get; set; }
public List<TrackResource> Tracks { get; set; } public List<TrackResource> Tracks { get; set; }
public List<ReleaseResource> Releases { get; set; }
public string SelectedRelease { get; set; }
} }

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
namespace NzbDrone.Core.MetadataSource.SkyHook.Resource
{
public class ReleaseResource
{
public string Id { get; set; }
public DateTime ReleaseDate { get; set; }
public int MediaCount { get; set; }
public int TrackCount { get; set; }
public string Disambiguation { get; set; }
public List<string> Label {get; set;}
public List<string> Country { get; set; }
public string Format { get; set; }
}
}

@ -9,16 +9,13 @@ using NzbDrone.Common.Http;
using NzbDrone.Core.Exceptions; using NzbDrone.Core.Exceptions;
using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaCover;
using NzbDrone.Core.MetadataSource.SkyHook.Resource; using NzbDrone.Core.MetadataSource.SkyHook.Resource;
using Newtonsoft.Json.Linq;
using NzbDrone.Core.Music; using NzbDrone.Core.Music;
using Newtonsoft.Json;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using System.Text.RegularExpressions;
using NzbDrone.Core.Profiles.Metadata; using NzbDrone.Core.Profiles.Metadata;
namespace NzbDrone.Core.MetadataSource.SkyHook namespace NzbDrone.Core.MetadataSource.SkyHook
{ {
public class SkyHookProxy : IProvideArtistInfo, ISearchForNewArtist public class SkyHookProxy : IProvideArtistInfo, ISearchForNewArtist, IProvideAlbumInfo
{ {
private readonly IHttpClient _httpClient; private readonly IHttpClient _httpClient;
private readonly Logger _logger; private readonly Logger _logger;
@ -28,9 +25,13 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
private readonly IConfigService _configService; private readonly IConfigService _configService;
private readonly IMetadataProfileService _metadataProfileService; private readonly IMetadataProfileService _metadataProfileService;
private IHttpRequestBuilderFactory customerRequestBuilder; private IHttpRequestBuilderFactory _customerRequestBuilder;
public SkyHookProxy(IHttpClient httpClient, ILidarrCloudRequestBuilder requestBuilder, IArtistService artistService, Logger logger, IConfigService configService, IMetadataProfileService metadataProfileService) public SkyHookProxy(IHttpClient httpClient,
ILidarrCloudRequestBuilder requestBuilder,
IArtistService artistService, Logger logger,
IConfigService configService,
IMetadataProfileService metadataProfileService)
{ {
_httpClient = httpClient; _httpClient = httpClient;
_configService = configService; _configService = configService;
@ -52,8 +53,8 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
var primaryTypes = metadataProfile.PrimaryAlbumTypes.Where(s => s.Allowed).Select(s => s.PrimaryAlbumType.Name); var primaryTypes = metadataProfile.PrimaryAlbumTypes.Where(s => s.Allowed).Select(s => s.PrimaryAlbumType.Name);
var secondaryTypes = metadataProfile.SecondaryAlbumTypes.Where(s => s.Allowed).Select(s => s.SecondaryAlbumType.Name); var secondaryTypes = metadataProfile.SecondaryAlbumTypes.Where(s => s.Allowed).Select(s => s.SecondaryAlbumType.Name);
var httpRequest = customerRequestBuilder.Create() var httpRequest = _customerRequestBuilder.Create()
.SetSegment("route", "artists/" + foreignArtistId) .SetSegment("route", "artist/" + foreignArtistId)
.AddQueryParam("primTypes", string.Join("|", primaryTypes)) .AddQueryParam("primTypes", string.Join("|", primaryTypes))
.AddQueryParam("secTypes", string.Join("|", secondaryTypes)) .AddQueryParam("secTypes", string.Join("|", secondaryTypes))
.Build(); .Build();
@ -86,16 +87,55 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
return new Tuple<Artist, List<Album>>(artist, albums.ToList()); return new Tuple<Artist, List<Album>>(artist, albums.ToList());
} }
public Tuple<Album, List<Track>> GetAlbumInfo(string foreignAlbumId, string releaseId)
{
_logger.Debug("Getting Album with LidarrAPI.MetadataID of {0}", foreignAlbumId);
SetCustomProvider();
var httpRequest = _customerRequestBuilder.Create()
.SetSegment("route", "album/" + foreignAlbumId)
.AddQueryParam("release", releaseId ?? string.Empty)
.Build();
httpRequest.AllowAutoRedirect = true;
httpRequest.SuppressHttpError = true;
var httpResponse = _httpClient.Get<AlbumResource>(httpRequest);
if (httpResponse.HasHttpError)
{
if (httpResponse.StatusCode == HttpStatusCode.NotFound)
{
throw new AlbumNotFoundException(foreignAlbumId);
}
else if (httpResponse.StatusCode == HttpStatusCode.BadRequest)
{
throw new BadRequestException(foreignAlbumId);
}
else
{
throw new HttpException(httpRequest, httpResponse);
}
}
var tracks = httpResponse.Resource.Tracks.Select(MapTrack);
var album = MapAlbum(httpResponse.Resource);
return new Tuple<Album, List<Track>>(album, tracks.ToList());
}
public List<Artist> SearchForNewArtist(string title) public List<Artist> SearchForNewArtist(string title)
{ {
try try
{ {
var lowerTitle = title.ToLowerInvariant(); var lowerTitle = title.ToLowerInvariant();
Console.WriteLine("Searching for " + lowerTitle);
if (lowerTitle.StartsWith("lidarr:") || lowerTitle.StartsWith("lidarrid:")) if (lowerTitle.StartsWith("lidarr:") || lowerTitle.StartsWith("lidarrid:"))
{ {
var slug = lowerTitle.Split(':')[1].Trim(); var slug = lowerTitle.Split(':')[1].Trim();
Guid searchGuid; Guid searchGuid;
bool isValid = Guid.TryParse(slug, out searchGuid); bool isValid = Guid.TryParse(slug, out searchGuid);
@ -118,10 +158,12 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
SetCustomProvider(); SetCustomProvider();
var httpRequest = customerRequestBuilder.Create() var httpRequest = _customerRequestBuilder.Create()
.SetSegment("route", "search") .SetSegment("route", "search")
.AddQueryParam("type", "artist") .AddQueryParam("type", "artist")
.AddQueryParam("query", title.ToLower().Trim()) .AddQueryParam("query", title.ToLower().Trim())
//.AddQueryParam("images","false") // Should pass these on import search to avoid looking to fanart and wiki
//.AddQueryParam("overview","false")
.Build(); .Build();
@ -143,12 +185,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
private Artist MapSearhResult(ArtistResource resource) private Artist MapSearhResult(ArtistResource resource)
{ {
var artist = _artistService.FindById(resource.Id); var artist = _artistService.FindById(resource.Id) ?? MapArtist(resource);
if (artist == null)
{
artist = MapArtist(resource);
}
return artist; return artist;
} }
@ -161,37 +198,66 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
album.ReleaseDate = resource.ReleaseDate; album.ReleaseDate = resource.ReleaseDate;
album.CleanTitle = Parser.Parser.CleanArtistName(album.Title); album.CleanTitle = Parser.Parser.CleanArtistName(album.Title);
album.AlbumType = resource.Type; album.AlbumType = resource.Type;
album.Images = resource.Images.Select(MapImage).ToList();
album.Label = resource.Labels;
if (resource.Images != null)
{
album.Images = resource.Images.Select(MapImage).ToList();
}
album.Label = resource.Labels;
album.Media = resource.Media.Select(MapMedium).ToList(); album.Media = resource.Media.Select(MapMedium).ToList();
album.Tracks = resource.Tracks.Select(MapTrack).ToList();
album.SecondaryTypes = resource.SecondaryTypes.Select(MapSecondaryTypes).ToList(); album.SecondaryTypes = resource.SecondaryTypes.Select(MapSecondaryTypes).ToList();
if (resource.Releases != null)
{
album.Releases = resource.Releases.Select(MapAlbumRelease).ToList();
album.CurrentRelease = album.Releases.FirstOrDefault(s => s.Id == resource.SelectedRelease);
}
return album; return album;
} }
private static Medium MapMedium(MediumResource resource) private static Medium MapMedium(MediumResource resource)
{ {
Medium medium = new Medium(); Medium medium = new Medium
medium.Name = resource.Name; {
medium.Number = resource.Position; Name = resource.Name,
medium.Format = resource.Format; Number = resource.Position,
Format = resource.Format
};
return medium; return medium;
} }
private static Track MapTrack(TrackResource resource) private static AlbumRelease MapAlbumRelease(ReleaseResource resource)
{ {
Track track = new Track(); AlbumRelease albumRelease = new AlbumRelease
track.Title = resource.TrackName; {
track.ForeignTrackId = resource.Id; Id = resource.Id,
track.TrackNumber = resource.TrackNumber; ReleaseDate = resource.ReleaseDate,
track.AbsoluteTrackNumber = resource.TrackPosition; TrackCount = resource.TrackCount,
track.Duration = resource.DurationMs; Format = resource.Format,
track.MediumNumber = resource.MediumNumber; MediaCount = resource.MediaCount,
Country = resource.Country,
Disambiguation = resource.Disambiguation,
Label = resource.Label
};
return albumRelease;
}
private static Track MapTrack(TrackResource resource)
{
Track track = new Track
{
Title = resource.TrackName,
ForeignTrackId = resource.Id,
TrackNumber = resource.TrackNumber,
AbsoluteTrackNumber = resource.TrackPosition,
Duration = resource.DurationMs,
MediumNumber = resource.MediumNumber
};
return track; return track;
} }
@ -213,7 +279,6 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
artist.Status = MapArtistStatus(resource.Status); artist.Status = MapArtistStatus(resource.Status);
artist.Ratings = MapRatings(resource.Rating); artist.Ratings = MapRatings(resource.Rating);
artist.Links = resource.Links.Select(MapLink).ToList(); artist.Links = resource.Links.Select(MapLink).ToList();
return artist; return artist;
} }
@ -251,14 +316,14 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
return ArtistStatusType.Continuing; return ArtistStatusType.Continuing;
} }
private static Music.Ratings MapRatings(RatingResource rating) private static Ratings MapRatings(RatingResource rating)
{ {
if (rating == null) if (rating == null)
{ {
return new Music.Ratings(); return new Ratings();
} }
return new Music.Ratings return new Ratings
{ {
Votes = rating.Count, Votes = rating.Count,
Value = rating.Value Value = rating.Value
@ -274,9 +339,9 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
}; };
} }
private static Music.Links MapLink(LinkResource arg) private static Links MapLink(LinkResource arg)
{ {
return new Music.Links return new Links
{ {
Url = arg.Target, Url = arg.Target,
Name = arg.Type Name = arg.Type
@ -337,11 +402,11 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
{ {
if (_configService.MetadataSource.IsNotNullOrWhiteSpace()) if (_configService.MetadataSource.IsNotNullOrWhiteSpace())
{ {
customerRequestBuilder = new HttpRequestBuilder(_configService.MetadataSource.TrimEnd("/") + "/{route}/").CreateFactory(); _customerRequestBuilder = new HttpRequestBuilder(_configService.MetadataSource.TrimEnd("/") + "/{route}").CreateFactory();
} }
else else
{ {
customerRequestBuilder = _requestBuilder; _customerRequestBuilder = _requestBuilder;
} }
} }
} }

@ -39,10 +39,26 @@ namespace NzbDrone.Core.Music
public AddArtistOptions AddOptions { get; set; } public AddArtistOptions AddOptions { get; set; }
public Artist Artist { get; set; } public Artist Artist { get; set; }
public Ratings Ratings { get; set; } public Ratings Ratings { get; set; }
public List<AlbumRelease> Releases { get; set; }
public AlbumRelease CurrentRelease { get; set; }
public override string ToString() public override string ToString()
{ {
return string.Format("[{0}][{1}]", ForeignAlbumId, Title.NullSafe()); return string.Format("[{0}][{1}]", ForeignAlbumId, Title.NullSafe());
} }
public void ApplyChanges(Album otherAlbum)
{
ForeignAlbumId = otherAlbum.ForeignAlbumId;
Tracks = otherAlbum.Tracks;
ProfileId = otherAlbum.ProfileId;
AddOptions = otherAlbum.AddOptions;
Monitored = otherAlbum.Monitored;
CurrentRelease = otherAlbum.CurrentRelease;
}
} }
} }

@ -0,0 +1,25 @@
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Music.Commands;
using NzbDrone.Core.Music.Events;
namespace NzbDrone.Core.Music
{
public class AlbumEditedService : IHandle<AlbumEditedEvent>
{
private readonly IManageCommandQueue _commandQueueManager;
public AlbumEditedService(IManageCommandQueue commandQueueManager)
{
_commandQueueManager = commandQueueManager;
}
public void Handle(AlbumEditedEvent message)
{
if (message.Album.CurrentRelease.Id != message.OldAlbum.CurrentRelease.Id)
{
_commandQueueManager.Push(new RefreshAlbumCommand(message.Album.Id));
}
}
}
}

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.Music
{
public class AlbumRelease : IEmbeddedDocument
{
public string Id { get; set; }
public DateTime? ReleaseDate { get; set; }
public int TrackCount { get; set; }
public int MediaCount { get; set; }
public string Disambiguation { get; set; }
public List<string> Country { get; set; }
public string Format { get; set; }
public List<string> Label { get; set; }
}
}

@ -19,6 +19,7 @@ namespace NzbDrone.Core.Music
List<Album> GetAlbums(IEnumerable<int> albumIds); List<Album> GetAlbums(IEnumerable<int> albumIds);
List<Album> GetAlbumsByArtist(int artistId); List<Album> GetAlbumsByArtist(int artistId);
Album AddAlbum(Album newAlbum); Album AddAlbum(Album newAlbum);
List<Album> AddAlbums(List<Album> newAlbums);
Album FindById(string spotifyId); Album FindById(string spotifyId);
Album FindByTitle(int artistId, string title); Album FindByTitle(int artistId, string title);
Album FindByTitleInexact(string title); Album FindByTitleInexact(string title);
@ -61,11 +62,19 @@ namespace NzbDrone.Core.Music
public Album AddAlbum(Album newAlbum) public Album AddAlbum(Album newAlbum)
{ {
_albumRepository.Insert(newAlbum); _albumRepository.Insert(newAlbum);
_eventAggregator.PublishEvent(new AlbumAddedEvent(GetAlbum(newAlbum.Id))); //_eventAggregator.PublishEvent(new AlbumAddedEvent(GetAlbum(newAlbum.Id)));
return newAlbum; return newAlbum;
} }
public List<Album> AddAlbums(List<Album> newAlbums)
{
_albumRepository.InsertMany(newAlbums);
//_eventAggregator.PublishEvent(new AlbumsAddedEvent(newAlbums.Select(s => s.Id).ToList()));
return newAlbums;
}
public void DeleteAlbum(int albumId, bool deleteFiles) public void DeleteAlbum(int albumId, bool deleteFiles)
{ {
var album = _albumRepository.Get(albumId); var album = _albumRepository.Get(albumId);
@ -140,11 +149,16 @@ namespace NzbDrone.Core.Music
public void DeleteMany(List<Album> albums) public void DeleteMany(List<Album> albums)
{ {
_albumRepository.DeleteMany(albums); _albumRepository.DeleteMany(albums);
foreach (var album in albums)
{
_eventAggregator.PublishEvent(new AlbumDeletedEvent(album, false));
}
} }
public Album UpdateAlbum(Album album) public Album UpdateAlbum(Album album)
{ {
var storedAlbum = GetAlbum(album.Id); // Is it Id or iTunesId? var storedAlbum = GetAlbum(album.Id);
var updatedAlbum = _albumRepository.Update(album); var updatedAlbum = _albumRepository.Update(album);
_eventAggregator.PublishEvent(new AlbumEditedEvent(updatedAlbum, storedAlbum)); _eventAggregator.PublishEvent(new AlbumEditedEvent(updatedAlbum, storedAlbum));

@ -0,0 +1,22 @@
using NzbDrone.Core.Messaging.Commands;
namespace NzbDrone.Core.Music.Commands
{
public class RefreshAlbumCommand : Command
{
public int? AlbumId { get; set; }
public RefreshAlbumCommand()
{
}
public RefreshAlbumCommand(int? albumId)
{
AlbumId = albumId;
}
public override bool SendUpdatesToClient => true;
public override bool UpdateScheduledTask => !AlbumId.HasValue;
}
}

@ -0,0 +1,15 @@
using System.Collections.Generic;
using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.Music.Events
{
public class AlbumsAddedEvent : IEvent
{
public List<int> AlbumIds { get; private set; }
public AlbumsAddedEvent(List<int> albumIds)
{
AlbumIds = albumIds;
}
}
}

@ -1,4 +1,4 @@
using NzbDrone.Common.Messaging; using NzbDrone.Common.Messaging;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
@ -12,12 +12,14 @@ namespace NzbDrone.Core.Music.Events
public Album Album { get; set; } public Album Album { get; set; }
public ReadOnlyCollection<Track> Added { get; private set; } public ReadOnlyCollection<Track> Added { get; private set; }
public ReadOnlyCollection<Track> Updated { get; private set; } public ReadOnlyCollection<Track> Updated { get; private set; }
public ReadOnlyCollection<Track> Removed { get; private set; }
public TrackInfoRefreshedEvent(Album album, IList<Track> added, IList<Track> updated) public TrackInfoRefreshedEvent(Album album, IList<Track> added, IList<Track> updated, IList<Track> removed)
{ {
Album = album; Album = album;
Added = new ReadOnlyCollection<Track>(added); Added = new ReadOnlyCollection<Track>(added);
Updated = new ReadOnlyCollection<Track>(updated); Updated = new ReadOnlyCollection<Track>(updated);
Removed = new ReadOnlyCollection<Track>(removed);
} }
} }
} }

@ -7,31 +7,104 @@ using System.Collections.Generic;
using NzbDrone.Core.Organizer; using NzbDrone.Core.Organizer;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using NzbDrone.Core.MetadataSource;
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Core.Exceptions;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Music.Commands;
namespace NzbDrone.Core.Music namespace NzbDrone.Core.Music
{ {
public interface IRefreshAlbumService public interface IRefreshAlbumService
{ {
void RefreshAlbumInfo(Artist artist, IEnumerable<Album> remoteAlbums); void RefreshAlbumInfo(Artist artist, IEnumerable<Album> remoteAlbums);
void RefreshAlbumInfo(Album album);
} }
public class RefreshAlbumService : IRefreshAlbumService public class RefreshAlbumService : IRefreshAlbumService, IExecute<RefreshAlbumCommand>
{ {
private readonly IAlbumService _albumService; private readonly IAlbumService _albumService;
private readonly IArtistService _artistService;
private readonly IProvideAlbumInfo _albumInfo;
private readonly IRefreshTrackService _refreshTrackService; private readonly IRefreshTrackService _refreshTrackService;
private readonly ITrackService _trackService;
private readonly IBuildFileNames _fileNameBuilder; private readonly IBuildFileNames _fileNameBuilder;
private readonly IEventAggregator _eventAggregator; private readonly IEventAggregator _eventAggregator;
private readonly Logger _logger; private readonly Logger _logger;
public RefreshAlbumService(IAlbumService albumService, IBuildFileNames fileNameBuilder, IRefreshTrackService refreshTrackService, IEventAggregator eventAggregator, Logger logger) public RefreshAlbumService(IAlbumService albumService,
IArtistService artistService,
IProvideAlbumInfo albumInfo,
IRefreshTrackService refreshTrackService,
ITrackService trackService,
IBuildFileNames fileNameBuilder,
IEventAggregator eventAggregator,
Logger logger)
{ {
_albumService = albumService; _albumService = albumService;
_fileNameBuilder = fileNameBuilder; _artistService = artistService;
_albumInfo = albumInfo;
_refreshTrackService = refreshTrackService; _refreshTrackService = refreshTrackService;
_trackService = trackService;
_fileNameBuilder = fileNameBuilder;
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
_logger = logger; _logger = logger;
} }
public void RefreshAlbumInfo(Album album)
{
_logger.ProgressInfo("Updating Info for {0}", album.Title);
Tuple<Album, List<Track>> tuple;
try
{
tuple = _albumInfo.GetAlbumInfo(album.ForeignAlbumId, album.CurrentRelease.Id);
}
catch (AlbumNotFoundException)
{
_logger.Error(
"Album '{0}' (LidarrAPI {1}) was not found, it may have been removed from Metadata sources.",
album.Title, album.ForeignAlbumId);
return;
}
var albumInfo = tuple.Item1;
if (album.ForeignAlbumId != albumInfo.ForeignAlbumId)
{
_logger.Warn(
"Album '{0}' (Album {1}) was replaced with '{2}' (LidarrAPI {3}), because the original was a duplicate.",
album.Title, album.ForeignAlbumId, albumInfo.Title, albumInfo.ForeignAlbumId);
album.ForeignAlbumId = albumInfo.ForeignAlbumId;
}
album.LastInfoSync = DateTime.UtcNow;
album.CleanTitle = albumInfo.CleanTitle;
album.Title = albumInfo.Title ?? "Unknown";
album.CleanTitle = Parser.Parser.CleanArtistName(album.Title);
album.AlbumType = albumInfo.AlbumType;
album.SecondaryTypes = albumInfo.SecondaryTypes;
album.Genres = albumInfo.Genres;
album.Media = albumInfo.Media;
album.Label = albumInfo.Label;
album.Images = albumInfo.Images;
album.ReleaseDate = albumInfo.ReleaseDate;
album.Duration = tuple.Item2.Sum(track => track.Duration);
album.Releases = albumInfo.Releases;
_refreshTrackService.RefreshTrackInfo(album, tuple.Item2);
_albumService.UpdateAlbum(album);
}
public void RefreshAlbumInfo(Artist artist, IEnumerable<Album> remoteAlbums) public void RefreshAlbumInfo(Artist artist, IEnumerable<Album> remoteAlbums)
{ {
_logger.Info("Starting album info refresh for: {0}", artist); _logger.Info("Starting album info refresh for: {0}", artist);
@ -42,67 +115,80 @@ namespace NzbDrone.Core.Music
var updateList = new List<Album>(); var updateList = new List<Album>();
var newList = new List<Album>(); var newList = new List<Album>();
var dupeFreeRemoteAlbums = remoteAlbums.DistinctBy(m => new { m.ForeignAlbumId, m.ReleaseDate }).ToList(); var dupeFreeRemoteAlbums = remoteAlbums.DistinctBy(m => new {m.ForeignAlbumId, m.ReleaseDate}).ToList();
foreach (var album in OrderAlbums(artist, dupeFreeRemoteAlbums)) foreach (var album in OrderAlbums(artist, dupeFreeRemoteAlbums))
{ {
try try
{ {
var albumToUpdate = GetAlbumToUpdate(artist, album, existingAlbums); var albumToUpdate = GetAlbumToUpdate(artist, album, existingAlbums);
Tuple<Album, List<Track>> tuple;
var albumInfo = new Album();
if (albumToUpdate != null) if (albumToUpdate != null)
{ {
tuple = _albumInfo.GetAlbumInfo(album.ForeignAlbumId, albumToUpdate.CurrentRelease.Id);
albumInfo = tuple.Item1;
existingAlbums.Remove(albumToUpdate); existingAlbums.Remove(albumToUpdate);
updateList.Add(albumToUpdate); updateList.Add(albumToUpdate);
} }
else else
{ {
albumToUpdate = new Album(); tuple = _albumInfo.GetAlbumInfo(album.ForeignAlbumId, null);
albumToUpdate.Monitored = artist.Monitored; albumInfo = tuple.Item1;
albumToUpdate.ProfileId = artist.ProfileId; albumToUpdate = new Album
albumToUpdate.Added = DateTime.UtcNow; {
Monitored = artist.Monitored,
ProfileId = artist.ProfileId,
Added = DateTime.UtcNow
};
albumToUpdate.ArtistId = artist.Id;
albumToUpdate.CleanTitle = albumInfo.CleanTitle;
albumToUpdate.ForeignAlbumId = albumInfo.ForeignAlbumId;
albumToUpdate.Title = albumInfo.Title ?? "Unknown";
albumToUpdate.AlbumType = albumInfo.AlbumType;
_albumService.AddAlbum(albumToUpdate);
newList.Add(albumToUpdate); newList.Add(albumToUpdate);
} }
albumToUpdate.ForeignAlbumId = album.ForeignAlbumId;
albumToUpdate.LastInfoSync = DateTime.UtcNow; albumToUpdate.LastInfoSync = DateTime.UtcNow;
albumToUpdate.CleanTitle = album.CleanTitle; albumToUpdate.CleanTitle = albumInfo.CleanTitle;
albumToUpdate.Title = album.Title ?? "Unknown"; albumToUpdate.Title = albumInfo.Title ?? "Unknown";
albumToUpdate.CleanTitle = Parser.Parser.CleanArtistName(albumToUpdate.Title); albumToUpdate.CleanTitle = Parser.Parser.CleanArtistName(albumToUpdate.Title);
albumToUpdate.ArtistId = artist.Id; albumToUpdate.AlbumType = albumInfo.AlbumType;
albumToUpdate.AlbumType = album.AlbumType; albumToUpdate.SecondaryTypes = albumInfo.SecondaryTypes;
albumToUpdate.SecondaryTypes = album.SecondaryTypes; albumToUpdate.Genres = albumInfo.Genres;
albumToUpdate.Genres = album.Genres; albumToUpdate.Media = albumInfo.Media;
albumToUpdate.Media = album.Media; albumToUpdate.Label = albumInfo.Label;
albumToUpdate.Label = album.Label; albumToUpdate.Images = albumInfo.Images;
albumToUpdate.Images = album.Images; albumToUpdate.ReleaseDate = albumInfo.ReleaseDate;
albumToUpdate.ReleaseDate = album.ReleaseDate; albumToUpdate.Duration = tuple.Item2.Sum(track => track.Duration);
albumToUpdate.Duration = album.Tracks.Sum(track => track.Duration); albumToUpdate.Releases = albumInfo.Releases;
albumToUpdate.CurrentRelease = albumInfo.CurrentRelease;
_refreshTrackService.RefreshTrackInfo(albumToUpdate, tuple.Item2);
successCount++; successCount++;
} }
catch (Exception e) catch (Exception e)
{ {
_logger.Fatal(e, "An error has occurred while updating track info for artist {0}. {1}", artist, album); _logger.Fatal(e, "An error has occurred while updating album info for artist {0}. {1}", artist,
album);
failCount++; failCount++;
} }
} }
var allAlbums = new List<Album>();
allAlbums.AddRange(newList);
allAlbums.AddRange(updateList);
// TODO: See if anything needs to be done here
//AdjustMultiEpisodeAirTime(artist, allTracks);
//AdjustDirectToDvdAirDate(artist, allTracks);
_albumService.DeleteMany(existingAlbums); _albumService.DeleteMany(existingAlbums);
_albumService.UpdateMany(updateList); _albumService.UpdateMany(updateList);
_albumService.InsertMany(newList); _albumService.UpdateMany(newList);
_eventAggregator.PublishEvent(new AlbumInfoRefreshedEvent(artist, newList, updateList)); _eventAggregator.PublishEvent(new AlbumInfoRefreshedEvent(artist, newList, updateList));
if (failCount != 0) if (failCount != 0)
@ -125,13 +211,26 @@ namespace NzbDrone.Core.Music
private Album GetAlbumToUpdate(Artist artist, Album album, List<Album> existingAlbums) private Album GetAlbumToUpdate(Artist artist, Album album, List<Album> existingAlbums)
{ {
return existingAlbums.FirstOrDefault(e => e.ForeignAlbumId == album.ForeignAlbumId/* && e.ReleaseDate == album.ReleaseDate*/); return existingAlbums.FirstOrDefault(
e => e.ForeignAlbumId == album.ForeignAlbumId /* && e.ReleaseDate == album.ReleaseDate*/);
} }
private IEnumerable<Album> OrderAlbums(Artist artist, List<Album> albums) private IEnumerable<Album> OrderAlbums(Artist artist, List<Album> albums)
{ {
return albums.OrderBy(e => e.ForeignAlbumId).ThenBy(e => e.ReleaseDate); return albums.OrderBy(e => e.ForeignAlbumId).ThenBy(e => e.ReleaseDate);
} }
public void Execute(RefreshAlbumCommand message)
{
if (message.AlbumId.HasValue)
{
var album = _albumService.GetAlbum(message.AlbumId.Value);
var artist = _artistService.GetArtist(album.ArtistId);
RefreshAlbumInfo(album);
_eventAggregator.PublishEvent(new ArtistUpdatedEvent(artist));
}
}
} }
} }

@ -20,6 +20,7 @@ namespace NzbDrone.Core.Music
{ {
private readonly IProvideArtistInfo _artistInfo; private readonly IProvideArtistInfo _artistInfo;
private readonly IArtistService _artistService; private readonly IArtistService _artistService;
private readonly IAlbumService _albumService;
private readonly IRefreshAlbumService _refreshAlbumService; private readonly IRefreshAlbumService _refreshAlbumService;
private readonly IRefreshTrackService _refreshTrackService; private readonly IRefreshTrackService _refreshTrackService;
private readonly IEventAggregator _eventAggregator; private readonly IEventAggregator _eventAggregator;
@ -29,6 +30,7 @@ namespace NzbDrone.Core.Music
public RefreshArtistService(IProvideArtistInfo artistInfo, public RefreshArtistService(IProvideArtistInfo artistInfo,
IArtistService artistService, IArtistService artistService,
IAlbumService albumService,
IRefreshAlbumService refreshAlbumService, IRefreshAlbumService refreshAlbumService,
IRefreshTrackService refreshTrackService, IRefreshTrackService refreshTrackService,
IEventAggregator eventAggregator, IEventAggregator eventAggregator,
@ -38,6 +40,7 @@ namespace NzbDrone.Core.Music
{ {
_artistInfo = artistInfo; _artistInfo = artistInfo;
_artistService = artistService; _artistService = artistService;
_albumService = albumService;
_refreshAlbumService = refreshAlbumService; _refreshAlbumService = refreshAlbumService;
_refreshTrackService = refreshTrackService; _refreshTrackService = refreshTrackService;
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
@ -92,52 +95,13 @@ namespace NzbDrone.Core.Music
{ {
_logger.Warn(e, "Couldn't update artist path for " + artist.Path); _logger.Warn(e, "Couldn't update artist path for " + artist.Path);
} }
//artist.Albums = UpdateAlbums(artist, artistInfo); # We don't need this since we don't store albums in artist table.
_artistService.UpdateArtist(artist);
_refreshAlbumService.RefreshAlbumInfo(artist, tuple.Item2);
foreach (var album in tuple.Item2)
{
_refreshTrackService.RefreshTrackInfo(album, album.Tracks);
}
_refreshAlbumService.RefreshAlbumInfo(artist, tuple.Item2);
_logger.Debug("Finished artist refresh for {0}", artist.Name); _logger.Debug("Finished artist refresh for {0}", artist.Name);
_eventAggregator.PublishEvent(new ArtistUpdatedEvent(artist)); _eventAggregator.PublishEvent(new ArtistUpdatedEvent(artist));
} }
private List<Album> UpdateAlbums(Artist artist, Artist artistInfo)
{
var albums = artistInfo.Albums.DistinctBy(s => s.ForeignAlbumId).ToList();
foreach (var album in albums)
{
var existingAlbum = artist.Albums.FirstOrDefault(s => s.ForeignAlbumId == album.ForeignAlbumId);
//Todo: Should this should use the previous season's monitored state?
if (existingAlbum == null)
{
//if (album.SeasonNumber == 0)
//{
// album.Monitored = false;
// continue;
//}
_logger.Debug("New album ({0}) for artist: [{1}] {2}, setting monitored to true", album.Title, artist.ForeignArtistId, artist.Name);
album.Monitored = true;
}
else
{
album.Monitored = existingAlbum.Monitored;
}
}
return albums;
}
public void Execute(RefreshArtistCommand message) public void Execute(RefreshArtistCommand message)
{ {
_eventAggregator.PublishEvent(new ArtistRefreshStartingEvent(message.Trigger == CommandTrigger.Manual)); _eventAggregator.PublishEvent(new ArtistRefreshStartingEvent(message.Trigger == CommandTrigger.Manual));

@ -68,6 +68,7 @@ namespace NzbDrone.Core.Music
trackToUpdate.AbsoluteTrackNumber = track.AbsoluteTrackNumber; trackToUpdate.AbsoluteTrackNumber = track.AbsoluteTrackNumber;
trackToUpdate.Title = track.Title ?? "Unknown"; trackToUpdate.Title = track.Title ?? "Unknown";
trackToUpdate.AlbumId = album.Id; trackToUpdate.AlbumId = album.Id;
trackToUpdate.ArtistId = album.ArtistId;
trackToUpdate.Album = track.Album ?? album; trackToUpdate.Album = track.Album ?? album;
trackToUpdate.Explicit = track.Explicit; trackToUpdate.Explicit = track.Explicit;
trackToUpdate.ArtistId = album.ArtistId; trackToUpdate.ArtistId = album.ArtistId;
@ -89,15 +90,11 @@ namespace NzbDrone.Core.Music
allTracks.AddRange(newList); allTracks.AddRange(newList);
allTracks.AddRange(updateList); allTracks.AddRange(updateList);
// TODO: See if anything needs to be done here
//AdjustMultiEpisodeAirTime(artist, allTracks);
//AdjustDirectToDvdAirDate(artist, allTracks);
_trackService.DeleteMany(existingTracks); _trackService.DeleteMany(existingTracks);
_trackService.UpdateMany(updateList); _trackService.UpdateMany(updateList);
_trackService.InsertMany(newList); _trackService.InsertMany(newList);
_eventAggregator.PublishEvent(new TrackInfoRefreshedEvent(album, newList, updateList)); _eventAggregator.PublishEvent(new TrackInfoRefreshedEvent(album, newList, updateList, existingTracks));
if (failCount != 0) if (failCount != 0)
{ {

@ -35,6 +35,7 @@ namespace NzbDrone.Core.Music
public class TrackService : ITrackService, public class TrackService : ITrackService,
IHandleAsync<ArtistDeletedEvent>, IHandleAsync<ArtistDeletedEvent>,
IHandleAsync<AlbumDeletedEvent>,
IHandle<TrackFileDeletedEvent>, IHandle<TrackFileDeletedEvent>,
IHandle<TrackFileAddedEvent> IHandle<TrackFileAddedEvent>
{ {
@ -163,6 +164,12 @@ namespace NzbDrone.Core.Music
_trackRepository.DeleteMany(tracks); _trackRepository.DeleteMany(tracks);
} }
public void HandleAsync(AlbumDeletedEvent message)
{
var tracks = GetTracksByAlbum(message.Album.Id);
_trackRepository.DeleteMany(tracks);
}
public void Handle(TrackFileDeletedEvent message) public void Handle(TrackFileDeletedEvent message)
{ {
foreach (var track in GetTracksByFileId(message.TrackFile.Id)) foreach (var track in GetTracksByFileId(message.TrackFile.Id))

@ -181,6 +181,7 @@
<Compile Include="Datastore\Migration\003_add_medium_support.cs" /> <Compile Include="Datastore\Migration\003_add_medium_support.cs" />
<Compile Include="Datastore\Migration\006_separate_automatic_and_interactive_search.cs" /> <Compile Include="Datastore\Migration\006_separate_automatic_and_interactive_search.cs" />
<Compile Include="Datastore\Migration\007_change_album_path_to_relative.cs" /> <Compile Include="Datastore\Migration\007_change_album_path_to_relative.cs" />
<Compile Include="Datastore\Migration\009_album_releases.cs" />
<Compile Include="Datastore\Migration\Framework\MigrationContext.cs" /> <Compile Include="Datastore\Migration\Framework\MigrationContext.cs" />
<Compile Include="Datastore\Migration\Framework\MigrationController.cs" /> <Compile Include="Datastore\Migration\Framework\MigrationController.cs" />
<Compile Include="Datastore\Migration\Framework\MigrationDbFactory.cs" /> <Compile Include="Datastore\Migration\Framework\MigrationDbFactory.cs" />
@ -424,6 +425,7 @@
<Compile Include="Download\ProcessDownloadDecisions.cs" /> <Compile Include="Download\ProcessDownloadDecisions.cs" />
<Compile Include="Download\ProcessedDecisions.cs" /> <Compile Include="Download\ProcessedDecisions.cs" />
<Compile Include="Download\RedownloadFailedDownloadService.cs" /> <Compile Include="Download\RedownloadFailedDownloadService.cs" />
<Compile Include="Exceptions\AlbumNotFoundException.cs" />
<Compile Include="Exceptions\ArtistNotFoundException.cs" /> <Compile Include="Exceptions\ArtistNotFoundException.cs" />
<Compile Include="Exceptions\BadRequestException.cs" /> <Compile Include="Exceptions\BadRequestException.cs" />
<Compile Include="Exceptions\DownstreamException.cs" /> <Compile Include="Exceptions\DownstreamException.cs" />
@ -730,6 +732,7 @@
<Compile Include="Messaging\IProcessMessage.cs" /> <Compile Include="Messaging\IProcessMessage.cs" />
<Compile Include="MetadataSource\IProvideArtistInfo.cs" /> <Compile Include="MetadataSource\IProvideArtistInfo.cs" />
<Compile Include="MetadataSource\ISearchForNewArtist.cs" /> <Compile Include="MetadataSource\ISearchForNewArtist.cs" />
<Compile Include="MetadataSource\IProvideAlbumInfo.cs" />
<Compile Include="MetadataSource\SkyHook\Resource\MediumResource.cs" /> <Compile Include="MetadataSource\SkyHook\Resource\MediumResource.cs" />
<Compile Include="MetadataSource\SkyHook\Resource\MemberResource.cs" /> <Compile Include="MetadataSource\SkyHook\Resource\MemberResource.cs" />
<Compile Include="MetadataSource\SkyHook\Resource\AlbumResource.cs" /> <Compile Include="MetadataSource\SkyHook\Resource\AlbumResource.cs" />
@ -737,6 +740,7 @@
<Compile Include="MetadataSource\SkyHook\Resource\LinkResource.cs" /> <Compile Include="MetadataSource\SkyHook\Resource\LinkResource.cs" />
<Compile Include="MetadataSource\SkyHook\Resource\ImageResource.cs" /> <Compile Include="MetadataSource\SkyHook\Resource\ImageResource.cs" />
<Compile Include="MetadataSource\SkyHook\Resource\RatingResource.cs" /> <Compile Include="MetadataSource\SkyHook\Resource\RatingResource.cs" />
<Compile Include="MetadataSource\SkyHook\Resource\ReleaseResource.cs" />
<Compile Include="MetadataSource\SkyHook\Resource\TimeOfDayResource.cs" /> <Compile Include="MetadataSource\SkyHook\Resource\TimeOfDayResource.cs" />
<Compile Include="MetadataSource\SkyHook\Resource\TrackResource.cs" /> <Compile Include="MetadataSource\SkyHook\Resource\TrackResource.cs" />
<Compile Include="MetadataSource\SkyHook\SkyHookProxy.cs" /> <Compile Include="MetadataSource\SkyHook\SkyHookProxy.cs" />
@ -761,9 +765,13 @@
<Compile Include="Extras\Metadata\MetadataRepository.cs" /> <Compile Include="Extras\Metadata\MetadataRepository.cs" />
<Compile Include="Extras\Metadata\MetadataService.cs" /> <Compile Include="Extras\Metadata\MetadataService.cs" />
<Compile Include="Extras\Metadata\MetadataType.cs" /> <Compile Include="Extras\Metadata\MetadataType.cs" />
<Compile Include="Music\AlbumEditedService.cs" />
<Compile Include="Music\AlbumRelease.cs" />
<Compile Include="Music\ArtistStatusType.cs" /> <Compile Include="Music\ArtistStatusType.cs" />
<Compile Include="Music\AlbumCutoffService.cs" /> <Compile Include="Music\AlbumCutoffService.cs" />
<Compile Include="Music\Commands\BulkMoveArtistCommand.cs" /> <Compile Include="Music\Commands\BulkMoveArtistCommand.cs" />
<Compile Include="Music\Commands\RefreshAlbumCommand.cs" />
<Compile Include="Music\Events\AlbumsAddedEvent.cs" />
<Compile Include="Music\Events\ArtistsImportedEvent.cs" /> <Compile Include="Music\Events\ArtistsImportedEvent.cs" />
<Compile Include="Music\SecondaryAlbumType.cs" /> <Compile Include="Music\SecondaryAlbumType.cs" />
<Compile Include="Music\PrimaryAlbumType.cs" /> <Compile Include="Music\PrimaryAlbumType.cs" />

@ -237,15 +237,6 @@ namespace NzbDrone.Integration.Test
changed = true; changed = true;
} }
result.Albums.ForEach(season =>
{
if (season.Monitored != monitored.Value)
{
season.Monitored = monitored.Value;
changed = true;
}
});
if (changed) if (changed)
{ {
Artist.Put(result); Artist.Put(result);

Loading…
Cancel
Save