mirror of
https://github.com/Readarr/Readarr.git
synced 2026-04-25 22:36:59 -04:00
New: Write metadata to tags, with UI for previewing changes (#633)
This commit is contained in:
@@ -23,6 +23,7 @@ import Popover from 'Components/Tooltip/Popover';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import TrackFileEditorModal from 'TrackFile/Editor/TrackFileEditorModal';
|
||||
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
|
||||
import RetagPreviewModalConnector from 'Retag/RetagPreviewModalConnector';
|
||||
import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector';
|
||||
import ArtistPoster from 'Artist/ArtistPoster';
|
||||
import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector';
|
||||
@@ -66,6 +67,7 @@ class ArtistDetails extends Component {
|
||||
|
||||
this.state = {
|
||||
isOrganizeModalOpen: false,
|
||||
isRetagModalOpen: false,
|
||||
isManageTracksOpen: false,
|
||||
isEditArtistModalOpen: false,
|
||||
isDeleteArtistModalOpen: false,
|
||||
@@ -89,6 +91,14 @@ class ArtistDetails extends Component {
|
||||
this.setState({ isOrganizeModalOpen: false });
|
||||
}
|
||||
|
||||
onRetagPress = () => {
|
||||
this.setState({ isRetagModalOpen: true });
|
||||
}
|
||||
|
||||
onRetagModalClose = () => {
|
||||
this.setState({ isRetagModalOpen: false });
|
||||
}
|
||||
|
||||
onManageTracksPress = () => {
|
||||
this.setState({ isManageTracksOpen: true });
|
||||
}
|
||||
@@ -207,6 +217,7 @@ class ArtistDetails extends Component {
|
||||
|
||||
const {
|
||||
isOrganizeModalOpen,
|
||||
isRetagModalOpen,
|
||||
isManageTracksOpen,
|
||||
isEditArtistModalOpen,
|
||||
isDeleteArtistModalOpen,
|
||||
@@ -276,6 +287,12 @@ class ArtistDetails extends Component {
|
||||
onPress={this.onOrganizePress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label="Preview Retag"
|
||||
iconName={icons.RETAG}
|
||||
onPress={this.onRetagPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label="Manage Tracks"
|
||||
iconName={icons.TRACK_FILE}
|
||||
@@ -600,6 +617,12 @@ class ArtistDetails extends Component {
|
||||
onModalClose={this.onOrganizeModalClose}
|
||||
/>
|
||||
|
||||
<RetagPreviewModalConnector
|
||||
isOpen={isRetagModalOpen}
|
||||
artistId={id}
|
||||
onModalClose={this.onRetagModalClose}
|
||||
/>
|
||||
|
||||
<TrackFileEditorModal
|
||||
isOpen={isManageTracksOpen}
|
||||
artistId={id}
|
||||
|
||||
@@ -14,6 +14,7 @@ import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import NoArtist from 'Artist/NoArtist';
|
||||
import OrganizeArtistModal from './Organize/OrganizeArtistModal';
|
||||
import RetagArtistModal from './AudioTags/RetagArtistModal';
|
||||
import ArtistEditorRowConnector from './ArtistEditorRowConnector';
|
||||
import ArtistEditorFooter from './ArtistEditorFooter';
|
||||
import ArtistEditorFilterModalConnector from './ArtistEditorFilterModalConnector';
|
||||
@@ -84,6 +85,7 @@ class ArtistEditor extends Component {
|
||||
lastToggled: null,
|
||||
selectedState: {},
|
||||
isOrganizingArtistModalOpen: false,
|
||||
isRetaggingArtistModalOpen: false,
|
||||
columns: getColumns(props.showLanguageProfile, props.showMetadataProfile)
|
||||
};
|
||||
}
|
||||
@@ -142,6 +144,18 @@ class ArtistEditor extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
onRetagArtistPress = () => {
|
||||
this.setState({ isRetaggingArtistModalOpen: true });
|
||||
}
|
||||
|
||||
onRetagArtistModalClose = (organized) => {
|
||||
this.setState({ isRetaggingArtistModalOpen: false });
|
||||
|
||||
if (organized === true) {
|
||||
this.onSelectAllChange({ value: false });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -162,6 +176,7 @@ class ArtistEditor extends Component {
|
||||
isDeleting,
|
||||
deleteError,
|
||||
isOrganizingArtist,
|
||||
isRetaggingArtist,
|
||||
showLanguageProfile,
|
||||
showMetadataProfile,
|
||||
onSortPress,
|
||||
@@ -250,10 +265,12 @@ class ArtistEditor extends Component {
|
||||
isDeleting={isDeleting}
|
||||
deleteError={deleteError}
|
||||
isOrganizingArtist={isOrganizingArtist}
|
||||
isRetaggingArtist={isRetaggingArtist}
|
||||
showLanguageProfile={showLanguageProfile}
|
||||
showMetadataProfile={showMetadataProfile}
|
||||
onSaveSelected={this.onSaveSelected}
|
||||
onOrganizeArtistPress={this.onOrganizeArtistPress}
|
||||
onRetagArtistPress={this.onRetagArtistPress}
|
||||
/>
|
||||
|
||||
<OrganizeArtistModal
|
||||
@@ -261,6 +278,13 @@ class ArtistEditor extends Component {
|
||||
artistIds={selectedArtistIds}
|
||||
onModalClose={this.onOrganizeArtistModalClose}
|
||||
/>
|
||||
|
||||
<RetagArtistModal
|
||||
isOpen={this.state.isRetaggingArtistModalOpen}
|
||||
artistIds={selectedArtistIds}
|
||||
onModalClose={this.onRetagArtistModalClose}
|
||||
/>
|
||||
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
@@ -282,6 +306,7 @@ ArtistEditor.propTypes = {
|
||||
isDeleting: PropTypes.bool.isRequired,
|
||||
deleteError: PropTypes.object,
|
||||
isOrganizingArtist: PropTypes.bool.isRequired,
|
||||
isRetaggingArtist: PropTypes.bool.isRequired,
|
||||
showLanguageProfile: PropTypes.bool.isRequired,
|
||||
showMetadataProfile: PropTypes.bool.isRequired,
|
||||
onSortPress: PropTypes.func.isRequired,
|
||||
|
||||
@@ -16,9 +16,11 @@ function createMapStateToProps() {
|
||||
(state) => state.settings.metadataProfiles,
|
||||
createClientSideCollectionSelector('artist', 'artistEditor'),
|
||||
createCommandExecutingSelector(commandNames.RENAME_ARTIST),
|
||||
(languageProfiles, metadataProfiles, artist, isOrganizingArtist) => {
|
||||
createCommandExecutingSelector(commandNames.RETAG_ARTIST),
|
||||
(languageProfiles, metadataProfiles, artist, isOrganizingArtist, isRetaggingArtist) => {
|
||||
return {
|
||||
isOrganizingArtist,
|
||||
isRetaggingArtist,
|
||||
showLanguageProfile: languageProfiles.items.length > 1,
|
||||
showMetadataProfile: metadataProfiles.items.length > 1,
|
||||
...artist
|
||||
|
||||
@@ -145,9 +145,11 @@ class ArtistEditorFooter extends Component {
|
||||
isSaving,
|
||||
isDeleting,
|
||||
isOrganizingArtist,
|
||||
isRetaggingArtist,
|
||||
showLanguageProfile,
|
||||
showMetadataProfile,
|
||||
onOrganizeArtistPress
|
||||
onOrganizeArtistPress,
|
||||
onRetagArtistPress
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@@ -288,19 +290,29 @@ class ArtistEditorFooter extends Component {
|
||||
className={styles.organizeSelectedButton}
|
||||
kind={kinds.WARNING}
|
||||
isSpinning={isOrganizingArtist}
|
||||
isDisabled={!selectedCount || isOrganizingArtist}
|
||||
isDisabled={!selectedCount || isOrganizingArtist || isRetaggingArtist}
|
||||
onPress={onOrganizeArtistPress}
|
||||
>
|
||||
Rename Files
|
||||
</SpinnerButton>
|
||||
|
||||
<SpinnerButton
|
||||
className={styles.organizeSelectedButton}
|
||||
kind={kinds.WARNING}
|
||||
isSpinning={isRetaggingArtist}
|
||||
isDisabled={!selectedCount || isOrganizingArtist || isRetaggingArtist}
|
||||
onPress={onRetagArtistPress}
|
||||
>
|
||||
Write Metadata Tags
|
||||
</SpinnerButton>
|
||||
|
||||
<SpinnerButton
|
||||
className={styles.tagsButton}
|
||||
isSpinning={isSaving && savingTags}
|
||||
isDisabled={!selectedCount || isOrganizingArtist}
|
||||
isDisabled={!selectedCount || isOrganizingArtist || isRetaggingArtist}
|
||||
onPress={this.onTagsPress}
|
||||
>
|
||||
Set Tags
|
||||
Set Lidarr Tags
|
||||
</SpinnerButton>
|
||||
</div>
|
||||
|
||||
@@ -350,10 +362,12 @@ ArtistEditorFooter.propTypes = {
|
||||
isDeleting: PropTypes.bool.isRequired,
|
||||
deleteError: PropTypes.object,
|
||||
isOrganizingArtist: PropTypes.bool.isRequired,
|
||||
isRetaggingArtist: PropTypes.bool.isRequired,
|
||||
showLanguageProfile: PropTypes.bool.isRequired,
|
||||
showMetadataProfile: PropTypes.bool.isRequired,
|
||||
onSaveSelected: PropTypes.func.isRequired,
|
||||
onOrganizeArtistPress: PropTypes.func.isRequired
|
||||
onOrganizeArtistPress: PropTypes.func.isRequired,
|
||||
onRetagArtistPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ArtistEditorFooter;
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import RetagArtistModalContentConnector from './RetagArtistModalContentConnector';
|
||||
|
||||
function RetagArtistModal(props) {
|
||||
const {
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<RetagArtistModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
RetagArtistModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default RetagArtistModal;
|
||||
@@ -0,0 +1,8 @@
|
||||
.retagIcon {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import Alert from 'Components/Alert';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Icon from 'Components/Icon';
|
||||
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 styles from './RetagArtistModalContent.css';
|
||||
|
||||
function RetagArtistModalContent(props) {
|
||||
const {
|
||||
artistNames,
|
||||
onModalClose,
|
||||
onRetagArtistPress
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Retag Selected Artist
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<Alert>
|
||||
Tip: To preview the tags that will be written... select "Cancel" then click any artist name and use the
|
||||
<Icon
|
||||
className={styles.retagIcon}
|
||||
name={icons.RETAG}
|
||||
/>
|
||||
</Alert>
|
||||
|
||||
<div className={styles.message}>
|
||||
Are you sure you want to re-tag all files in the {artistNames.length} selected artist?
|
||||
</div>
|
||||
<ul>
|
||||
{
|
||||
artistNames.map((artistName) => {
|
||||
return (
|
||||
<li key={artistName}>
|
||||
{artistName}
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
kind={kinds.DANGER}
|
||||
onPress={onRetagArtistPress}
|
||||
>
|
||||
Retag
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
RetagArtistModalContent.propTypes = {
|
||||
artistNames: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onRetagArtistPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default RetagArtistModalContent;
|
||||
@@ -0,0 +1,67 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import RetagArtistModalContent from './RetagArtistModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { artistIds }) => artistIds,
|
||||
createAllArtistSelector(),
|
||||
(artistIds, allArtists) => {
|
||||
const artist = _.intersectionWith(allArtists, artistIds, (s, id) => {
|
||||
return s.id === id;
|
||||
});
|
||||
|
||||
const sortedArtist = _.orderBy(artist, 'sortName');
|
||||
const artistNames = _.map(sortedArtist, 'artistName');
|
||||
|
||||
return {
|
||||
artistNames
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
executeCommand
|
||||
};
|
||||
|
||||
class RetagArtistModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRetagArtistPress = () => {
|
||||
this.props.executeCommand({
|
||||
name: commandNames.RETAG_ARTIST,
|
||||
artistIds: this.props.artistIds
|
||||
});
|
||||
|
||||
this.props.onModalClose(true);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render(props) {
|
||||
return (
|
||||
<RetagArtistModalContent
|
||||
{...this.props}
|
||||
onRetagArtistPress={this.onRetagArtistPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RetagArtistModalContentConnector.propTypes = {
|
||||
artistIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
executeCommand: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(RetagArtistModalContentConnector);
|
||||
Reference in New Issue
Block a user