New: Update DB to store all releases for an album (#517)

* New: Store all releases for an album and track artists

* Add Overview, links and release date by release

* Tidy up

* Fix metadata refresh errors following musicbrainz edits
This commit is contained in:
ta264 2018-12-15 00:02:43 +00:00 committed by Qstick
commit c392569a63
136 changed files with 2305 additions and 1120 deletions

View file

@ -106,6 +106,7 @@
.sizeOnDisk,
.qualityProfileName,
.links,
.tags {
margin-left: 8px;
font-weight: 300;

View file

@ -2,14 +2,17 @@ import _ from 'lodash';
import moment from 'moment';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import TextTruncate from 'react-text-truncate';
import formatBytes from 'Utilities/Number/formatBytes';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import { align, icons, sizes } from 'Helpers/Props';
import { align, icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
import fonts from 'Styles/Variables/fonts';
import HeartRating from 'Components/HeartRating';
import Icon from 'Components/Icon';
import IconButton from 'Components/Link/IconButton';
import Label from 'Components/Label';
import Tooltip from 'Components/Tooltip/Tooltip';
import AlbumCover from 'Album/AlbumCover';
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
import EditAlbumModalConnector from 'Album/Edit/EditAlbumModalConnector';
@ -24,9 +27,12 @@ import AlbumDetailsMediumConnector from './AlbumDetailsMediumConnector';
import ArtistHistoryModal from 'Artist/History/ArtistHistoryModal';
import InteractiveSearchModal from 'InteractiveSearch/InteractiveSearchModal';
import TrackFileEditorModal from 'TrackFile/Editor/TrackFileEditorModal';
import AlbumDetailsLinks from './AlbumDetailsLinks';
import styles from './AlbumDetails.css';
const defaultFontSize = parseInt(fonts.defaultFontSize);
const lineHeight = parseFloat(fonts.lineHeight);
function getFanartUrl(images) {
const fanartImage = _.find(images, { coverType: 'fanart' });
if (fanartImage) {
@ -135,14 +141,17 @@ class AlbumDetails extends Component {
render() {
const {
id,
foreignAlbumId,
title,
disambiguation,
overview,
albumType,
statistics = {},
monitored,
releaseDate,
ratings,
images,
links,
media,
isFetching,
isPopulated,
@ -357,6 +366,38 @@ class AlbumDetails extends Component {
</Label>
}
<Tooltip
anchor={
<Label
className={styles.detailsLabel}
size={sizes.LARGE}
>
<Icon
name={icons.EXTERNAL_LINK}
size={17}
/>
<span className={styles.links}>
Links
</span>
</Label>
}
tooltip={
<AlbumDetailsLinks
foreignAlbumId={foreignAlbumId}
links={links}
/>
}
kind={kinds.INVERSE}
position={tooltipPositions.BOTTOM}
/>
</div>
<div className={styles.overview}>
<TextTruncate
line={Math.floor(125 / (defaultFontSize * lineHeight))}
text={overview}
/>
</div>
</div>
</div>
@ -446,11 +487,13 @@ AlbumDetails.propTypes = {
foreignAlbumId: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
disambiguation: PropTypes.string,
overview: PropTypes.string,
albumType: PropTypes.string.isRequired,
statistics: PropTypes.object.isRequired,
releaseDate: PropTypes.string.isRequired,
ratings: PropTypes.object.isRequired,
images: PropTypes.arrayOf(PropTypes.object).isRequired,
links: PropTypes.arrayOf(PropTypes.object).isRequired,
media: PropTypes.arrayOf(PropTypes.object).isRequired,
monitored: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,

View file

@ -67,6 +67,10 @@ const mapDispatchToProps = {
clearTrackFiles
};
function getMonitoredReleases(props) {
return _.map(_.filter(props.releases, { monitored: true }), 'id').sort();
}
class AlbumDetailsConnector extends Component {
componentDidMount() {
@ -75,14 +79,8 @@ class AlbumDetailsConnector extends Component {
}
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) {
if (!_.isEqual(getMonitoredReleases(prevProps), getMonitoredReleases(this.props)) ||
(prevProps.anyReleaseOk === false && this.props.anyReleaseOk === true)) {
this.unpopulate();
this.populate();
}

View file

@ -0,0 +1,13 @@
.links {
margin: 0;
}
.link {
white-space: nowrap;
}
.linkLabel {
composes: label from 'Components/Label.css';
cursor: pointer;
}

View file

@ -0,0 +1,63 @@
import PropTypes from 'prop-types';
import React from 'react';
import { kinds, sizes } from 'Helpers/Props';
import Label from 'Components/Label';
import Link from 'Components/Link/Link';
import styles from './AlbumDetailsLinks.css';
function AlbumDetailsLinks(props) {
const {
foreignAlbumId,
links
} = props;
return (
<div className={styles.links}>
<Link
className={styles.link}
to={`https://musicbrainz.org/release-group/${foreignAlbumId}`}
>
<Label
className={styles.linkLabel}
kind={kinds.INFO}
size={sizes.LARGE}
>
Musicbrainz
</Label>
</Link>
{links.map((link, index) => {
return (
<span key={index}>
<Link className={styles.link}
to={link.url}
key={index}
>
<Label
className={styles.linkLabel}
kind={kinds.INFO}
size={sizes.LARGE}
>
{link.name}
</Label>
</Link>
{(index > 0 && index % 5 === 0) &&
<br />
}
</span>
);
})}
</div>
);
}
AlbumDetailsLinks.propTypes = {
foreignAlbumId: PropTypes.string.isRequired,
links: PropTypes.arrayOf(PropTypes.object).isRequired
};
export default AlbumDetailsLinks;

View file

@ -19,7 +19,7 @@ function getMediumStatistics(tracks) {
if (track.trackFileId) {
trackCount++;
trackFileCount++;
} else if (track.monitored) {
} else {
trackCount++;
}

View file

@ -178,7 +178,6 @@ 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,

View file

@ -43,7 +43,7 @@ class EditAlbumModalContent extends Component {
const {
monitored,
currentRelease,
anyReleaseOk,
releases
} = item;
@ -69,15 +69,26 @@ class EditAlbumModalContent extends Component {
/>
</FormGroup>
<FormGroup>
<FormLabel>Automatically Switch Release</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="anyReleaseOk"
helpText="Lidarr will automatically switch to the release best matching downloaded tracks"
{...anyReleaseOk}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel> Release</FormLabel>
<FormInputGroup
type={inputTypes.ALBUM_RELEASE_SELECT}
name="currentRelease"
name="releases"
helpText="Change release for this album"
albumReleases={releases}
selectedRelease={currentRelease}
onChange={onInputChange}
/>
</FormGroup>

View file

@ -23,7 +23,7 @@ function createMapStateToProps() {
const albumSettings = _.pick(album, [
'monitored',
'currentRelease',
'anyReleaseOk',
'releases'
]);

View file

@ -344,6 +344,14 @@ class ArtistDetails extends Component {
to={`/artist/${previousArtist.foreignArtistId}`}
/>
<IconButton
className={styles.artistNavigationButton}
name={icons.ARROW_UP}
size={30}
title={'Go to artist listing'}
to={'/'}
/>
<IconButton
className={styles.artistNavigationButton}
name={icons.ARROW_RIGHT}

View file

@ -9,15 +9,14 @@ import SelectInput from './SelectInput';
function createMapStateToProps() {
return createSelector(
(state, { albumReleases }) => albumReleases,
(state, { selectedRelease }) => selectedRelease,
(albumReleases, selectedRelease) => {
(albumReleases) => {
const values = _.map(albumReleases.value, (albumRelease) => {
return {
key: albumRelease.id,
key: albumRelease.foreignReleaseId,
value: `${albumRelease.title}` +
`${albumRelease.disambiguation ? ' (' : ''}${titleCase(albumRelease.disambiguation)}${albumRelease.disambiguation ? ')' : ''}` +
`, ${albumRelease.mediaCount} med, ${albumRelease.trackCount} tracks` +
`, ${albumRelease.mediumCount} med, ${albumRelease.trackCount} tracks` +
`${albumRelease.country.length > 0 ? ', ' : ''}${albumRelease.country}` +
`${albumRelease.format ? ', [' : ''}${albumRelease.format}${albumRelease.format ? ']' : ''}`
};
@ -25,7 +24,7 @@ function createMapStateToProps() {
const sortedValues = _.orderBy(values, ['value']);
const value = selectedRelease.value.id;
const value = _.find(albumReleases.value, { monitored: true }).foreignReleaseId;
return {
values: sortedValues,
@ -45,7 +44,10 @@ class AlbumReleaseSelectInputConnector extends Component {
albumReleases
} = this.props;
this.props.onChange({ name, value: _.find(albumReleases.value, { id: value }) });
let updatedReleases = _.map(albumReleases.value, (e) => ({ ...e, monitored: false }));
_.find(updatedReleases, { foreignReleaseId: value }).monitored = true;
this.props.onChange({ name, value: updatedReleases });
}
render() {

View file

@ -105,6 +105,7 @@ class InteractiveImportModalContentConnector extends Component {
const {
artist,
album,
albumReleaseId,
tracks,
quality,
language
@ -140,6 +141,7 @@ class InteractiveImportModalContentConnector extends Component {
folderName: item.folderName,
artistId: artist.id,
albumId: album.id,
albumReleaseId,
trackIds: _.map(tracks, 'id'),
quality,
language,