[UI Work] Interactive Import, More Artist Detail

This commit is contained in:
Qstick 2017-09-12 23:28:45 -04:00
parent 0054226307
commit f05332cf6e
42 changed files with 486 additions and 549 deletions

View File

@ -71,7 +71,7 @@ class ImportArtistRowConnector extends Component {
<ImportArtistRow
{...this.props}
onInputChange={this.onInputChange}
onSeriesSelect={this.onSeriesSelect}
onArtistSelect={this.onArtistSelect}
/>
);
}

View File

@ -99,10 +99,10 @@ class ImportArtistSelectArtist extends Component {
});
}
onSeriesSelect = (foreignArtistId) => {
onArtistSelect = (foreignArtistId) => {
this.setState({ isOpen: false });
this.props.onSeriesSelect(foreignArtistId);
this.props.onArtistSelect(foreignArtistId);
}
//
@ -117,7 +117,7 @@ class ImportArtistSelectArtist extends Component {
error,
items,
queued,
onSeriesSelect
onArtistSelect
} = this.props;
const errorMessage = error &&
@ -233,7 +233,7 @@ class ImportArtistSelectArtist extends Component {
overview={item.overview}
// year={item.year}
// network={item.network}
onPress={this.onSeriesSelect}
onPress={this.onArtistSelect}
/>
);
})
@ -257,7 +257,7 @@ ImportArtistSelectArtist.propTypes = {
items: PropTypes.arrayOf(PropTypes.object).isRequired,
queued: PropTypes.bool.isRequired,
onSearchInputChange: PropTypes.func.isRequired,
onSeriesSelect: PropTypes.func.isRequired
onArtistSelect: PropTypes.func.isRequired
};
ImportArtistSelectArtist.defaultProps = {

View File

@ -33,7 +33,7 @@ class ImportArtistSelectArtistConnector extends Component {
});
}
onSeriesSelect = (foreignArtistId) => {
onArtistSelect = (foreignArtistId) => {
const {
id,
items
@ -53,7 +53,7 @@ class ImportArtistSelectArtistConnector extends Component {
<ImportArtistSelectArtist
{...this.props}
onSearchInputChange={this.onSearchInputChange}
onSeriesSelect={this.onSeriesSelect}
onArtistSelect={this.onArtistSelect}
/>
);
}

View File

@ -45,7 +45,7 @@ function App({ store, history }) {
<PageConnector>
<Switch>
{/*
Series
Artist
*/}
<Route

View File

@ -112,7 +112,7 @@ class SeriesDetails extends Component {
this.setState(getExpandedState(selectAll(expandedState, !allExpanded)));
}
onExpandPress = (seasonNumber, isExpanded) => {
onExpandPress = (albumId, isExpanded) => {
this.setState((state) => {
const convertedState = {
allSelected: state.allExpanded,
@ -120,7 +120,7 @@ class SeriesDetails extends Component {
selectedState: state.expandedState
};
const newState = toggleSelected(convertedState, [], seasonNumber, isExpanded, false);
const newState = toggleSelected(convertedState, [], albumId, isExpanded, false);
return getExpandedState(newState);
});
@ -136,10 +136,9 @@ class SeriesDetails extends Component {
tvMazeId,
imdbId,
artistName,
runtime,
ratings,
sizeOnDisk,
episodeFileCount,
trackFileCount,
qualityProfileId,
monitored,
status,
@ -175,10 +174,10 @@ class SeriesDetails extends Component {
let episodeFilesCountMessage = 'No episode files';
if (episodeFileCount === 1) {
if (trackFileCount === 1) {
episodeFilesCountMessage = '1 episode file';
} else if (episodeFileCount > 1) {
episodeFilesCountMessage = `${episodeFileCount} episode files`;
} else if (trackFileCount > 1) {
episodeFilesCountMessage = `${trackFileCount} episode files`;
}
let expandIcon = icons.EXPAND_INDETERMINATE;
@ -310,13 +309,6 @@ class SeriesDetails extends Component {
<div className={styles.details}>
<div>
{
!!runtime &&
<span className={styles.runtime}>
{runtime} Minutes
</span>
}
<HeartRating
rating={ratings.value}
iconSize={20}
@ -491,10 +483,12 @@ class SeriesDetails extends Component {
albums.slice(0).reverse().map((season) => {
return (
<SeriesDetailsSeasonConnector
key={season.seasonNumber}
key={season.id}
artistId={id}
albumId={season.id}
statistics={season.statistics}
{...season}
isExpanded={expandedState[season.seasonNumber]}
isExpanded={expandedState[season.id]}
onExpandPress={this.onExpandPress}
/>
);
@ -548,10 +542,9 @@ SeriesDetails.propTypes = {
tvMazeId: PropTypes.number,
imdbId: PropTypes.string,
artistName: PropTypes.string.isRequired,
runtime: PropTypes.number.isRequired,
ratings: PropTypes.object.isRequired,
sizeOnDisk: PropTypes.number.isRequired,
episodeFileCount: PropTypes.number,
trackFileCount: PropTypes.number,
qualityProfileId: PropTypes.number.isRequired,
monitored: PropTypes.bool.isRequired,
status: PropTypes.string.isRequired,

View File

@ -16,30 +16,16 @@ function SeriesDetailsLinks(props) {
<div className={styles.links}>
<Link
className={styles.link}
to={`http://www.thetvdb.com/?tab=series&id=${foreignArtistId}`}
to={`https://musicbrainz.org/artist/${foreignArtistId}`}
>
<Label
className={styles.linkLabel}
kind={kinds.INFO}
size={sizes.LARGE}
>
The TVDB
Musicbrainz
</Label>
</Link>
<Link
className={styles.link}
to={`http://trakt.tv/search/tvdb/${foreignArtistId}?id_type=show`}
>
<Label
className={styles.linkLabel}
kind={kinds.INFO}
size={sizes.LARGE}
>
Trakt
</Label>
</Link>
{
!!tvMazeId &&
<Link

View File

@ -38,7 +38,7 @@
.left {
display: flex;
align-items: center;
flex: 0 1 300px;
flex: 0 1 600px;
}
.left,

View File

@ -23,30 +23,6 @@ import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnecto
import EpisodeRowConnector from './EpisodeRowConnector';
import styles from './SeriesDetailsSeason.css';
function getSeasonStatistics(episodes) {
let episodeCount = 0;
let episodeFileCount = 0;
let totalEpisodeCount = 0;
episodes.forEach((episode) => {
if (episode.episodeFileId || (episode.monitored && isBefore(episode.airDateUtc))) {
episodeCount++;
}
if (episode.episodeFileId) {
episodeFileCount++;
}
totalEpisodeCount++;
});
return {
episodeCount,
episodeFileCount,
totalEpisodeCount
};
}
function getEpisodeCountKind(monitored, episodeFileCount, episodeCount) {
if (episodeFileCount === episodeCount && episodeCount > 0) {
return kinds.SUCCESS;
@ -89,7 +65,7 @@ class SeriesDetailsSeason extends Component {
_expandByDefault() {
const {
seasonNumber,
albumId,
onExpandPress,
items
} = this.props;
@ -99,7 +75,7 @@ class SeriesDetailsSeason extends Component {
isAfter(item.airDateUtc, { days: -30 });
});
onExpandPress(seasonNumber, expand && seasonNumber > 0);
onExpandPress(albumId, expand && albumId > 0);
}
//
@ -123,11 +99,11 @@ class SeriesDetailsSeason extends Component {
onExpandPress = () => {
const {
seasonNumber,
albumId,
isExpanded
} = this.props;
this.props.onExpandPress(seasonNumber, !isExpanded);
this.props.onExpandPress(albumId, !isExpanded);
}
onMonitorEpisodePress = (episodeId, monitored, { shiftKey }) => {
@ -155,7 +131,10 @@ class SeriesDetailsSeason extends Component {
const {
artistId,
monitored,
seasonNumber,
title,
releaseDate,
albumId,
statistics,
items,
columns,
isSaving,
@ -169,10 +148,10 @@ class SeriesDetailsSeason extends Component {
} = this.props;
const {
episodeCount,
episodeFileCount,
totalEpisodeCount
} = getSeasonStatistics(items);
trackCount,
trackFileCount,
totalTrackCount
} = statistics;
const {
isOrganizeModalOpen,
@ -194,22 +173,22 @@ class SeriesDetailsSeason extends Component {
/>
{
seasonNumber === 0 ?
albumId === 0 ?
<span className={styles.seasonNumber}>
Specials
</span> :
<span className={styles.seasonNumber}>
Season {seasonNumber}
{title}
</span>
}
<Label
title={`${totalEpisodeCount} episodes total. ${episodeFileCount} episodes with files.`}
kind={getEpisodeCountKind(monitored, episodeFileCount, episodeCount)}
title={`${totalTrackCount} tracks total. ${trackFileCount} tracks with files.`}
kind={getEpisodeCountKind(monitored, trackFileCount, trackCount)}
size={sizes.LARGE}
>
{
<span>{episodeFileCount} / {episodeCount}</span>
<span>{trackFileCount} / {trackCount}</span>
}
</Label>
</div>
@ -218,12 +197,7 @@ class SeriesDetailsSeason extends Component {
className={styles.expandButton}
onPress={this.onExpandPress}
>
<Icon
className={styles.expandButtonIcon}
name={isExpanded ? icons.COLLAPSE : icons.EXPAND}
title={isExpanded ? 'Hide episodes' : 'Show episodes'}
size={24}
/>
{
!isSmallScreen &&
<span>&nbsp;</span>
@ -277,7 +251,7 @@ class SeriesDetailsSeason extends Component {
name={icons.EPISODE_FILE}
/>
Manage Episodes
Manage Tracks
</MenuItem>
</MenuContent>
</Menu> :
@ -286,7 +260,7 @@ class SeriesDetailsSeason extends Component {
<SpinnerIconButton
className={styles.actionButton}
name={icons.SEARCH}
title="Search for monitored episodes in this seasons"
title="Search for album"
size={24}
isSpinning={isSearching}
onPress={onSearchPress}
@ -295,7 +269,7 @@ class SeriesDetailsSeason extends Component {
<IconButton
className={styles.actionButton}
name={icons.ORGANIZE}
title="Preview rename for this season"
title="Preview rename for this album"
size={24}
onPress={this.onOrganizePress}
/>
@ -303,7 +277,7 @@ class SeriesDetailsSeason extends Component {
<IconButton
className={styles.actionButton}
name={icons.EPISODE_FILE}
title="Manage episode files in this series"
title="Manage track files in this artist"
size={24}
onPress={this.onManageEpisodesPress}
/>
@ -312,59 +286,17 @@ class SeriesDetailsSeason extends Component {
</div>
<div>
{
isExpanded &&
<div className={styles.episodes}>
{
items.length ?
<Table
columns={columns}
onTableOptionChange={onTableOptionChange}
>
<TableBody>
{
items.map((item) => {
return (
<EpisodeRowConnector
key={item.id}
columns={columns}
{...item}
onMonitorEpisodePress={this.onMonitorEpisodePress}
/>
);
})
}
</TableBody>
</Table> :
<div className={styles.noEpisodes}>
No episodes in this season
</div>
}
<div className={styles.collapseButtonContainer}>
<IconButton
name={icons.COLLAPSE}
size={20}
title="Hide episodes"
onPress={this.onExpandPress}
/>
</div>
</div>
}
</div>
<OrganizePreviewModalConnector
isOpen={isOrganizeModalOpen}
artistId={artistId}
seasonNumber={seasonNumber}
albumId={albumId}
onModalClose={this.onOrganizeModalClose}
/>
<EpisodeFileEditorModal
isOpen={isManageEpisodesOpen}
artistId={artistId}
seasonNumber={seasonNumber}
albumId={albumId}
onModalClose={this.onManageEpisodesModalClose}
/>
</div>
@ -375,7 +307,10 @@ class SeriesDetailsSeason extends Component {
SeriesDetailsSeason.propTypes = {
artistId: PropTypes.number.isRequired,
monitored: PropTypes.bool.isRequired,
seasonNumber: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
releaseDate: PropTypes.string.isRequired,
albumId: PropTypes.number.isRequired,
statistics: PropTypes.object.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
isSaving: PropTypes.bool,
@ -390,4 +325,12 @@ SeriesDetailsSeason.propTypes = {
onSearchPress: PropTypes.func.isRequired
};
SeriesDetailsSeason.defaultProps = {
statistics: {
trackFileCount: 0,
totalTrackCount: 0,
percentOfTracks: 0
}
};
export default SeriesDetailsSeason;

View File

@ -60,12 +60,12 @@ class SeriesDetailsSeasonConnector extends Component {
onMonitorSeasonPress = (monitored) => {
const {
artistId,
seasonNumber
albumId
} = this.props;
this.props.toggleSeasonMonitored({
artistId,
seasonNumber,
albumId,
monitored
});
}
@ -73,13 +73,13 @@ class SeriesDetailsSeasonConnector extends Component {
onSearchPress = () => {
const {
artistId,
seasonNumber
albumId
} = this.props;
this.props.executeCommand({
name: commandNames.SEASON_SEARCH,
artistId,
seasonNumber
albumIds: [albumId]
});
}
@ -108,7 +108,7 @@ class SeriesDetailsSeasonConnector extends Component {
SeriesDetailsSeasonConnector.propTypes = {
artistId: PropTypes.number.isRequired,
seasonNumber: PropTypes.number.isRequired,
albumId: PropTypes.number.isRequired,
toggleSeasonMonitored: PropTypes.func.isRequired,
toggleEpisodesMonitored: PropTypes.func.isRequired,
setEpisodesTableOption: PropTypes.func.isRequired,

View File

@ -15,5 +15,5 @@ export const RENAME_FILES = 'RenameFiles';
export const RENAME_ARTIST = 'RenameArtist';
export const RESET_API_KEY = 'ResetApiKey';
export const RSS_SYNC = 'RssSync';
export const SEASON_SEARCH = 'SeasonSearch';
export const SEASON_SEARCH = 'AlbumSearch';
export const ARTIST_SEARCH = 'ArtistSearch';

View File

@ -1,9 +1,9 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Modal from 'Components/Modal/Modal';
import SelectSeasonModalContentConnector from './SelectSeasonModalContentConnector';
import SelectAlbumModalContentConnector from './SelectAlbumModalContentConnector';
class SelectSeasonModal extends Component {
class SelectAlbumModal extends Component {
//
// Render
@ -20,7 +20,7 @@ class SelectSeasonModal extends Component {
isOpen={isOpen}
onModalClose={onModalClose}
>
<SelectSeasonModalContentConnector
<SelectAlbumModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
@ -29,9 +29,9 @@ class SelectSeasonModal extends Component {
}
}
SelectSeasonModal.propTypes = {
SelectAlbumModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default SelectSeasonModal;
export default SelectAlbumModal;

View File

@ -5,9 +5,9 @@ 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 SelectSeasonRow from './SelectSeasonRow';
import SelectAlbumRow from './SelectAlbumRow';
class SelectSeasonModalContent extends Component {
class SelectAlbumModalContent extends Component {
//
// Render
@ -15,24 +15,25 @@ class SelectSeasonModalContent extends Component {
render() {
const {
items,
onSeasonSelect,
onAlbumSelect,
onModalClose
} = this.props;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Manual Import - Select Season
Manual Import - Select Album
</ModalHeader>
<ModalBody>
{
items.map((item) => {
return (
<SelectSeasonRow
key={item.seasonNumber}
seasonNumber={item.seasonNumber}
onSeasonSelect={onSeasonSelect}
<SelectAlbumRow
key={item.id}
id={item.id}
title={item.title}
onAlbumSelect={onAlbumSelect}
/>
);
})
@ -49,10 +50,10 @@ class SelectSeasonModalContent extends Component {
}
}
SelectSeasonModalContent.propTypes = {
SelectAlbumModalContent.propTypes = {
items: PropTypes.arrayOf(PropTypes.object).isRequired,
onSeasonSelect: PropTypes.func.isRequired,
onAlbumSelect: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default SelectSeasonModalContent;
export default SelectAlbumModalContent;

View File

@ -1,17 +1,18 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions';
import createArtistSelector from 'Store/Selectors/createArtistSelector';
import SelectSeasonModalContent from './SelectSeasonModalContent';
import SelectAlbumModalContent from './SelectAlbumModalContent';
function createMapStateToProps() {
return createSelector(
createArtistSelector(),
(series) => {
return {
items: series.seasons
items: series.albums
};
}
);
@ -21,16 +22,18 @@ const mapDispatchToProps = {
updateInteractiveImportItem
};
class SelectSeasonModalContentConnector extends Component {
class SelectAlbumModalContentConnector extends Component {
//
// Listeners
onSeasonSelect = (seasonNumber) => {
onAlbumSelect = (albumId) => {
const album = _.find(this.props.items, { id: albumId });
this.props.ids.forEach((id) => {
this.props.updateInteractiveImportItem({
id,
seasonNumber,
album,
episodes: []
});
});
@ -43,15 +46,15 @@ class SelectSeasonModalContentConnector extends Component {
render() {
return (
<SelectSeasonModalContent
<SelectAlbumModalContent
{...this.props}
onSeasonSelect={this.onSeasonSelect}
onAlbumSelect={this.onAlbumSelect}
/>
);
}
}
SelectSeasonModalContentConnector.propTypes = {
SelectAlbumModalContentConnector.propTypes = {
ids: PropTypes.arrayOf(PropTypes.number).isRequired,
artistId: PropTypes.number.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
@ -59,4 +62,4 @@ SelectSeasonModalContentConnector.propTypes = {
onModalClose: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(SelectSeasonModalContentConnector);
export default connect(createMapStateToProps, mapDispatchToProps)(SelectAlbumModalContentConnector);

View File

@ -0,0 +1,37 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Link from 'Components/Link/Link';
import styles from './SelectAlbumRow.css';
class SelectAlbumRow extends Component {
//
// Listeners
onPress = () => {
this.props.onAlbumSelect(this.props.id);
}
//
// Render
render() {
return (
<Link
className={styles.season}
component="div"
onPress={this.onPress}
>
{this.props.title}
</Link>
);
}
}
SelectAlbumRow.propTypes = {
id: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
onAlbumSelect: PropTypes.func.isRequired
};
export default SelectAlbumRow;

View File

@ -37,7 +37,7 @@ class SelectArtistModalContent extends Component {
render() {
const {
items,
onSeriesSelect,
onArtistSelect,
onModalClose
} = this.props;
@ -46,7 +46,7 @@ class SelectArtistModalContent extends Component {
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Manual Import - Select Series
Manual Import - Select Artist
</ModalHeader>
<ModalBody
@ -55,7 +55,7 @@ class SelectArtistModalContent extends Component {
>
<TextInput
className={styles.filterInput}
placeholder="Filter series"
placeholder="Filter artist"
name="filter"
value={filter}
autoFocus={true}
@ -65,13 +65,13 @@ class SelectArtistModalContent extends Component {
<Scroller className={styles.scroller}>
{
items.map((item) => {
return item.title.toLowerCase().includes(filter) ?
return item.artistName.toLowerCase().includes(filter) ?
(
<SelectArtistRow
key={item.id}
id={item.id}
title={item.title}
onSeriesSelect={onSeriesSelect}
artistName={item.artistName}
onArtistSelect={onArtistSelect}
/>
) :
null;
@ -92,7 +92,7 @@ class SelectArtistModalContent extends Component {
SelectArtistModalContent.propTypes = {
items: PropTypes.arrayOf(PropTypes.object).isRequired,
onSeriesSelect: PropTypes.func.isRequired,
onArtistSelect: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};

View File

@ -27,15 +27,15 @@ class SelectArtistModalContentConnector extends Component {
//
// Listeners
onSeriesSelect = (artistId) => {
const series = _.find(this.props.items, { id: artistId });
onArtistSelect = (artistId) => {
const artist = _.find(this.props.items, { id: artistId });
this.props.ids.forEach((id) => {
this.props.updateInteractiveImportItem({
id,
series,
seasonNumber: undefined,
episodes: []
artist,
album: undefined,
tracks: []
});
});
@ -49,7 +49,7 @@ class SelectArtistModalContentConnector extends Component {
return (
<SelectArtistModalContent
{...this.props}
onSeriesSelect={this.onSeriesSelect}
onArtistSelect={this.onArtistSelect}
/>
);
}

View File

@ -1,4 +1,4 @@
.series {
.artist {
padding: 8px;
border-bottom: 1px solid $borderColor;
}

View File

@ -9,7 +9,7 @@ class SelectArtistRow extends Component {
// Listeners
onPress = () => {
this.props.onSeriesSelect(this.props.id);
this.props.onArtistSelect(this.props.id);
}
//
@ -18,11 +18,11 @@ class SelectArtistRow extends Component {
render() {
return (
<Link
className={styles.series}
className={styles.artist}
component="div"
onPress={this.onPress}
>
{this.props.title}
{this.props.artistName}
</Link>
);
}
@ -30,8 +30,8 @@ class SelectArtistRow extends Component {
SelectArtistRow.propTypes = {
id: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
onSeriesSelect: PropTypes.func.isRequired
artistName: PropTypes.string.isRequired,
onArtistSelect: PropTypes.func.isRequired
};
export default SelectArtistRow;

View File

@ -15,8 +15,8 @@ import ModalBody from 'Components/Modal/ModalBody';
import ModalFooter from 'Components/Modal/ModalFooter';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import SelectArtistModal from 'InteractiveImport/Series/SelectArtistModal';
import SelectSeasonModal from 'InteractiveImport/Season/SelectSeasonModal';
import SelectArtistModal from 'InteractiveImport/Artist/SelectArtistModal';
import SelectAlbumModal from 'InteractiveImport/Album/SelectAlbumModal';
import InteractiveImportRow from './InteractiveImportRow';
import styles from './InteractiveImportModalContent.css';
@ -28,19 +28,19 @@ const columns = [
isVisible: true
},
{
name: 'series',
label: 'Series',
name: 'artist',
label: 'Artist',
isSortable: true,
isVisible: true
},
{
name: 'season',
label: 'Season',
name: 'album',
label: 'Album',
isVisible: true
},
{
name: 'episodes',
label: 'Episode(s)',
name: 'tracks',
label: 'Track(s)',
isVisible: true
},
{
@ -79,7 +79,7 @@ class InteractiveImportModalContent extends Component {
selectedState: {},
invalidRowsSelected: [],
isSelectArtistModalOpen: false,
isSelectSeasonModalOpen: false
isSelectAlbumModalOpen: false
};
}
@ -131,16 +131,16 @@ class InteractiveImportModalContent extends Component {
this.setState({ isSelectArtistModalOpen: true });
}
onSelectSeasonPress = () => {
this.setState({ isSelectSeasonModalOpen: true });
onSelectAlbumPress = () => {
this.setState({ isSelectAlbumModalOpen: true });
}
onSelectArtistModalClose = () => {
this.setState({ isSelectArtistModalOpen: false });
}
onSelectSeasonModalClose = () => {
this.setState({ isSelectSeasonModalOpen: false });
onSelectAlbumModalClose = () => {
this.setState({ isSelectAlbumModalOpen: false });
}
//
@ -169,7 +169,7 @@ class InteractiveImportModalContent extends Component {
selectedState,
invalidRowsSelected,
isSelectArtistModalOpen,
isSelectSeasonModalOpen
isSelectAlbumModalOpen
} = this.state;
const selectedIds = this.getSelectedIds();
@ -250,11 +250,11 @@ class InteractiveImportModalContent extends Component {
<div className={downloadId ? styles.leftButtons : styles.centerButtons}>
<Button onPress={this.onSelectArtistPress}>
Select Series
Select Artist
</Button>
<Button onPress={this.onSelectSeasonPress}>
Select Season
<Button onPress={this.onSelectAlbumPress}>
Select Album
</Button>
</div>
@ -284,11 +284,11 @@ class InteractiveImportModalContent extends Component {
onModalClose={this.onSelectArtistModalClose}
/>
<SelectSeasonModal
isOpen={isSelectSeasonModalOpen}
<SelectAlbumModal
isOpen={isSelectAlbumModalOpen}
ids={selectedIds}
artistId={selectedItem && selectedItem.series && selectedItem.series.id}
onModalClose={this.onSelectSeasonModalClose}
onModalClose={this.onSelectAlbumModalClose}
/>
</ModalContent>
);

View File

@ -71,31 +71,32 @@ class InteractiveImportModalContentConnector extends Component {
if (isSelected) {
const {
series,
seasonNumber,
episodes,
artist,
album,
tracks,
quality
} = item;
if (!series) {
this.setState({ interactiveImportErrorMessage: 'Series must be chosen for each selected file' });
if (!artist) {
this.setState({ interactiveImportErrorMessage: 'Artist must be chosen for each selected file' });
return false;
}
if (isNaN(seasonNumber)) {
this.setState({ interactiveImportErrorMessage: 'Season must be chosen for each selected file' });
if (!album) {
this.setState({ interactiveImportErrorMessage: 'Album must be chosen for each selected file' });
return false;
}
if (!episodes || !episodes.length) {
this.setState({ interactiveImportErrorMessage: 'One or more episodes must be chosen for each selected file' });
if (!tracks || !tracks.length) {
this.setState({ interactiveImportErrorMessage: 'One or more tracks must be chosen for each selected file' });
return false;
}
files.push({
path: item.path,
artistId: series.id,
episodeIds: _.map(episodes, 'id'),
artistId: artist.id,
albumId: album.id,
trackIds: _.map(tracks, 'id'),
quality,
downloadId: this.props.downloadId
});

View File

@ -9,9 +9,9 @@ import TableRowCellButton from 'Components/Table/Cells/TableRowCellButton';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import Popover from 'Components/Tooltip/Popover';
import EpisodeQuality from 'Episode/EpisodeQuality';
import SelectArtistModal from 'InteractiveImport/Series/SelectArtistModal';
import SelectSeasonModal from 'InteractiveImport/Season/SelectSeasonModal';
import SelectEpisodeModal from 'InteractiveImport/Episode/SelectEpisodeModal';
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 InteractiveImportRowCellPlaceholder from './InteractiveImportRowCellPlaceholder';
import styles from './InteractiveImportRow.css';
@ -26,8 +26,8 @@ class InteractiveImportRow extends Component {
this.state = {
isSelectArtistModalOpen: false,
isSelectSeasonModalOpen: false,
isSelectEpisodeModalOpen: false,
isSelectAlbumModalOpen: false,
isSelectTrackModalOpen: false,
isSelectQualityModalOpen: false
};
}
@ -35,13 +35,13 @@ class InteractiveImportRow extends Component {
componentDidMount() {
const {
id,
series,
seasonNumber,
episodes,
artist,
album,
tracks,
quality
} = this.props;
if (series && seasonNumber !== undefined && episodes.length && quality) {
if (artist && album !== undefined && tracks.length && quality) {
this.props.onSelectedChange({ id, value: true });
}
}
@ -49,9 +49,9 @@ class InteractiveImportRow extends Component {
componentDidUpdate(prevProps) {
const {
id,
series,
seasonNumber,
episodes,
artist,
album,
tracks,
quality,
isSelected,
onValidRowChange
@ -61,7 +61,7 @@ class InteractiveImportRow extends Component {
return;
}
const isValid = !!(series && seasonNumber != null && episodes.length && quality);
const isValid = !!(artist && album != null && tracks.length && quality);
if (isSelected && !isValid) {
onValidRowChange(id, false);
@ -91,12 +91,12 @@ class InteractiveImportRow extends Component {
this.setState({ isSelectArtistModalOpen: true });
}
onSelectSeasonPress = () => {
this.setState({ isSelectSeasonModalOpen: true });
onSelectAlbumPress = () => {
this.setState({ isSelectAlbumModalOpen: true });
}
onSelectEpisodePress = () => {
this.setState({ isSelectEpisodeModalOpen: true });
onSelectTrackPress = () => {
this.setState({ isSelectTrackModalOpen: true });
}
onSelectQualityPress = () => {
@ -108,13 +108,13 @@ class InteractiveImportRow extends Component {
this.selectRowAfterChange(changed);
}
onSelectSeasonModalClose = (changed) => {
this.setState({ isSelectSeasonModalOpen: false });
onSelectAlbumModalClose = (changed) => {
this.setState({ isSelectAlbumModalOpen: false });
this.selectRowAfterChange(changed);
}
onSelectEpisodeModalClose = (changed) => {
this.setState({ isSelectEpisodeModalOpen: false });
onSelectTrackModalClose = (changed) => {
this.setState({ isSelectTrackModalOpen: false });
this.selectRowAfterChange(changed);
}
@ -130,9 +130,9 @@ class InteractiveImportRow extends Component {
const {
id,
relativePath,
series,
seasonNumber,
episodes,
artist,
album,
tracks,
quality,
size,
rejections,
@ -142,18 +142,19 @@ class InteractiveImportRow extends Component {
const {
isSelectArtistModalOpen,
isSelectSeasonModalOpen,
isSelectEpisodeModalOpen,
isSelectAlbumModalOpen,
isSelectTrackModalOpen,
isSelectQualityModalOpen
} = this.state;
const seriesTitle = series ? series.title : '';
const episodeNumbers = episodes.map((episode) => episode.episodeNumber)
const seriesTitle = artist ? artist.artistName : '';
const albumTitle = album ? album.title : '';
const trackNumbers = tracks.map((episode) => episode.trackNumber)
.join(', ');
const showSeriesPlaceholder = isSelected && !series;
const showSeasonNumberPlaceholder = isSelected && !!series && isNaN(seasonNumber);
const showEpisodeNumbersPlaceholder = isSelected && Number.isInteger(seasonNumber) && !episodes.length;
const showSeriesPlaceholder = isSelected && !artist;
const showSeasonNumberPlaceholder = isSelected && !!artist && !album;
const showEpisodeNumbersPlaceholder = isSelected && !!album && !tracks.length;
return (
<TableRow>
@ -179,20 +180,20 @@ class InteractiveImportRow extends Component {
</TableRowCellButton>
<TableRowCellButton
isDisabled={!series}
onPress={this.onSelectSeasonPress}
isDisabled={!artist}
onPress={this.onSelectAlbumPress}
>
{
showSeasonNumberPlaceholder ? <InteractiveImportRowCellPlaceholder /> : seasonNumber
showSeasonNumberPlaceholder ? <InteractiveImportRowCellPlaceholder /> : albumTitle
}
</TableRowCellButton>
<TableRowCellButton
isDisabled={!series || isNaN(seasonNumber)}
onPress={this.onSelectEpisodePress}
isDisabled={!artist || !album}
onPress={this.onSelectTrackPress}
>
{
showEpisodeNumbersPlaceholder ? <InteractiveImportRowCellPlaceholder /> : episodeNumbers
showEpisodeNumbersPlaceholder ? <InteractiveImportRowCellPlaceholder /> : trackNumbers
}
</TableRowCellButton>
@ -244,19 +245,19 @@ class InteractiveImportRow extends Component {
onModalClose={this.onSelectArtistModalClose}
/>
<SelectSeasonModal
isOpen={isSelectSeasonModalOpen}
<SelectAlbumModal
isOpen={isSelectAlbumModalOpen}
ids={[id]}
artistId={series && series.id}
onModalClose={this.onSelectSeasonModalClose}
artistId={artist && artist.id}
onModalClose={this.onSelectAlbumModalClose}
/>
<SelectEpisodeModal
isOpen={isSelectEpisodeModalOpen}
<SelectTrackModal
isOpen={isSelectTrackModalOpen}
id={id}
artistId={series && series.id}
seasonNumber={seasonNumber}
onModalClose={this.onSelectEpisodeModalClose}
artistId={artist && artist.id}
albumId={album && album.id}
onModalClose={this.onSelectTrackModalClose}
/>
<SelectQualityModal
@ -276,9 +277,9 @@ class InteractiveImportRow extends Component {
InteractiveImportRow.propTypes = {
id: PropTypes.number.isRequired,
relativePath: PropTypes.string.isRequired,
series: PropTypes.object,
seasonNumber: PropTypes.number,
episodes: PropTypes.arrayOf(PropTypes.object).isRequired,
artist: PropTypes.object,
album: PropTypes.object,
tracks: PropTypes.arrayOf(PropTypes.object).isRequired,
quality: PropTypes.object,
size: PropTypes.number.isRequired,
rejections: PropTypes.arrayOf(PropTypes.object).isRequired,
@ -288,7 +289,7 @@ InteractiveImportRow.propTypes = {
};
InteractiveImportRow.defaultProps = {
episodes: []
tracks: []
};
export default InteractiveImportRow;

View File

@ -1,40 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Link from 'Components/Link/Link';
import styles from './SelectSeasonRow.css';
class SelectSeasonRow extends Component {
//
// Listeners
onPress = () => {
this.props.onSeasonSelect(this.props.seasonNumber);
}
//
// Render
render() {
const seasonNumber = this.props.seasonNumber;
return (
<Link
className={styles.season}
component="div"
onPress={this.onPress}
>
{
seasonNumber === 0 ? 'Specials' : `Season ${seasonNumber}`
}
</Link>
);
}
}
SelectSeasonRow.propTypes = {
seasonNumber: PropTypes.number.isRequired,
onSeasonSelect: PropTypes.func.isRequired
};
export default SelectSeasonRow;

View File

@ -1,9 +1,9 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Modal from 'Components/Modal/Modal';
import SelectEpisodeModalContentConnector from './SelectEpisodeModalContentConnector';
import SelectTrackModalContentConnector from './SelectTrackModalContentConnector';
class SelectEpisodeModal extends Component {
class SelectTrackModal extends Component {
//
// Render
@ -20,7 +20,7 @@ class SelectEpisodeModal extends Component {
isOpen={isOpen}
onModalClose={onModalClose}
>
<SelectEpisodeModalContentConnector
<SelectTrackModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
@ -29,9 +29,9 @@ class SelectEpisodeModal extends Component {
}
}
SelectEpisodeModal.propTypes = {
SelectTrackModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default SelectEpisodeModal;
export default SelectTrackModal;

View File

@ -12,11 +12,11 @@ import ModalBody from 'Components/Modal/ModalBody';
import ModalFooter from 'Components/Modal/ModalFooter';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import SelectEpisodeRow from './SelectEpisodeRow';
import SelectTrackRow from './SelectTrackRow';
const columns = [
{
name: 'episodeNumber',
name: 'trackNumber',
label: '#',
isSortable: true,
isVisible: true
@ -25,15 +25,10 @@ const columns = [
name: 'title',
label: 'Title',
isVisible: true
},
{
name: 'airDate',
label: 'Air Date',
isVisible: true
}
];
class SelectEpisodeModalContent extends Component {
class SelectTrackModalContent extends Component {
//
// Lifecycle
@ -69,8 +64,8 @@ class SelectEpisodeModalContent extends Component {
});
}
onEpisodesSelect = () => {
this.props.onEpisodesSelect(this.getSelectedIds());
onTracksSelect = () => {
this.props.onTracksSelect(this.getSelectedIds());
}
//
@ -94,12 +89,12 @@ class SelectEpisodeModalContent extends Component {
selectedState
} = this.state;
const errorMessage = error && error.message || 'Unable to load episodes';
const errorMessage = error && error.message || 'Unable to load tracks';
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Manual Import - Select Episode(s)
Manual Import - Select Track(s)
</ModalHeader>
<ModalBody>
@ -129,12 +124,11 @@ class SelectEpisodeModalContent extends Component {
{
items.map((item) => {
return (
<SelectEpisodeRow
<SelectTrackRow
key={item.id}
id={item.id}
episodeNumber={item.episodeNumber}
trackNumber={item.trackNumber}
title={item.title}
airDate={item.airDate}
isSelected={selectedState[item.id]}
onSelectedChange={this.onSelectedChange}
/>
@ -147,7 +141,7 @@ class SelectEpisodeModalContent extends Component {
{
isPopulated && !items.length &&
'No episodes were found for the selected season'
'No tracks were found for the selected album'
}
</ModalBody>
@ -158,9 +152,9 @@ class SelectEpisodeModalContent extends Component {
<Button
kind={kinds.SUCCESS}
onPress={this.onEpisodesSelect}
onPress={this.onTracksSelect}
>
Select Episodes
Select Tracks
</Button>
</ModalFooter>
</ModalContent>
@ -168,7 +162,7 @@ class SelectEpisodeModalContent extends Component {
}
}
SelectEpisodeModalContent.propTypes = {
SelectTrackModalContent.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
@ -176,8 +170,8 @@ SelectEpisodeModalContent.propTypes = {
sortKey: PropTypes.string,
sortDirection: PropTypes.string,
onSortPress: PropTypes.func.isRequired,
onEpisodesSelect: PropTypes.func.isRequired,
onTracksSelect: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default SelectEpisodeModalContent;
export default SelectTrackModalContent;

View File

@ -6,7 +6,7 @@ import connectSection from 'Store/connectSection';
import { fetchEpisodes, setEpisodesSort, clearEpisodes } from 'Store/Actions/episodeActions';
import { updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import SelectEpisodeModalContent from './SelectEpisodeModalContent';
import SelectTrackModalContent from './SelectTrackModalContent';
function createMapStateToProps() {
return createSelector(
@ -24,7 +24,7 @@ const mapDispatchToProps = {
updateInteractiveImportItem
};
class SelectEpisodeModalContentConnector extends Component {
class SelectTrackModalContentConnector extends Component {
//
// Lifecycle
@ -32,10 +32,10 @@ class SelectEpisodeModalContentConnector extends Component {
componentDidMount() {
const {
artistId,
seasonNumber
albumId
} = this.props;
this.props.fetchEpisodes({ artistId, seasonNumber });
this.props.fetchEpisodes({ artistId, albumId });
}
componentWillUnmount() {
@ -51,8 +51,8 @@ class SelectEpisodeModalContentConnector extends Component {
this.props.setEpisodesSort({ sortKey, sortDirection });
}
onEpisodesSelect = (episodeIds) => {
const episodes = _.reduce(this.props.items, (acc, item) => {
onTracksSelect = (episodeIds) => {
const tracks = _.reduce(this.props.items, (acc, item) => {
if (episodeIds.indexOf(item.id) > -1) {
acc.push(item);
}
@ -62,7 +62,7 @@ class SelectEpisodeModalContentConnector extends Component {
this.props.updateInteractiveImportItem({
id: this.props.id,
episodes: _.sortBy(episodes, 'episodeNumber')
tracks: _.sortBy(tracks, 'trackNumber')
});
this.props.onModalClose(true);
@ -73,19 +73,19 @@ class SelectEpisodeModalContentConnector extends Component {
render() {
return (
<SelectEpisodeModalContent
<SelectTrackModalContent
{...this.props}
onSortPress={this.onSortPress}
onEpisodesSelect={this.onEpisodesSelect}
onTracksSelect={this.onTracksSelect}
/>
);
}
}
SelectEpisodeModalContentConnector.propTypes = {
SelectTrackModalContentConnector.propTypes = {
id: PropTypes.number.isRequired,
artistId: PropTypes.number.isRequired,
seasonNumber: PropTypes.number.isRequired,
albumId: PropTypes.number.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
fetchEpisodes: PropTypes.func.isRequired,
setEpisodesSort: PropTypes.func.isRequired,
@ -100,4 +100,4 @@ export default connectSection(
undefined,
undefined,
{ section: 'episodes' }
)(SelectEpisodeModalContentConnector);
)(SelectTrackModalContentConnector);

View File

@ -4,7 +4,7 @@ import TableRowButton from 'Components/Table/TableRowButton';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
class SelectEpisodeRow extends Component {
class SelectTrackRow extends Component {
//
// Listeners
@ -24,9 +24,8 @@ class SelectEpisodeRow extends Component {
render() {
const {
id,
episodeNumber,
trackNumber,
title,
airDate,
isSelected,
onSelectedChange
} = this.props;
@ -40,28 +39,24 @@ class SelectEpisodeRow extends Component {
/>
<TableRowCell>
{episodeNumber}
{trackNumber}
</TableRowCell>
<TableRowCell>
{title}
</TableRowCell>
<TableRowCell>
{airDate}
</TableRowCell>
</TableRowButton>
);
}
}
SelectEpisodeRow.propTypes = {
SelectTrackRow.propTypes = {
id: PropTypes.number.isRequired,
episodeNumber: PropTypes.number.isRequired,
trackNumber: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
airDate: PropTypes.string.isRequired,
isSelected: PropTypes.bool,
onSelectedChange: PropTypes.func.isRequired
};
export default SelectEpisodeRow;
export default SelectTrackRow;

View File

@ -9,7 +9,7 @@ import { updateItem } from './baseActions';
const section = 'episodes';
const episodeActionHandlers = {
[types.FETCH_EPISODES]: createFetchHandler(section, '/episode'),
[types.FETCH_EPISODES]: createFetchHandler(section, '/track'),
[types.TOGGLE_EPISODE_MONITORED]: function(payload) {
return function(dispatch, getState) {

View File

@ -8,10 +8,10 @@ import * as types from './actionTypes';
import { set, removeItem, updateItem } from './baseActions';
const section = 'episodeFiles';
const deleteEpisodeFile = createRemoveItemHandler(section, '/episodeFile');
const deleteEpisodeFile = createRemoveItemHandler(section, '/trackFile');
const episodeFileActionHandlers = {
[types.FETCH_EPISODE_FILES]: createFetchHandler(section, '/episodeFile'),
[types.FETCH_EPISODE_FILES]: createFetchHandler(section, '/trackFile'),
[types.DELETE_EPISODE_FILE]: function(payload) {
return function(dispatch, getState) {

View File

@ -13,8 +13,8 @@ export default function(history) {
isProduction
} = window.Sonarr;
const dsn = isProduction ? 'https://b80ca60625b443c38b242e0d21681eb7@sentry.sonarr.tv/13' :
'https://8dbaacdfe2ff4caf97dc7945aecf9ace@sentry.sonarr.tv/12';
const dsn = isProduction ? 'https://c3a5b33e08de4e18b7d0505e942dbc95:e35e6d535b034995a6896022c6bfed04@sentry.io/216290' :
'https://c3a5b33e08de4e18b7d0505e942dbc95:e35e6d535b034995a6896022c6bfed04@sentry.io/216290';
const middlewares = [];

View File

@ -60,11 +60,6 @@ namespace Lidarr.Api.V3.Artist
//public bool SeasonFolder { get; set; }
//public bool Monitored { get; set; }
//public bool UseSceneNumbering { get; set; }
//public int Runtime { get; set; }
//public int TvdbId { get; set; }
//public int TvRageId { get; set; }
//public int TvMazeId { get; set; }
//public DateTime? FirstAired { get; set; }
public DateTime? LastInfoSync { get; set; }
////public SeriesTypes SeriesType { get; set; }
@ -138,10 +133,6 @@ namespace Lidarr.Api.V3.Artist
//AlternateTitles
SortName = model.SortName,
//TotalEpisodeCount
//EpisodeCount
//EpisodeFileCount
//SizeOnDisk
Status = model.Status,
Overview = model.Overview,
//NextAiring
@ -160,12 +151,6 @@ namespace Lidarr.Api.V3.Artist
AlbumFolder = model.AlbumFolder,
Monitored = model.Monitored,
//UseSceneNumbering = model.UseSceneNumbering,
//Runtime = model.Runtime,
//TvdbId = model.TvdbId,
//TvRageId = model.TvRageId,
//TvMazeId = model.TvMazeId,
//FirstAired = model.FirstAired,
LastInfoSync = model.LastInfoSync,
//SeriesType = model.SeriesType,
CleanName = model.CleanName,
@ -193,10 +178,6 @@ namespace Lidarr.Api.V3.Artist
//AlternateTitles
SortName = resource.SortName,
//TotalEpisodeCount
//EpisodeCount
//EpisodeFileCount
//SizeOnDisk
Status = resource.Status,
Overview = resource.Overview,
//NextAiring
@ -215,12 +196,6 @@ namespace Lidarr.Api.V3.Artist
AlbumFolder = resource.AlbumFolder,
Monitored = resource.Monitored,
//UseSceneNumbering = resource.UseSceneNumbering,
//Runtime = resource.Runtime,
//TvdbId = resource.TvdbId,
//TvRageId = resource.TvRageId,
//TvMazeId = resource.TvMazeId,
//FirstAired = resource.FirstAired,
LastInfoSync = resource.LastInfoSync,
//SeriesType = resource.SeriesType,
CleanName = resource.CleanName,

View File

@ -3,6 +3,7 @@ using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.MediaFiles.TrackImport.Manual;
using NzbDrone.Core.Qualities;
using Lidarr.Api.V3.Artist;
using Lidarr.Api.V3.Albums;
using Lidarr.Api.V3.Tracks;
using Lidarr.Http.REST;
using System.Collections.Generic;
@ -16,9 +17,9 @@ namespace Lidarr.Api.V3.ManualImport
public string RelativePath { get; set; }
public string Name { get; set; }
public long Size { get; set; }
//public ArtistResource Artist { get; set; }
public int? SeasonNumber { get; set; }
public List<TrackResource> Episodes { get; set; }
public ArtistResource Artist { get; set; }
public AlbumResource Album { get; set; }
public List<TrackResource> Tracks { get; set; }
public QualityModel Quality { get; set; }
public int QualityWeight { get; set; }
public string DownloadId { get; set; }
@ -38,9 +39,9 @@ namespace Lidarr.Api.V3.ManualImport
RelativePath = model.RelativePath,
Name = model.Name,
Size = model.Size,
//Artist = model.,
SeasonNumber = model.SeasonNumber,
//Episodes = model.Episodes.ToResource(),
Artist = model.Artist.ToResource(),
Album = model.Album.ToResource(),
Tracks = model.Tracks.ToResource(),
Quality = model.Quality,
//QualityWeight
DownloadId = model.DownloadId,

View File

@ -15,9 +15,6 @@ namespace NzbDrone.Api.ManualImport
public string RelativePath { get; set; }
public string Name { get; set; }
public long Size { get; set; }
public SeriesResource Series { get; set; }
public int? SeasonNumber { get; set; }
public List<EpisodeResource> Episodes { get; set; }
public QualityModel Quality { get; set; }
public int QualityWeight { get; set; }
public string DownloadId { get; set; }
@ -38,9 +35,6 @@ namespace NzbDrone.Api.ManualImport
RelativePath = model.RelativePath,
Name = model.Name,
Size = model.Size,
Series = model.Series.ToResource(),
SeasonNumber = model.SeasonNumber,
Episodes = model.Episodes.ToResource(),
Quality = model.Quality,
//QualityWeight
DownloadId = model.DownloadId,

View File

@ -84,7 +84,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
trackFile.Size = _diskProvider.GetFileSize(localTrack.Path);
trackFile.Quality = localTrack.Quality;
trackFile.MediaInfo = localTrack.MediaInfo;
trackFile.AlbumId = _albumRepository.FindByArtistAndName(localTrack.Artist.Name, Parser.Parser.CleanArtistTitle(localTrack.ParsedTrackInfo.AlbumTitle)).Id;
trackFile.AlbumId = localTrack.Album.Id;
trackFile.ReleaseGroup = localTrack.ParsedTrackInfo.ReleaseGroup;
trackFile.Tracks = localTrack.Tracks;
trackFile.Language = localTrack.Language;

View File

@ -6,8 +6,9 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
public class ManualImportFile
{
public string Path { get; set; }
public int SeriesId { get; set; }
public List<int> EpisodeIds { get; set; }
public int ArtistId { get; set; }
public int AlbumId { get; set; }
public List<int> TrackIds { get; set; }
public QualityModel Quality { get; set; }
public string DownloadId { get; set; }
}

View File

@ -1,7 +1,7 @@
using System.Collections.Generic;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Music;
namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
{
@ -11,9 +11,9 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
public string RelativePath { get; set; }
public string Name { get; set; }
public long Size { get; set; }
public Series Series { get; set; }
public int? SeasonNumber { get; set; }
public List<Episode> Episodes { get; set; }
public Artist Artist { get; set; }
public Album Album { get; set; }
public List<Track> Tracks { get; set; }
public QualityModel Quality { get; set; }
public string DownloadId { get; set; }
public IEnumerable<Rejection> Rejections { get; set; }

View File

@ -14,7 +14,7 @@ using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Music;
namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
{
@ -29,10 +29,11 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
private readonly IParsingService _parsingService;
private readonly IDiskScanService _diskScanService;
private readonly IMakeImportDecision _importDecisionMaker;
private readonly ISeriesService _seriesService;
private readonly IEpisodeService _episodeService;
private readonly IArtistService _artistService;
private readonly IAlbumService _albumService;
private readonly ITrackService _trackService;
private readonly IVideoFileInfoReader _videoFileInfoReader;
private readonly IImportApprovedEpisodes _importApprovedEpisodes;
private readonly IImportApprovedTracks _importApprovedTracks;
private readonly ITrackedDownloadService _trackedDownloadService;
private readonly IDownloadedEpisodesImportService _downloadedEpisodesImportService;
private readonly IEventAggregator _eventAggregator;
@ -42,10 +43,11 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
IParsingService parsingService,
IDiskScanService diskScanService,
IMakeImportDecision importDecisionMaker,
ISeriesService seriesService,
IEpisodeService episodeService,
IArtistService artistService,
IAlbumService albumService,
ITrackService trackService,
IVideoFileInfoReader videoFileInfoReader,
IImportApprovedEpisodes importApprovedEpisodes,
IImportApprovedTracks importApprovedTracks,
ITrackedDownloadService trackedDownloadService,
IDownloadedEpisodesImportService downloadedEpisodesImportService,
IEventAggregator eventAggregator,
@ -55,10 +57,11 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
_parsingService = parsingService;
_diskScanService = diskScanService;
_importDecisionMaker = importDecisionMaker;
_seriesService = seriesService;
_episodeService = episodeService;
_artistService = artistService;
_albumService = albumService;
_trackService = trackService;
_videoFileInfoReader = videoFileInfoReader;
_importApprovedEpisodes = importApprovedEpisodes;
_importApprovedTracks = importApprovedTracks;
_trackedDownloadService = trackedDownloadService;
_downloadedEpisodesImportService = downloadedEpisodesImportService;
_eventAggregator = eventAggregator;
@ -94,179 +97,176 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
private List<ManualImportItem> ProcessFolder(string folder, string downloadId)
{
throw new System.NotImplementedException("TODO: This will be rewritten for Music");
//var directoryInfo = new DirectoryInfo(folder);
//var series = _parsingService.GetSeries(directoryInfo.Name);
var directoryInfo = new DirectoryInfo(folder);
var artist = _parsingService.GetArtist(directoryInfo.Name);
//if (series == null && downloadId.IsNotNullOrWhiteSpace())
//{
// var trackedDownload = _trackedDownloadService.Find(downloadId);
// series = trackedDownload.RemoteEpisode.Series;
//}
if (artist == null && downloadId.IsNotNullOrWhiteSpace())
{
var trackedDownload = _trackedDownloadService.Find(downloadId);
artist = trackedDownload.RemoteAlbum.Artist;
}
//if (series == null)
//{
// var files = _diskScanService.GetVideoFiles(folder);
if (artist == null)
{
var files = _diskScanService.GetAudioFiles(folder);
// return files.Select(file => ProcessFile(file, downloadId, folder)).Where(i => i != null).ToList();
//}
return files.Select(file => ProcessFile(file, downloadId, folder)).Where(i => i != null).ToList();
}
//var folderInfo = Parser.Parser.ParseTitle(directoryInfo.Name);
//var seriesFiles = _diskScanService.GetVideoFiles(folder).ToList();
//var decisions = _importDecisionMaker.GetImportDecisions(seriesFiles, series, folderInfo, SceneSource(series, folder));
var folderInfo = Parser.Parser.ParseMusicTitle(directoryInfo.Name);
var artistFiles = _diskScanService.GetAudioFiles(folder).ToList();
var decisions = _importDecisionMaker.GetImportDecisions(artistFiles, artist, folderInfo);
//return decisions.Select(decision => MapItem(decision, folder, downloadId)).ToList();
return decisions.Select(decision => MapItem(decision, folder, downloadId)).ToList();
}
private ManualImportItem ProcessFile(string file, string downloadId, string folder = null)
{
throw new System.NotImplementedException("TODO: This will be rewritten for Music");
//if (folder.IsNullOrWhiteSpace())
//{
// folder = new FileInfo(file).Directory.FullName;
//}
if (folder.IsNullOrWhiteSpace())
{
folder = new FileInfo(file).Directory.FullName;
}
//var relativeFile = folder.GetRelativePath(file);
var relativeFile = folder.GetRelativePath(file);
//var series = _parsingService.GetSeries(relativeFile.Split('\\', '/')[0]);
var artist = _parsingService.GetArtist(relativeFile.Split('\\', '/')[0]);
//if (series == null)
//{
// series = _parsingService.GetSeries(relativeFile);
//}
if (artist == null)
{
artist = _parsingService.GetArtistFromTag(file);
}
//if (series == null && downloadId.IsNotNullOrWhiteSpace())
//{
// var trackedDownload = _trackedDownloadService.Find(downloadId);
// series = trackedDownload.RemoteEpisode.Series;
//}
if (artist == null && downloadId.IsNotNullOrWhiteSpace())
{
var trackedDownload = _trackedDownloadService.Find(downloadId);
artist = trackedDownload.RemoteAlbum.Artist;
}
//if (series == null)
//{
// var localEpisode = new LocalEpisode();
// localEpisode.Path = file;
// localEpisode.Quality = QualityParser.ParseQuality(file);
// localEpisode.Size = _diskProvider.GetFileSize(file);
if (artist == null)
{
var localTrack = new LocalTrack();
localTrack.Path = file;
localTrack.Quality = QualityParser.ParseQuality(file);
localTrack.Size = _diskProvider.GetFileSize(file);
// return MapItem(new ImportDecision(localEpisode, new Rejection("Unknown Series")), folder, downloadId);
//}
return MapItem(new ImportDecision(localTrack, new Rejection("Unknown Artist")), folder, downloadId);
}
//var importDecisions = _importDecisionMaker.GetImportDecisions(new List<string> {file},
// series, null, SceneSource(series, folder));
var importDecisions = _importDecisionMaker.GetImportDecisions(new List<string> { file },
artist, null);
//return importDecisions.Any() ? MapItem(importDecisions.First(), folder, downloadId) : null;
return importDecisions.Any() ? MapItem(importDecisions.First(), folder, downloadId) : null;
}
private bool SceneSource(Series series, string folder)
private bool SceneSource(Artist artist, string folder)
{
return !(series.Path.PathEquals(folder) || series.Path.IsParentPath(folder));
return !(artist.Path.PathEquals(folder) || artist.Path.IsParentPath(folder));
}
private ManualImportItem MapItem(ImportDecision decision, string folder, string downloadId)
{
throw new System.NotImplementedException("TODO: This will be rewritten for Music");
//var item = new ManualImportItem();
var item = new ManualImportItem();
//item.Path = decision.LocalEpisode.Path;
//item.RelativePath = folder.GetRelativePath(decision.LocalEpisode.Path);
//item.Name = Path.GetFileNameWithoutExtension(decision.LocalEpisode.Path);
//item.DownloadId = downloadId;
item.Path = decision.LocalTrack.Path;
item.RelativePath = folder.GetRelativePath(decision.LocalTrack.Path);
item.Name = Path.GetFileNameWithoutExtension(decision.LocalTrack.Path);
item.DownloadId = downloadId;
//if (decision.LocalEpisode.Series != null)
//{
// item.Series = decision.LocalEpisode.Series;
//}
if (decision.LocalTrack.Artist != null)
{
item.Artist = decision.LocalTrack.Artist;
}
//if (decision.LocalEpisode.Episodes.Any())
//{
// item.SeasonNumber = decision.LocalEpisode.SeasonNumber;
// item.Episodes = decision.LocalEpisode.Episodes;
//}
if (decision.LocalTrack.Tracks.Any())
{
item.Tracks = decision.LocalTrack.Tracks;
}
//item.Quality = decision.LocalEpisode.Quality;
//item.Size = _diskProvider.GetFileSize(decision.LocalEpisode.Path);
//item.Rejections = decision.Rejections;
item.Quality = decision.LocalTrack.Quality;
item.Size = _diskProvider.GetFileSize(decision.LocalTrack.Path);
item.Rejections = decision.Rejections;
//return item;
return item;
}
public void Execute(ManualImportCommand message)
{
_logger.ProgressTrace("Manually importing {0} files using mode {1}", message.Files.Count, message.ImportMode);
throw new System.NotImplementedException("TODO: This will be rewritten for Music");
//var imported = new List<ImportResult>();
//var importedTrackedDownload = new List<ManuallyImportedFile>();
var imported = new List<ImportResult>();
var importedTrackedDownload = new List<ManuallyImportedFile>();
//for (int i = 0; i < message.Files.Count; i++)
//{
// _logger.ProgressTrace("Processing file {0} of {1}", i + 1, message.Files.Count);
for (int i = 0; i < message.Files.Count; i++)
{
_logger.ProgressTrace("Processing file {0} of {1}", i + 1, message.Files.Count);
// var file = message.Files[i];
// var series = _seriesService.GetSeries(file.SeriesId);
// var episodes = _episodeService.GetEpisodes(file.EpisodeIds);
// var parsedEpisodeInfo = Parser.Parser.ParsePath(file.Path) ?? new ParsedEpisodeInfo();
// var mediaInfo = _videoFileInfoReader.GetMediaInfo(file.Path);
// var existingFile = series.Path.IsParentPath(file.Path);
var file = message.Files[i];
var artist = _artistService.GetArtist(file.ArtistId);
var album = _albumService.GetAlbum(file.AlbumId);
var tracks = _trackService.GetTracks(file.TrackIds);
var parsedTrackInfo = Parser.Parser.ParseMusicPath(file.Path) ?? new ParsedTrackInfo();
var mediaInfo = _videoFileInfoReader.GetMediaInfo(file.Path);
var existingFile = artist.Path.IsParentPath(file.Path);
// var localEpisode = new LocalEpisode
// {
// ExistingFile = false,
// Episodes = episodes,
// MediaInfo = mediaInfo,
// ParsedEpisodeInfo = parsedEpisodeInfo,
// Path = file.Path,
// Quality = file.Quality,
// Series = series,
// Size = 0
// };
var localTrack = new LocalTrack
{
ExistingFile = false,
Tracks = tracks,
MediaInfo = mediaInfo,
ParsedTrackInfo = parsedTrackInfo,
Path = file.Path,
Quality = file.Quality,
Artist = artist,
Album = album,
Size = 0
};
// //TODO: Cleanup non-tracked downloads
//TODO: Cleanup non-tracked downloads
// var importDecision = new ImportDecision(localEpisode);
var importDecision = new ImportDecision(localTrack);
// if (file.DownloadId.IsNullOrWhiteSpace())
// {
// imported.AddRange(_importApprovedEpisodes.Import(new List<ImportDecision> { importDecision }, !existingFile, null, message.ImportMode));
// }
if (file.DownloadId.IsNullOrWhiteSpace())
{
imported.AddRange(_importApprovedTracks.Import(new List<ImportDecision> { importDecision }, !existingFile, null, message.ImportMode));
}
// else
// {
// var trackedDownload = _trackedDownloadService.Find(file.DownloadId);
// var importResult = _importApprovedEpisodes.Import(new List<ImportDecision> { importDecision }, true, trackedDownload.DownloadItem, message.ImportMode).First();
else
{
var trackedDownload = _trackedDownloadService.Find(file.DownloadId);
var importResult = _importApprovedTracks.Import(new List<ImportDecision> { importDecision }, true, trackedDownload.DownloadItem, message.ImportMode).First();
// imported.Add(importResult);
imported.Add(importResult);
// importedTrackedDownload.Add(new ManuallyImportedFile
// {
// TrackedDownload = trackedDownload,
// ImportResult = importResult
// });
// }
//}
importedTrackedDownload.Add(new ManuallyImportedFile
{
TrackedDownload = trackedDownload,
ImportResult = importResult
});
}
}
//_logger.ProgressTrace("Manually imported {0} files", imported.Count);
_logger.ProgressTrace("Manually imported {0} files", imported.Count);
//foreach (var groupedTrackedDownload in importedTrackedDownload.GroupBy(i => i.TrackedDownload.DownloadItem.DownloadId).ToList())
//{
// var trackedDownload = groupedTrackedDownload.First().TrackedDownload;
foreach (var groupedTrackedDownload in importedTrackedDownload.GroupBy(i => i.TrackedDownload.DownloadItem.DownloadId).ToList())
{
var trackedDownload = groupedTrackedDownload.First().TrackedDownload;
// if (_diskProvider.FolderExists(trackedDownload.DownloadItem.OutputPath.FullPath))
// {
// if (_downloadedEpisodesImportService.ShouldDeleteFolder(
// new DirectoryInfo(trackedDownload.DownloadItem.OutputPath.FullPath),
// trackedDownload.RemoteEpisode.Series) && !trackedDownload.DownloadItem.IsReadOnly)
// {
// _diskProvider.DeleteFolder(trackedDownload.DownloadItem.OutputPath.FullPath, true);
// }
// }
if (_diskProvider.FolderExists(trackedDownload.DownloadItem.OutputPath.FullPath))
{
if (_downloadedEpisodesImportService.ShouldDeleteFolder(
new DirectoryInfo(trackedDownload.DownloadItem.OutputPath.FullPath),
trackedDownload.RemoteEpisode.Series) && !trackedDownload.DownloadItem.IsReadOnly)
{
_diskProvider.DeleteFolder(trackedDownload.DownloadItem.OutputPath.FullPath, true);
}
}
// if (groupedTrackedDownload.Select(c => c.ImportResult).Count(c => c.Result == ImportResultType.Imported) >= Math.Max(1, trackedDownload.RemoteEpisode.Episodes.Count))
// {
// trackedDownload.State = TrackedDownloadStage.Imported;
// _eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload));
// }
//}
if (groupedTrackedDownload.Select(c => c.ImportResult).Count(c => c.Result == ImportResultType.Imported) >= Math.Max(1, trackedDownload.RemoteAlbum.Albums.Count))
{
trackedDownload.State = TrackedDownloadStage.Imported;
_eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload));
}
}
}
}
}

View File

@ -15,6 +15,9 @@ namespace NzbDrone.Core.Parser.Model
public string ArtistTitle { get; set; }
public string AlbumTitle { get; set; }
public ArtistTitleInfo ArtistTitleInfo { get; set; }
public string ArtistMBId { get; set; }
public string AlbumMBId { get; set; }
public string TrackMBId { get; set; }
public QualityModel Quality { get; set; }
public int[] TrackNumbers { get; set; }
public Language Language { get; set; }
@ -26,21 +29,18 @@ namespace NzbDrone.Core.Parser.Model
TrackNumbers = new int[0];
}
public override string ToString()
{
string episodeString = "[Unknown Track]";
string trackString = "[Unknown Track]";
if (TrackNumbers != null && TrackNumbers.Any())
{
episodeString = string.Format("T{0}", string.Join("-", TrackNumbers.Select(c => c.ToString("00"))));
trackString = string.Format("T{0}", string.Join("-", TrackNumbers.Select(c => c.ToString("00"))));
}
return string.Format("{0} - {1} {2}", ArtistTitle, episodeString, Quality);
return string.Format("{0} - {1} {2}", ArtistTitle, trackString, Quality);
}
}
}

View File

@ -336,9 +336,16 @@ namespace NzbDrone.Core.Parser
var trackName = file.Tag.Title;
var trackNumber = file.Tag.Track;
var artist = file.Tag.FirstAlbumArtist;
if (artist.IsNotNullOrWhiteSpace())
{
artist = file.Tag.FirstPerformer;
}
var artistTitleInfo = new ArtistTitleInfo
{
Title = file.Tag.Title,
Title = artist,
Year = (int)file.Tag.Year
};
@ -348,7 +355,10 @@ namespace NzbDrone.Core.Parser
{
Language = Language.English, //TODO Parse from Tag/Mediainfo
AlbumTitle = file.Tag.Album,
ArtistTitle = file.Tag.FirstAlbumArtist,
ArtistTitle = artist,
ArtistMBId = file.Tag.MusicBrainzArtistId,
AlbumMBId = file.Tag.MusicBrainzReleaseId,
TrackMBId = file.Tag.MusicBrainzReleaseType,
Quality = QualityParser.ParseQuality(trackName),
TrackNumbers = temp,
ArtistTitleInfo = artistTitleInfo,

View File

@ -18,6 +18,8 @@ namespace NzbDrone.Core.Parser
LocalEpisode GetLocalEpisode(string filename, Series series);
LocalEpisode GetLocalEpisode(string filename, Series series, ParsedEpisodeInfo folderInfo, bool sceneSource);
Series GetSeries(string title);
Artist GetArtist(string title);
Artist GetArtistFromTag(string file);
RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int tvdbId, int tvRageId, SearchCriteriaBase searchCriteria = null);
RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int seriesId, IEnumerable<int> episodeIds);
RemoteAlbum Map(ParsedAlbumInfo parsedAlbumInfo, SearchCriteriaBase searchCriteria = null);
@ -137,6 +139,44 @@ namespace NzbDrone.Core.Parser
return series;
}
public Artist GetArtist(string title)
{
var parsedAlbumInfo = Parser.ParseAlbumTitle(title);
if (parsedAlbumInfo == null || parsedAlbumInfo.ArtistName.IsNullOrWhiteSpace())
{
return _artistService.FindByName(title);
}
return _artistService.FindByName(parsedAlbumInfo.ArtistName);
}
public Artist GetArtistFromTag(string file)
{
var parsedTrackInfo = Parser.ParseMusicPath(file);
var artist = new Artist();
if (parsedTrackInfo.ArtistMBId.IsNotNullOrWhiteSpace())
{
artist = _artistService.FindById(parsedTrackInfo.ArtistMBId);
if (artist != null)
{
return artist;
}
}
if (parsedTrackInfo == null || parsedTrackInfo.ArtistTitle.IsNullOrWhiteSpace())
{
return null;
}
return _artistService.FindByName(parsedTrackInfo.ArtistTitle);
}
[System.Obsolete("Used for sonarr, not lidarr")]
public RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int tvdbId, int tvRageId, SearchCriteriaBase searchCriteria = null)
{
@ -670,10 +710,12 @@ namespace NzbDrone.Core.Parser
}
var tracks = GetTracks(parsedTrackInfo, artist);
var album = _albumService.FindByTitle(artist.Id, parsedTrackInfo.AlbumTitle);
return new LocalTrack
{
Artist = artist,
Album = album,
Quality = parsedTrackInfo.Quality,
Language = parsedTrackInfo.Language,
Tracks = tracks,