mirror of
https://github.com/lidarr/lidarr.git
synced 2025-07-30 03:38:26 -07:00
Whole album matching and fingerprinting (#592)
* Cache result of GetAllArtists
* Fixed: Manual import not respecting album import notifications
* Fixed: partial album imports stay in queue, prompting manual import
* Fixed: Allow release if tracks are missing
* Fixed: Be tolerant of missing/extra "The" at start of artist name
* Improve manual import UI
* Omit video tracks from DB entirely
* Revert "faster test packaging in build.sh"
This reverts commit 2723e2a7b8
.
-u and -T are not supported on macOS
* Fix tests on linux and macOS
* Actually lint on linux
On linux yarn runs scripts with sh not bash so ** doesn't recursively glob
* Match whole albums
* Option to disable fingerprinting
* Rip out MediaInfo
* Don't split up things that have the same album selected in manual import
* Try to speed up IndentificationService
* More speedups
* Some fixes and increase power of recording id
* Fix NRE when no tags
* Fix NRE when some (but not all) files in a directory have missing tags
* Bump taglib, tidy up tag parsing
* Add a health check
* Remove media info setting
* Tags -> audioTags
* Add some tests where tags are null
* Rename history events
* Add missing method to interface
* Reinstate MediaInfo tags and update info with artist scan
Also adds migration to remove old format media info
* This file no longer exists
* Don't penalise year if missing from tags
* Formatting improvements
* Use correct system newline
* Switch to the netstandard2.0 library to support net 461
* TagLib.File is IDisposable so should be in a using
* Improve filename matching and add tests
* Neater logging of parsed tags
* Fix disk scan tests for new media info update
* Fix quality detection source
* Fix Inexact Artist/Album match
* Add button to clear track mapping
* Fix warning
* Pacify eslint
* Use \ not /
* Fix UI updates
* Fix media covers
Prevent localizing URL propaging back to the metadata object
* Reduce database overhead broadcasting UI updates
* Relax timings a bit to make test pass
* Remove irrelevant tests
* Test framework for identification service
* Fix PreferMissingToBadMatch test case
* Make fingerprinting more robust
* More logging
* Penalize unknown media format and country
* Prefer USA to UK
* Allow Data CD
* Fix exception if fingerprinting fails for all files
* Fix tests
* Fix NRE
* Allow apostrophes and remove accents in filename aggregation
* Address codacy issues
* Cope with old versions of fpcalc and suggest upgrade
* fpcalc health check passes if fingerprinting disabled
* Get the Artist meta with the artist
* Fix the mapper so that lazy loaded lists will be populated on Join
And therefore we can join TrackFiles on Tracks by default and avoid an
extra query
* Rename subtitle -> lyric
* Tidy up MediaInfoFormatter
This commit is contained in:
parent
8bf364945f
commit
bb02d73c42
174 changed files with 11577 additions and 3490 deletions
|
@ -59,16 +59,19 @@ class SelectAlbumModalContentConnector extends Component {
|
|||
onAlbumSelect = (albumId) => {
|
||||
const album = _.find(this.props.items, { id: albumId });
|
||||
|
||||
this.props.ids.forEach((id) => {
|
||||
const ids = this.props.ids;
|
||||
|
||||
ids.forEach((id) => {
|
||||
this.props.updateInteractiveImportItem({
|
||||
id,
|
||||
album,
|
||||
tracks: [],
|
||||
rejections: []
|
||||
});
|
||||
this.props.saveInteractiveImportItem({ id });
|
||||
});
|
||||
|
||||
this.props.saveInteractiveImportItem({ id: ids });
|
||||
|
||||
this.props.onModalClose(true);
|
||||
}
|
||||
|
||||
|
|
65
frontend/src/InteractiveImport/FileDetails.css
Normal file
65
frontend/src/InteractiveImport/FileDetails.css
Normal file
|
@ -0,0 +1,65 @@
|
|||
.fileDetails {
|
||||
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: 18px;
|
||||
}
|
||||
|
||||
.filename {
|
||||
flex-grow: 1;
|
||||
margin-right: 10px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.expandButton {
|
||||
position: relative;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
composes: button from 'Components/Link/IconButton.css';
|
||||
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.audioTags {
|
||||
padding-top: 15px;
|
||||
padding-bottom: 15px;
|
||||
border-top: 1px solid $borderColor;
|
||||
}
|
||||
|
||||
.expandButtonIcon {
|
||||
composes: actionButton;
|
||||
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-top: -12px;
|
||||
margin-left: -15px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.medium {
|
||||
border-right: 0;
|
||||
border-left: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.expandButtonIcon {
|
||||
position: static;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
258
frontend/src/InteractiveImport/FileDetails.js
Normal file
258
frontend/src/InteractiveImport/FileDetails.js
Normal file
|
@ -0,0 +1,258 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
import DescriptionListItemTitle from 'Components/DescriptionList/DescriptionListItemTitle';
|
||||
import DescriptionListItemDescription from 'Components/DescriptionList/DescriptionListItemDescription';
|
||||
import styles from './FileDetails.css';
|
||||
|
||||
class FileDetails extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isExpanded: props.isExpanded
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onExpandPress = () => {
|
||||
const {
|
||||
isExpanded
|
||||
} = this.state;
|
||||
this.setState({ isExpanded: !isExpanded });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
renderRejections() {
|
||||
const {
|
||||
rejections
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<span>
|
||||
<DescriptionListItemTitle>
|
||||
Rejections
|
||||
</DescriptionListItemTitle>
|
||||
{
|
||||
_.map(rejections, (item, key) => {
|
||||
return (
|
||||
<DescriptionListItemDescription key={key}>
|
||||
{item.reason}
|
||||
</DescriptionListItemDescription>
|
||||
);
|
||||
})
|
||||
}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
filename,
|
||||
audioTags,
|
||||
rejections
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
isExpanded
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.fileDetails}
|
||||
>
|
||||
<div className={styles.header} onClick={this.onExpandPress}>
|
||||
<div className={styles.filename}>
|
||||
{filename}
|
||||
</div>
|
||||
|
||||
<div className={styles.expandButton}>
|
||||
<Icon
|
||||
className={styles.expandButtonIcon}
|
||||
name={isExpanded ? icons.COLLAPSE : icons.EXPAND}
|
||||
title={isExpanded ? 'Hide file info' : 'Show file info'}
|
||||
size={24}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{
|
||||
isExpanded &&
|
||||
<div className={styles.audioTags}>
|
||||
|
||||
<DescriptionList>
|
||||
{
|
||||
audioTags.title !== undefined &&
|
||||
<DescriptionListItem
|
||||
title="Track Title"
|
||||
data={audioTags.title}
|
||||
/>
|
||||
}
|
||||
{
|
||||
audioTags.trackNumbers[0] > 0 &&
|
||||
<DescriptionListItem
|
||||
title="Track Number"
|
||||
data={audioTags.trackNumbers[0]}
|
||||
/>
|
||||
}
|
||||
{
|
||||
audioTags.discNumber > 0 &&
|
||||
<DescriptionListItem
|
||||
title="Disc Number"
|
||||
data={audioTags.discNumber}
|
||||
/>
|
||||
}
|
||||
{
|
||||
audioTags.discCount > 0 &&
|
||||
<DescriptionListItem
|
||||
title="Disc Count"
|
||||
data={audioTags.discCount}
|
||||
/>
|
||||
}
|
||||
{
|
||||
audioTags.albumTitle !== undefined &&
|
||||
<DescriptionListItem
|
||||
title="Album"
|
||||
data={audioTags.albumTitle}
|
||||
/>
|
||||
}
|
||||
{
|
||||
audioTags.artistTitle !== undefined &&
|
||||
<DescriptionListItem
|
||||
title="Artist"
|
||||
data={audioTags.artistTitle}
|
||||
/>
|
||||
}
|
||||
{
|
||||
audioTags.country !== undefined &&
|
||||
<DescriptionListItem
|
||||
title="Country"
|
||||
data={audioTags.country.name}
|
||||
/>
|
||||
}
|
||||
{
|
||||
audioTags.year > 0 &&
|
||||
<DescriptionListItem
|
||||
title="Year"
|
||||
data={audioTags.year}
|
||||
/>
|
||||
}
|
||||
{
|
||||
audioTags.label !== undefined &&
|
||||
<DescriptionListItem
|
||||
title="Label"
|
||||
data={audioTags.label}
|
||||
/>
|
||||
}
|
||||
{
|
||||
audioTags.catalogNumber !== undefined &&
|
||||
<DescriptionListItem
|
||||
title="Catalog Number"
|
||||
data={audioTags.catalogNumber}
|
||||
/>
|
||||
}
|
||||
{
|
||||
audioTags.disambiguation !== undefined &&
|
||||
<DescriptionListItem
|
||||
title="Disambiguation"
|
||||
data={audioTags.disambiguation}
|
||||
/>
|
||||
}
|
||||
{
|
||||
audioTags.duration !== undefined &&
|
||||
<DescriptionListItem
|
||||
title="Duration"
|
||||
data={formatTimeSpan(audioTags.duration)}
|
||||
/>
|
||||
}
|
||||
{
|
||||
audioTags.artistMBId !== undefined &&
|
||||
<Link
|
||||
to={`https://musicbrainz.org/artist/${audioTags.artistMBId}`}
|
||||
>
|
||||
<DescriptionListItem
|
||||
title="MusicBrainz Artist ID"
|
||||
data={audioTags.artistMBId}
|
||||
/>
|
||||
</Link>
|
||||
}
|
||||
{
|
||||
audioTags.albumMBId !== undefined &&
|
||||
<Link
|
||||
to={`https://musicbrainz.org/release-group/${audioTags.albumMBId}`}
|
||||
>
|
||||
<DescriptionListItem
|
||||
title="MusicBrainz Album ID"
|
||||
data={audioTags.albumMBId}
|
||||
/>
|
||||
</Link>
|
||||
}
|
||||
{
|
||||
audioTags.releaseMBId !== undefined &&
|
||||
<Link
|
||||
to={`https://musicbrainz.org/release/${audioTags.releaseMBId}`}
|
||||
>
|
||||
<DescriptionListItem
|
||||
title="MusicBrainz Release ID"
|
||||
data={audioTags.releaseMBId}
|
||||
/>
|
||||
</Link>
|
||||
}
|
||||
{
|
||||
audioTags.recordingMBId !== undefined &&
|
||||
<Link
|
||||
to={`https://musicbrainz.org/recording/${audioTags.recordingMBId}`}
|
||||
>
|
||||
<DescriptionListItem
|
||||
title="MusicBrainz Recording ID"
|
||||
data={audioTags.recordingMBId}
|
||||
/>
|
||||
</Link>
|
||||
}
|
||||
{
|
||||
audioTags.trackMBId !== undefined &&
|
||||
<Link
|
||||
to={`https://musicbrainz.org/track/${audioTags.trackMBId}`}
|
||||
>
|
||||
<DescriptionListItem
|
||||
title="MusicBrainz Track ID"
|
||||
data={audioTags.trackMBId}
|
||||
/>
|
||||
</Link>
|
||||
}
|
||||
{
|
||||
rejections.length > 0 &&
|
||||
this.renderRejections()
|
||||
}
|
||||
</DescriptionList>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FileDetails.propTypes = {
|
||||
audioTags: PropTypes.object.isRequired,
|
||||
filename: PropTypes.string.isRequired,
|
||||
rejections: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isExpanded: PropTypes.bool
|
||||
};
|
||||
|
||||
export default FileDetails;
|
|
@ -19,12 +19,13 @@
|
|||
.centerButtons,
|
||||
.rightButtons {
|
||||
display: flex;
|
||||
flex: 1 0 33%;
|
||||
flex: 1 2 25%;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.centerButtons {
|
||||
justify-content: center;
|
||||
flex: 2 1 50%;
|
||||
}
|
||||
|
||||
.rightButtons {
|
||||
|
|
|
@ -155,6 +155,18 @@ class InteractiveImportModalContent extends Component {
|
|||
this.setState({ isSelectAlbumModalOpen: true });
|
||||
}
|
||||
|
||||
onClearTrackMappingPress = () => {
|
||||
const selectedIds = this.getSelectedIds();
|
||||
|
||||
selectedIds.forEach((id) => {
|
||||
this.props.updateInteractiveImportItem({
|
||||
id,
|
||||
tracks: [],
|
||||
rejections: []
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onSelectArtistModalClose = () => {
|
||||
this.setState({ isSelectArtistModalOpen: false });
|
||||
}
|
||||
|
@ -328,6 +340,10 @@ class InteractiveImportModalContent extends Component {
|
|||
>
|
||||
Select Album
|
||||
</Button>
|
||||
|
||||
<Button onPress={this.onClearTrackMappingPress}>
|
||||
Clear Track Mapping
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className={styles.rightButtons}>
|
||||
|
@ -362,6 +378,7 @@ class InteractiveImportModalContent extends Component {
|
|||
artistId={selectedItem && selectedItem.artist && selectedItem.artist.id}
|
||||
onModalClose={this.onSelectAlbumModalClose}
|
||||
/>
|
||||
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
@ -387,6 +404,7 @@ InteractiveImportModalContent.propTypes = {
|
|||
onFilterExistingFilesChange: PropTypes.func.isRequired,
|
||||
onImportModeChange: PropTypes.func.isRequired,
|
||||
onImportSelectedPress: PropTypes.func.isRequired,
|
||||
updateInteractiveImportItem: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
|||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchInteractiveImportItems, setInteractiveImportSort, clearInteractiveImport, setInteractiveImportMode } from 'Store/Actions/interactiveImportActions';
|
||||
import { fetchInteractiveImportItems, setInteractiveImportSort, clearInteractiveImport, setInteractiveImportMode, updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
|
@ -23,6 +23,7 @@ const mapDispatchToProps = {
|
|||
setInteractiveImportSort,
|
||||
setInteractiveImportMode,
|
||||
clearInteractiveImport,
|
||||
updateInteractiveImportItem,
|
||||
executeCommand
|
||||
};
|
||||
|
||||
|
@ -195,6 +196,7 @@ InteractiveImportModalContentConnector.propTypes = {
|
|||
setInteractiveImportSort: PropTypes.func.isRequired,
|
||||
clearInteractiveImport: PropTypes.func.isRequired,
|
||||
setInteractiveImportMode: PropTypes.func.isRequired,
|
||||
updateInteractiveImportItem: PropTypes.func.isRequired,
|
||||
executeCommand: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
|
|
@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
|||
import React, { Component } from 'react';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import { icons, kinds, tooltipPositions, sortDirections } from 'Helpers/Props';
|
||||
import Icon from 'Components/Icon';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
|
@ -167,11 +167,13 @@ class InteractiveImportRow extends Component {
|
|||
relativePath,
|
||||
artist,
|
||||
album,
|
||||
albumReleaseId,
|
||||
tracks,
|
||||
quality,
|
||||
language,
|
||||
size,
|
||||
rejections,
|
||||
audioTags,
|
||||
isSelected,
|
||||
onSelectedChange
|
||||
} = this.props;
|
||||
|
@ -327,6 +329,11 @@ class InteractiveImportRow extends Component {
|
|||
id={id}
|
||||
artistId={artist && artist.id}
|
||||
albumId={album && album.id}
|
||||
albumReleaseId={albumReleaseId}
|
||||
rejections={rejections}
|
||||
audioTags={audioTags}
|
||||
sortKey='mediumNumber'
|
||||
sortDirection={sortDirections.ASCENDING}
|
||||
filename={relativePath}
|
||||
onModalClose={this.onSelectTrackModalClose}
|
||||
/>
|
||||
|
@ -358,11 +365,13 @@ InteractiveImportRow.propTypes = {
|
|||
relativePath: PropTypes.string.isRequired,
|
||||
artist: PropTypes.object,
|
||||
album: PropTypes.object,
|
||||
albumReleaseId: PropTypes.number,
|
||||
tracks: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
quality: PropTypes.object,
|
||||
language: PropTypes.object,
|
||||
size: PropTypes.number.isRequired,
|
||||
rejections: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
audioTags: PropTypes.object.isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
onSelectedChange: PropTypes.func.isRequired,
|
||||
onValidRowChange: PropTypes.func.isRequired
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import _ from 'lodash';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import selectAll from 'Utilities/Table/selectAll';
|
||||
|
@ -14,6 +15,7 @@ import ModalFooter from 'Components/Modal/ModalFooter';
|
|||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import SelectTrackRow from './SelectTrackRow';
|
||||
import FileDetails from '../FileDetails';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
|
@ -32,6 +34,19 @@ const columns = [
|
|||
name: 'title',
|
||||
label: 'Title',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'trackStatus',
|
||||
label: 'Status',
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
|
||||
const selectAllBlankColumn = [
|
||||
{
|
||||
name: 'dummy',
|
||||
label: ' ',
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -43,12 +58,17 @@ class SelectTrackModalContent extends Component {
|
|||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
const selectedTracks = _.filter(props.selectedTracksByItem, ['id', props.id])[0].tracks;
|
||||
const init = _.zipObject(selectedTracks, _.times(selectedTracks.length, _.constant(true)));
|
||||
|
||||
this.state = {
|
||||
allSelected: false,
|
||||
allUnselected: false,
|
||||
lastToggled: null,
|
||||
selectedState: {}
|
||||
selectedState: init
|
||||
};
|
||||
|
||||
props.onSortPress( props.sortKey, props.sortDirection );
|
||||
}
|
||||
|
||||
//
|
||||
|
@ -80,6 +100,9 @@ class SelectTrackModalContent extends Component {
|
|||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
audioTags,
|
||||
rejections,
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
|
@ -88,6 +111,7 @@ class SelectTrackModalContent extends Component {
|
|||
sortDirection,
|
||||
onSortPress,
|
||||
onModalClose,
|
||||
selectedTracksByItem,
|
||||
filename
|
||||
} = this.props;
|
||||
|
||||
|
@ -97,13 +121,23 @@ class SelectTrackModalContent extends Component {
|
|||
selectedState
|
||||
} = this.state;
|
||||
|
||||
const title = `Manual Import - Select Track(s): ${filename}`;
|
||||
const errorMessage = getErrorMessage(error, 'Unable to load tracks');
|
||||
|
||||
// all tracks selected for other items
|
||||
const otherSelected = _.map(_.filter(selectedTracksByItem, (item) => {
|
||||
return item.id !== id;
|
||||
}), (x) => {
|
||||
return x.tracks;
|
||||
}).flat();
|
||||
// tracks selected for the current file
|
||||
const currentSelected = _.keys(_.pickBy(selectedState, _.identity)).map(Number);
|
||||
// only enable selectAll if no other files have any tracks selected.
|
||||
const selectAllEnabled = otherSelected.length === 0;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{title}
|
||||
Manual Import - Select Track(s):
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
|
@ -117,11 +151,18 @@ class SelectTrackModalContent extends Component {
|
|||
<div>{errorMessage}</div>
|
||||
}
|
||||
|
||||
<FileDetails
|
||||
audioTags={audioTags}
|
||||
filename={filename}
|
||||
rejections={rejections}
|
||||
isExpanded={false}
|
||||
/>
|
||||
|
||||
{
|
||||
isPopulated && !!items.length &&
|
||||
<Table
|
||||
columns={columns}
|
||||
selectAll={true}
|
||||
columns={selectAllEnabled ? columns : selectAllBlankColumn.concat(columns)}
|
||||
selectAll={selectAllEnabled}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
sortKey={sortKey}
|
||||
|
@ -139,6 +180,9 @@ class SelectTrackModalContent extends Component {
|
|||
mediumNumber={item.mediumNumber}
|
||||
trackNumber={item.absoluteTrackNumber}
|
||||
title={item.title}
|
||||
hasFile={item.hasFile}
|
||||
importSelected={otherSelected.concat(currentSelected).includes(item.id)}
|
||||
isDisabled={otherSelected.includes(item.id)}
|
||||
isSelected={selectedState[item.id]}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
/>
|
||||
|
@ -173,6 +217,9 @@ class SelectTrackModalContent extends Component {
|
|||
}
|
||||
|
||||
SelectTrackModalContent.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
rejections: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
audioTags: PropTypes.object.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
|
@ -182,6 +229,7 @@ SelectTrackModalContent.propTypes = {
|
|||
onSortPress: PropTypes.func.isRequired,
|
||||
onTracksSelect: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
selectedTracksByItem: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
filename: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
|
|
|
@ -11,8 +11,19 @@ import SelectTrackModalContent from './SelectTrackModalContent';
|
|||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createClientSideCollectionSelector('tracks'),
|
||||
(tracks) => {
|
||||
return tracks;
|
||||
createClientSideCollectionSelector('interactiveImport'),
|
||||
(tracks, interactiveImport) => {
|
||||
|
||||
const selectedTracksByItem = _.map(interactiveImport.items, (item) => {
|
||||
return { id: item.id, tracks: _.map(item.tracks, (track) => {
|
||||
return track.id;
|
||||
}) };
|
||||
});
|
||||
|
||||
return {
|
||||
...tracks,
|
||||
selectedTracksByItem
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -32,10 +43,11 @@ class SelectTrackModalContentConnector extends Component {
|
|||
componentDidMount() {
|
||||
const {
|
||||
artistId,
|
||||
albumId
|
||||
albumId,
|
||||
albumReleaseId
|
||||
} = this.props;
|
||||
|
||||
this.props.fetchTracks({ artistId, albumId });
|
||||
this.props.fetchTracks({ artistId, albumId, albumReleaseId });
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -86,6 +98,9 @@ SelectTrackModalContentConnector.propTypes = {
|
|||
id: PropTypes.number.isRequired,
|
||||
artistId: PropTypes.number.isRequired,
|
||||
albumId: PropTypes.number.isRequired,
|
||||
albumReleaseId: PropTypes.number.isRequired,
|
||||
rejections: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
audioTags: PropTypes.object.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
fetchTracks: PropTypes.func.isRequired,
|
||||
setTracksSort: PropTypes.func.isRequired,
|
||||
|
|
|
@ -3,6 +3,9 @@ import React, { Component } from 'react';
|
|||
import TableRowButton from 'Components/Table/TableRowButton';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import Icon from 'Components/Icon';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
|
||||
class SelectTrackRow extends Component {
|
||||
|
||||
|
@ -27,16 +30,50 @@ class SelectTrackRow extends Component {
|
|||
mediumNumber,
|
||||
trackNumber,
|
||||
title,
|
||||
hasFile,
|
||||
importSelected,
|
||||
isSelected,
|
||||
isDisabled,
|
||||
onSelectedChange
|
||||
} = this.props;
|
||||
|
||||
let iconName = icons.UNKNOWN;
|
||||
let iconKind = kinds.DEFAULT;
|
||||
let iconTip = '';
|
||||
|
||||
if (hasFile && !importSelected) {
|
||||
iconName = icons.DOWNLOADED;
|
||||
iconKind = kinds.DEFAULT;
|
||||
iconTip = 'Track already in library.';
|
||||
} else if (!hasFile && !importSelected) {
|
||||
iconName = icons.UNKNOWN;
|
||||
iconKind = kinds.DEFAULT;
|
||||
iconTip = 'Track missing from library and no import selected.';
|
||||
} else if (importSelected && hasFile) {
|
||||
iconName = icons.FILEIMPORT;
|
||||
iconKind = kinds.WARNING;
|
||||
iconTip = 'Warning: Existing track will be replaced by download.';
|
||||
} else if (importSelected && !hasFile) {
|
||||
iconName = icons.FILEIMPORT;
|
||||
iconKind = kinds.DEFAULT;
|
||||
iconTip = 'Track missing from library and selected for import.';
|
||||
}
|
||||
|
||||
// isDisabled can only be true if importSelected is true
|
||||
if (isDisabled) {
|
||||
iconTip = `${iconTip}\nAnother file is selected to import for this track.`;
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRowButton onPress={this.onPress}>
|
||||
<TableRowButton
|
||||
onPress={this.onPress}
|
||||
isDisabled={isDisabled}
|
||||
>
|
||||
<TableSelectCell
|
||||
id={id}
|
||||
isSelected={isSelected}
|
||||
onSelectedChange={onSelectedChange}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
|
||||
<TableRowCell>
|
||||
|
@ -51,6 +88,19 @@ class SelectTrackRow extends Component {
|
|||
{title}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon
|
||||
name={iconName}
|
||||
kind={iconKind}
|
||||
/>
|
||||
}
|
||||
title={'Track status'}
|
||||
body={iconTip}
|
||||
position={tooltipPositions.LEFT}
|
||||
/>
|
||||
</TableRowCell>
|
||||
</TableRowButton>
|
||||
);
|
||||
}
|
||||
|
@ -61,7 +111,10 @@ SelectTrackRow.propTypes = {
|
|||
mediumNumber: PropTypes.number.isRequired,
|
||||
trackNumber: PropTypes.number.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
hasFile: PropTypes.bool.isRequired,
|
||||
importSelected: PropTypes.bool.isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
isDisabled: PropTypes.bool,
|
||||
onSelectedChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue