Files
Readarr/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js
ta264 bb02d73c42 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
2019-02-16 09:49:24 -05:00

385 lines
10 KiB
JavaScript

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, sortDirections } from 'Helpers/Props';
import Icon from 'Components/Icon';
import TableRow from 'Components/Table/TableRow';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRowCellButton from 'Components/Table/Cells/TableRowCellButton';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import Popover from 'Components/Tooltip/Popover';
import TrackQuality from 'Album/TrackQuality';
import EpisodeLanguage from 'Album/EpisodeLanguage';
import SelectArtistModal from 'InteractiveImport/Artist/SelectArtistModal';
import SelectAlbumModal from 'InteractiveImport/Album/SelectAlbumModal';
import SelectTrackModal from 'InteractiveImport/Track/SelectTrackModal';
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal';
import InteractiveImportRowCellPlaceholder from './InteractiveImportRowCellPlaceholder';
import styles from './InteractiveImportRow.css';
class InteractiveImportRow extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isSelectArtistModalOpen: false,
isSelectAlbumModalOpen: false,
isSelectTrackModalOpen: false,
isSelectQualityModalOpen: false,
isSelectLanguageModalOpen: false
};
}
componentDidMount() {
const {
id,
artist,
album,
tracks,
quality,
language
} = this.props;
if (
artist &&
album != null &&
tracks.length &&
quality &&
language
) {
this.props.onSelectedChange({ id, value: true });
}
}
componentDidUpdate(prevProps) {
const {
id,
artist,
album,
tracks,
quality,
language,
isSelected,
onValidRowChange
} = this.props;
if (
prevProps.artist === artist &&
prevProps.album === album &&
!hasDifferentItems(prevProps.tracks, tracks) &&
prevProps.quality === quality &&
prevProps.language === language &&
prevProps.isSelected === isSelected
) {
return;
}
const isValid = !!(
artist &&
album &&
tracks.length &&
quality &&
language
);
if (isSelected && !isValid) {
onValidRowChange(id, false);
} else {
onValidRowChange(id, true);
}
}
//
// Control
selectRowAfterChange = (value) => {
const {
id,
isSelected
} = this.props;
if (!isSelected && value === true) {
this.props.onSelectedChange({ id, value });
}
}
//
// Listeners
onSelectArtistPress = () => {
this.setState({ isSelectArtistModalOpen: true });
}
onSelectAlbumPress = () => {
this.setState({ isSelectAlbumModalOpen: true });
}
onSelectTrackPress = () => {
this.setState({ isSelectTrackModalOpen: true });
}
onSelectQualityPress = () => {
this.setState({ isSelectQualityModalOpen: true });
}
onSelectLanguagePress = () => {
this.setState({ isSelectLanguageModalOpen: true });
}
onSelectArtistModalClose = (changed) => {
this.setState({ isSelectArtistModalOpen: false });
this.selectRowAfterChange(changed);
}
onSelectAlbumModalClose = (changed) => {
this.setState({ isSelectAlbumModalOpen: false });
this.selectRowAfterChange(changed);
}
onSelectTrackModalClose = (changed) => {
this.setState({ isSelectTrackModalOpen: false });
this.selectRowAfterChange(changed);
}
onSelectQualityModalClose = (changed) => {
this.setState({ isSelectQualityModalOpen: false });
this.selectRowAfterChange(changed);
}
onSelectLanguageModalClose = (changed) => {
this.setState({ isSelectLanguageModalOpen: false });
this.selectRowAfterChange(changed);
}
//
// Render
render() {
const {
id,
allowArtistChange,
relativePath,
artist,
album,
albumReleaseId,
tracks,
quality,
language,
size,
rejections,
audioTags,
isSelected,
onSelectedChange
} = this.props;
const {
isSelectArtistModalOpen,
isSelectAlbumModalOpen,
isSelectTrackModalOpen,
isSelectQualityModalOpen,
isSelectLanguageModalOpen
} = this.state;
const artistName = artist ? artist.artistName : '';
let albumTitle = '';
if (album) {
albumTitle = album.disambiguation ? `${album.title} (${album.disambiguation})` : album.title;
}
const trackNumbers = tracks.map((track) => `${track.mediumNumber}x${track.trackNumber}`)
.join(', ');
const showArtistPlaceholder = isSelected && !artist;
const showAlbumNumberPlaceholder = isSelected && !!artist && !album;
const showTrackNumbersPlaceholder = isSelected && !!album && !tracks.length;
const showQualityPlaceholder = isSelected && !quality;
const showLanguagePlaceholder = isSelected && !language;
return (
<TableRow>
<TableSelectCell
id={id}
isSelected={isSelected}
onSelectedChange={onSelectedChange}
/>
<TableRowCell
className={styles.relativePath}
title={relativePath}
>
{relativePath}
</TableRowCell>
<TableRowCellButton
isDisabled={!allowArtistChange}
onPress={this.onSelectArtistPress}
>
{
showArtistPlaceholder ? <InteractiveImportRowCellPlaceholder /> : artistName
}
</TableRowCellButton>
<TableRowCellButton
isDisabled={!artist}
onPress={this.onSelectAlbumPress}
>
{
showAlbumNumberPlaceholder ? <InteractiveImportRowCellPlaceholder /> : albumTitle
}
</TableRowCellButton>
<TableRowCellButton
isDisabled={!artist || !album}
onPress={this.onSelectTrackPress}
>
{
showTrackNumbersPlaceholder ? <InteractiveImportRowCellPlaceholder /> : trackNumbers
}
</TableRowCellButton>
<TableRowCellButton
className={styles.quality}
onPress={this.onSelectQualityPress}
>
{
showQualityPlaceholder &&
<InteractiveImportRowCellPlaceholder />
}
{
!showQualityPlaceholder && !!quality &&
<TrackQuality
className={styles.label}
quality={quality}
/>
}
</TableRowCellButton>
<TableRowCellButton
className={styles.language}
onPress={this.onSelectLanguagePress}
>
{
showLanguagePlaceholder &&
<InteractiveImportRowCellPlaceholder />
}
{
!showLanguagePlaceholder && !!language &&
<EpisodeLanguage
className={styles.label}
language={language}
/>
}
</TableRowCellButton>
<TableRowCell>
{formatBytes(size)}
</TableRowCell>
<TableRowCell>
{
!!rejections.length &&
<Popover
anchor={
<Icon
name={icons.DANGER}
kind={kinds.DANGER}
/>
}
title="Release Rejected"
body={
<ul>
{
rejections.map((rejection, index) => {
return (
<li key={index}>
{rejection.reason}
</li>
);
})
}
</ul>
}
position={tooltipPositions.LEFT}
/>
}
</TableRowCell>
<SelectArtistModal
isOpen={isSelectArtistModalOpen}
ids={[id]}
onModalClose={this.onSelectArtistModalClose}
/>
<SelectAlbumModal
isOpen={isSelectAlbumModalOpen}
ids={[id]}
artistId={artist && artist.id}
onModalClose={this.onSelectAlbumModalClose}
/>
<SelectTrackModal
isOpen={isSelectTrackModalOpen}
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}
/>
<SelectQualityModal
isOpen={isSelectQualityModalOpen}
id={id}
qualityId={quality ? quality.quality.id : 0}
proper={quality ? quality.revision.version > 1 : false}
real={quality ? quality.revision.real > 0 : false}
onModalClose={this.onSelectQualityModalClose}
/>
<SelectLanguageModal
isOpen={isSelectLanguageModalOpen}
id={id}
languageId={language ? language.id : 0}
onModalClose={this.onSelectLanguageModalClose}
/>
</TableRow>
);
}
}
InteractiveImportRow.propTypes = {
id: PropTypes.number.isRequired,
allowArtistChange: PropTypes.bool.isRequired,
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
};
InteractiveImportRow.defaultProps = {
tracks: []
};
export default InteractiveImportRow;