From 21428cba6f378ba8ef6f13a7cb6ced300bfd0ae9 Mon Sep 17 00:00:00 2001 From: Qstick Date: Wed, 15 Nov 2017 21:24:33 -0500 Subject: [PATCH] Medium Support (Multi-disc Albums), Quality Grouping (#121) * Multi Disc Stage 1 - Backend Work * Quality Group Functionality * Fixed: Only show wanted album types on ArtistDetail page * Add Media Count Column to ArtistDetail Page * Parser updates for multidisc cases, other usenet release title formats * Search for Tracks by Medium Number in Addition to Title and TrackNumber * Medium Renaming Token for Track Naming * fixup Codacy and Comment Cleanup * fixup remove comments --- .../src/Activity/History/HistoryConnector.js | 6 +- frontend/src/Activity/Queue/QueueConnector.js | 6 +- .../AddNewArtist/AddNewArtistSearchResult.js | 6 +- .../EpisodeDetailsModalContentConnector.js | 88 ++--- frontend/src/Album/EpisodeLanguage.js | 8 +- frontend/src/Album/EpisodeQuality.js | 3 + frontend/src/Album/History/AlbumHistoryRow.js | 15 +- .../Album/Summary/EpisodeSummaryConnector.js | 4 +- frontend/src/Album/Summary/TrackDetailRow.js | 21 +- frontend/src/Artist/Details/AlbumRow.js | 12 + frontend/src/Artist/Details/ArtistDetails.css | 13 + frontend/src/Artist/Details/ArtistDetails.js | 55 +-- .../Index/Overview/ArtistIndexOverview.css | 4 + .../Index/Overview/ArtistIndexOverview.js | 13 +- frontend/src/Calendar/CalendarConnector.js | 4 +- .../Form/EnhancedSelectInputOption.css | 4 + .../Form/EnhancedSelectInputOption.js | 6 +- frontend/src/Components/Form/FormGroup.css | 4 + frontend/src/Components/Form/FormGroup.js | 2 +- frontend/src/Components/Form/FormLabel.css | 10 +- frontend/src/Components/Form/FormLabel.js | 7 +- .../Components/Form/RootFolderSelectInput.js | 7 +- .../Form/RootFolderSelectInputConnector.js | 15 +- .../src/Components/Loading/LoadingMessage.js | 4 +- frontend/src/Components/Modal/Modal.css | 17 +- frontend/src/Components/Modal/Modal.js | 3 + frontend/src/Components/Modal/ModalBody.css | 2 - .../src/Components/Page/Header/PageHeader.js | 8 +- .../Page/Header/PageHeaderActionsMenu.js | 12 + frontend/src/Components/SignalRConnector.js | 27 +- .../TableOptionsColumnDragPreview.js | 4 +- frontend/src/Components/Tooltip/Popover.js | 52 ++- frontend/src/Components/Tooltip/Tooltip.js | 13 +- frontend/src/Helpers/Props/icons.js | 3 + frontend/src/Helpers/Props/sizes.js | 4 +- .../Interactive/InteractiveImportRow.css | 9 +- .../Interactive/InteractiveImportRow.js | 2 + .../Quality/SelectQualityModalContent.js | 6 +- .../SelectQualityModalContentConnector.js | 3 +- .../MediaManagement/Naming/NamingModal.js | 51 +++ .../LanguageProfileItemDragPreview.js | 4 +- .../Quality/EditQualityProfileModal.js | 60 +++- .../EditQualityProfileModalContent.css | 15 + .../Quality/EditQualityProfileModalContent.js | 327 ++++++++++++------ ...EditQualityProfileModalContentConnector.js | 313 +++++++++++++++-- .../Profiles/Quality/QualityProfile.css | 7 + .../Profiles/Quality/QualityProfile.js | 59 +++- .../Profiles/Quality/QualityProfileItem.css | 49 ++- .../Profiles/Quality/QualityProfileItem.js | 66 +++- .../Quality/QualityProfileItemDragPreview.js | 18 +- .../Quality/QualityProfileItemDragSource.css | 4 +- .../Quality/QualityProfileItemDragSource.js | 156 +++++++-- .../Quality/QualityProfileItemGroup.css | 105 ++++++ .../Quality/QualityProfileItemGroup.js | 200 +++++++++++ .../Profiles/Quality/QualityProfileItems.css | 13 +- .../Profiles/Quality/QualityProfileItems.js | 144 ++++++-- .../Profiles/Quality/QualityProfiles.js | 1 - .../Quality/QualityProfilesConnector.js | 4 +- .../Actions/Creators/createFetchHandler.js | 12 +- .../Creators/createSaveProviderHandler.js | 4 +- .../Creators/createTestProviderHandler.js | 4 +- frontend/src/Store/Actions/actionTypes.js | 1 + .../Store/Actions/addArtistActionHandlers.js | 2 +- .../Store/Actions/releaseActionHandlers.js | 19 +- frontend/src/Store/Actions/releaseActions.js | 1 + .../src/Store/Reducers/episodeReducers.js | 5 + frontend/src/Store/Reducers/trackReducers.js | 11 +- frontend/src/Styles/Variables/dimensions.js | 10 +- .../TrackFileEditorModalContentConnector.js | 17 +- .../TrackFile/Editor/TrackFileEditorRow.js | 2 +- .../src/Utilities/Quality/getQualities.js | 16 + frontend/src/Utilities/createAjaxRequest.js | 44 ++- package.json | 1 - src/Lidarr.Api.V1/Albums/AlbumResource.cs | 18 +- src/Lidarr.Api.V1/Albums/MediumResource.cs | 56 +++ src/Lidarr.Api.V1/History/HistoryModule.cs | 28 ++ src/Lidarr.Api.V1/Lidarr.Api.V1.csproj | 4 +- .../Quality/QualityCutoffValidator.cs | 45 +++ .../Profiles/Quality/QualityItemsValidator.cs | 197 +++++++++++ .../Profiles/Quality/QualityProfileModule.cs | 10 +- .../Quality/QualityProfileResource.cs | 25 +- .../Quality/QualityProfileSchemaModule.cs | 44 ++- .../Quality/QualityProfileValidation.cs | 43 --- src/Lidarr.Api.V1/Tracks/TrackResource.cs | 13 +- .../Datastore/MarrDataLazyLoadingFixture.cs | 2 +- .../CutoffSpecificationFixture.cs | 20 +- .../HistorySpecificationFixture.cs | 8 +- ...ityAllowedByProfileSpecificationFixture.cs | 2 +- .../QualityUpgradeSpecificationFixture.cs | 2 +- .../QueueSpecificationFixture.cs | 8 +- .../RssSync/DelaySpecificationFixture.cs | 2 +- .../DeletedTrackFileSpecificationFixture.cs | 2 +- .../RssSync/ProperSpecificationFixture.cs | 2 +- .../UpgradeDiskSpecificationFixture.cs | 2 +- .../PendingReleaseServiceTests/AddFixture.cs | 2 +- .../RemoveGrabbedFixture.cs | 2 +- .../RemoveRejectedFixture.cs | 2 +- .../HistoryTests/HistoryServiceFixture.cs | 4 +- .../ArtistRepositoryFixture.cs | 53 +++ .../NzbDrone.Core.Test.csproj | 3 + .../FileNameBuilderTests/CleanTitleFixture.cs | 4 +- .../FileNameBuilderFixture.cs | 10 +- .../FileNameBuilderTests/TitleTheFixture.cs | 2 +- .../ParserTests/ParserFixture.cs | 97 +++--- .../Delay/DelayProfileServiceFixture.cs | 112 ++++++ .../Profiles/ProfileRepositoryFixture.cs | 2 +- .../Qualities/QualityIndexCompareToFixture.cs | 36 ++ .../Qualities/QualityModelComparerFixture.cs | 67 ++++ .../Migration/003_add_medium_support.cs | 22 ++ src/NzbDrone.Core/Datastore/TableMapping.cs | 1 + .../DownloadDecisionComparer.cs | 2 +- .../DecisionEngine/DownloadDecisionMaker.cs | 23 +- .../Specifications/CutoffSpecification.cs | 9 +- .../QualityAllowedByProfileSpecification.cs | 8 +- .../RssSync/DelaySpecification.cs | 4 +- .../DecisionEngine/UpgradableSpecification.cs | 9 +- .../Metadata/Consumers/Wdtv/WdtvMetadata.cs | 2 +- .../History/HistoryRepository.cs | 17 + src/NzbDrone.Core/History/HistoryService.cs | 6 + .../MediaFiles/RenameTrackFileService.cs | 2 +- .../TrackImport/ImportApprovedTracks.cs | 3 +- .../Specifications/UpgradeSpecification.cs | 3 - .../MediaFiles/UpgradeMediaFileService.cs | 14 +- .../SkyHook/Resource/AlbumResource.cs | 7 +- .../SkyHook/Resource/MediumResource.cs | 16 + .../SkyHook/Resource/TrackResource.cs | 6 +- .../MetadataSource/SkyHook/SkyHookProxy.cs | 18 +- src/NzbDrone.Core/Music/AddArtistService.cs | 3 +- src/NzbDrone.Core/Music/Album.cs | 6 +- src/NzbDrone.Core/Music/AlbumCutoffService.cs | 6 +- src/NzbDrone.Core/Music/AlbumService.cs | 2 +- src/NzbDrone.Core/Music/Medium.cs | 12 + .../Music/RefreshAlbumService.cs | 1 + .../Music/RefreshTrackService.cs | 4 +- src/NzbDrone.Core/Music/Track.cs | 6 +- src/NzbDrone.Core/Music/TrackRepository.cs | 15 +- src/NzbDrone.Core/Music/TrackService.cs | 12 +- .../Notifications/Webhook/WebhookTrack.cs | 2 +- src/NzbDrone.Core/NzbDrone.Core.csproj | 4 + .../Organizer/FileNameBuilder.cs | 27 +- .../Organizer/FileNameSampleService.cs | 14 +- .../Parser/Model/ParsedAlbumInfo.cs | 1 + .../Parser/Model/ParsedTrackInfo.cs | 1 + src/NzbDrone.Core/Parser/Parser.cs | 28 +- src/NzbDrone.Core/Parser/ParsingService.cs | 5 +- src/NzbDrone.Core/Profiles/Quality/Profile.cs | 51 ++- .../Profiles/Quality/ProfileQualityItem.cs | 36 ++ .../Profiles/Quality/ProfileService.cs | 4 +- .../Profiles/Quality/QualityIndex.cs | 55 +++ src/NzbDrone.Core/Qualities/Quality.cs | 3 +- .../Qualities/QualityDefinition.cs | 5 +- .../Qualities/QualityModelComparer.cs | 26 +- .../IntegrationTestBase.cs | 8 +- yarn.lock | 4 - 154 files changed, 2957 insertions(+), 712 deletions(-) create mode 100644 frontend/src/Settings/Profiles/Quality/QualityProfileItemGroup.css create mode 100644 frontend/src/Settings/Profiles/Quality/QualityProfileItemGroup.js create mode 100644 frontend/src/Utilities/Quality/getQualities.js create mode 100644 src/Lidarr.Api.V1/Albums/MediumResource.cs create mode 100644 src/Lidarr.Api.V1/Profiles/Quality/QualityCutoffValidator.cs create mode 100644 src/Lidarr.Api.V1/Profiles/Quality/QualityItemsValidator.cs delete mode 100644 src/Lidarr.Api.V1/Profiles/Quality/QualityProfileValidation.cs create mode 100644 src/NzbDrone.Core.Test/MusicTests/ArtistRepositoryTests/ArtistRepositoryFixture.cs create mode 100644 src/NzbDrone.Core.Test/Profiles/Delay/DelayProfileServiceFixture.cs create mode 100644 src/NzbDrone.Core.Test/Profiles/Qualities/QualityIndexCompareToFixture.cs create mode 100644 src/NzbDrone.Core/Datastore/Migration/003_add_medium_support.cs create mode 100644 src/NzbDrone.Core/MetadataSource/SkyHook/Resource/MediumResource.cs create mode 100644 src/NzbDrone.Core/Music/Medium.cs create mode 100644 src/NzbDrone.Core/Profiles/Quality/QualityIndex.cs diff --git a/frontend/src/Activity/History/HistoryConnector.js b/frontend/src/Activity/History/HistoryConnector.js index c3eb9d8b1..c43c1aa43 100644 --- a/frontend/src/Activity/History/HistoryConnector.js +++ b/frontend/src/Activity/History/HistoryConnector.js @@ -43,7 +43,11 @@ class HistoryConnector extends Component { componentDidUpdate(prevProps) { if (hasDifferentItems(prevProps.items, this.props.items)) { const albumIds = selectUniqueIds(this.props.items, 'albumId'); - this.props.fetchEpisodes({ albumIds }); + if (albumIds.length) { + this.props.fetchEpisodes({ albumIds }); + } else { + this.props.clearEpisodes(); + } } } diff --git a/frontend/src/Activity/Queue/QueueConnector.js b/frontend/src/Activity/Queue/QueueConnector.js index 2e5c36ced..966e46900 100644 --- a/frontend/src/Activity/Queue/QueueConnector.js +++ b/frontend/src/Activity/Queue/QueueConnector.js @@ -52,7 +52,11 @@ class QueueConnector extends Component { componentDidUpdate(prevProps) { if (hasDifferentItems(prevProps.items, this.props.items)) { const albumIds = selectUniqueIds(this.props.items, 'albumId'); - this.props.fetchEpisodes({ albumIds }); + if (albumIds.length) { + this.props.fetchEpisodes({ albumIds }); + } else { + this.props.clearEpisodes(); + } } } diff --git a/frontend/src/AddArtist/AddNewArtist/AddNewArtistSearchResult.js b/frontend/src/AddArtist/AddNewArtist/AddNewArtistSearchResult.js index 5cae6fbb4..e1fe3a2b9 100644 --- a/frontend/src/AddArtist/AddNewArtist/AddNewArtistSearchResult.js +++ b/frontend/src/AddArtist/AddNewArtist/AddNewArtistSearchResult.js @@ -44,7 +44,7 @@ class AddNewArtistSearchResult extends Component { componentDidUpdate(prevProps) { if (!prevProps.isExistingArtist && this.props.isExistingArtist) { - this.onAddSerisModalClose(); + this.onAddArtistModalClose(); } } @@ -55,7 +55,7 @@ class AddNewArtistSearchResult extends Component { this.setState({ isNewAddArtistModalOpen: true }); } - onAddSerisModalClose = () => { + onAddArtistModalClose = () => { this.setState({ isNewAddArtistModalOpen: false }); } @@ -183,7 +183,7 @@ class AddNewArtistSearchResult extends Component { year={year} overview={overview} images={images} - onModalClose={this.onAddSerisModalClose} + onModalClose={this.onAddArtistModalClose} /> ); diff --git a/frontend/src/Album/EpisodeDetailsModalContentConnector.js b/frontend/src/Album/EpisodeDetailsModalContentConnector.js index 0e84426f5..42790757e 100644 --- a/frontend/src/Album/EpisodeDetailsModalContentConnector.js +++ b/frontend/src/Album/EpisodeDetailsModalContentConnector.js @@ -2,13 +2,12 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; -import { clearReleases } from 'Store/Actions/releaseActions'; +import { cancelFetchReleases, clearReleases } from 'Store/Actions/releaseActions'; import { toggleEpisodeMonitored } from 'Store/Actions/episodeActions'; import createEpisodeSelector from 'Store/Selectors/createEpisodeSelector'; import createArtistSelector from 'Store/Selectors/createArtistSelector'; import episodeEntities from 'Album/episodeEntities'; import { fetchTracks, clearTracks } from 'Store/Actions/trackActions'; -import { fetchTrackFiles, clearTrackFiles } from 'Store/Actions/trackFileActions'; import EpisodeDetailsModalContent from './EpisodeDetailsModalContent'; function createMapStateToProps() { @@ -32,14 +31,38 @@ function createMapStateToProps() { ); } -const mapDispatchToProps = { - clearReleases, - fetchTracks, - clearTracks, - fetchTrackFiles, - clearTrackFiles, - toggleEpisodeMonitored -}; +function createMapDispatchToProps(dispatch, props) { + return { + dispatchCancelFetchReleases() { + dispatch(cancelFetchReleases()); + }, + + dispatchClearReleases() { + dispatch(clearReleases()); + }, + + dispatchFetchTracks({ artistId, albumId }) { + dispatch(fetchTracks({ artistId, albumId })); + }, + + dispatchClearTracks() { + dispatch(clearTracks()); + }, + + onMonitorAlbumPress(monitored) { + const { + albumId, + episodeEntity + } = this.props; + + dispatch(toggleEpisodeMonitored({ + episodeEntity, + albumId, + monitored + })); + } + }; +} class EpisodeDetailsModalContentConnector extends Component { @@ -53,7 +76,8 @@ class EpisodeDetailsModalContentConnector extends Component { // Clear pending releases here so we can reshow the search // results even after switching tabs. this._unpopulate(); - this.props.clearReleases(); + this.props.dispatchCancelFetchReleases(); + this.props.dispatchClearReleases(); } // @@ -62,40 +86,24 @@ class EpisodeDetailsModalContentConnector extends Component { _populate() { const artistId = this.props.artistId; const albumId = this.props.albumId; - this.props.fetchTracks({ artistId, albumId }); - // this.props.fetchTrackFiles({ artistId, albumId }); + this.props.dispatchFetchTracks({ artistId, albumId }); } _unpopulate() { - this.props.clearTracks(); - // this.props.clearTrackFiles(); + this.props.dispatchClearTracks(); } // - // Listeners + // Render - onMonitorAlbumPress = (monitored) => { + render() { const { - albumId, - episodeEntity + dispatchClearReleases, + ...otherProps } = this.props; - this.props.toggleEpisodeMonitored({ - episodeEntity, - albumId, - monitored - }); - } - - // - // Render - - render() { return ( - + ); } } @@ -104,16 +112,14 @@ EpisodeDetailsModalContentConnector.propTypes = { albumId: PropTypes.number.isRequired, episodeEntity: PropTypes.string.isRequired, artistId: PropTypes.number.isRequired, - fetchTracks: PropTypes.func.isRequired, - clearTracks: PropTypes.func.isRequired, - fetchTrackFiles: PropTypes.func.isRequired, - clearTrackFiles: PropTypes.func.isRequired, - clearReleases: PropTypes.func.isRequired, - toggleEpisodeMonitored: PropTypes.func.isRequired + dispatchFetchTracks: PropTypes.func.isRequired, + dispatchClearTracks: PropTypes.func.isRequired, + dispatchCancelFetchReleases: PropTypes.func.isRequired, + dispatchClearReleases: PropTypes.func.isRequired }; EpisodeDetailsModalContentConnector.defaultProps = { episodeEntity: episodeEntities.EPISODES }; -export default connect(createMapStateToProps, mapDispatchToProps)(EpisodeDetailsModalContentConnector); +export default connect(createMapStateToProps, createMapDispatchToProps)(EpisodeDetailsModalContentConnector); diff --git a/frontend/src/Album/EpisodeLanguage.js b/frontend/src/Album/EpisodeLanguage.js index fc784ef51..7a5a24963 100644 --- a/frontend/src/Album/EpisodeLanguage.js +++ b/frontend/src/Album/EpisodeLanguage.js @@ -3,20 +3,24 @@ import React from 'react'; import Label from 'Components/Label'; function EpisodeLanguage(props) { - const language = props.language; + const { + className, + language + } = props; if (!language) { return null; } return ( -