diff --git a/frontend/src/Artist/ArtistBanner.js b/frontend/src/Artist/ArtistBanner.js index 3edd1b0bc..d6c378b5f 100644 --- a/frontend/src/Artist/ArtistBanner.js +++ b/frontend/src/Artist/ArtistBanner.js @@ -40,8 +40,8 @@ class ArtistBanner extends Component { pixelRatio, banner, bannerUrl: getBannerUrl(banner, pixelRatio * size), - hasError: false, - isLoaded: false + isLoaded: false, + hasError: false }; } @@ -52,17 +52,18 @@ class ArtistBanner extends Component { } = this.props; const { + banner, pixelRatio } = this.state; - const banner = findBanner(images); + const nextBanner = findBanner(images); - if (banner && banner.url !== this.state.banner.url) { + if (nextBanner && (!banner || nextBanner.url !== banner.url)) { this.setState({ - banner, - bannerUrl: getBannerUrl(banner, pixelRatio * size), - hasError: false, - isLoaded: false + banner: nextBanner, + posterUrl: getBannerUrl(nextBanner, pixelRatio * size), + isLoaded: false, + hasError: false }); } } @@ -75,7 +76,10 @@ class ArtistBanner extends Component { } onLoad = () => { - this.setState({ isLoaded: true }); + this.setState({ + isLoaded: true, + hasError: false + }); } // diff --git a/frontend/src/Artist/ArtistPoster.js b/frontend/src/Artist/ArtistPoster.js index 964e4f684..18cde6173 100644 --- a/frontend/src/Artist/ArtistPoster.js +++ b/frontend/src/Artist/ArtistPoster.js @@ -40,8 +40,8 @@ class ArtistPoster extends Component { pixelRatio, poster, posterUrl: getPosterUrl(poster, pixelRatio * size), - hasError: false, - isLoaded: false + isLoaded: false, + hasError: false }; } @@ -52,17 +52,18 @@ class ArtistPoster extends Component { } = this.props; const { + poster, pixelRatio } = this.state; - const poster = findPoster(images); + const nextPoster = findPoster(images); - if (poster && poster.url !== this.state.poster.url) { + if (nextPoster && (!poster || nextPoster.url !== poster.url)) { this.setState({ - poster, - posterUrl: getPosterUrl(poster, pixelRatio * size), - hasError: false, - isLoaded: false + poster: nextPoster, + posterUrl: getPosterUrl(nextPoster, pixelRatio * size), + isLoaded: false, + hasError: false }); } } @@ -75,7 +76,10 @@ class ArtistPoster extends Component { } onLoad = () => { - this.setState({ isLoaded: true }); + this.setState({ + isLoaded: true, + hasError: false + }); } // diff --git a/frontend/src/Artist/Details/ArtistDetails.js b/frontend/src/Artist/Details/ArtistDetails.js index 2fe9d732c..9dd246d51 100644 --- a/frontend/src/Artist/Details/ArtistDetails.js +++ b/frontend/src/Artist/Details/ArtistDetails.js @@ -570,4 +570,8 @@ ArtistDetails.propTypes = { onSearchPress: PropTypes.func.isRequired }; +ArtistDetails.defaultProps = { + isSaving: false +}; + export default ArtistDetails; diff --git a/frontend/src/Components/Page/Header/PageHeader.css b/frontend/src/Components/Page/Header/PageHeader.css index 1c8de74b6..3bfcbc10b 100644 --- a/frontend/src/Components/Page/Header/PageHeader.css +++ b/frontend/src/Components/Page/Header/PageHeader.css @@ -1,5 +1,5 @@ .header { - z-index: 2; + z-index: 3; display: flex; align-items: center; flex: 0 0 auto; diff --git a/frontend/src/Components/Page/Sidebar/PageSidebar.css b/frontend/src/Components/Page/Sidebar/PageSidebar.css index 293d4ae7f..fdbd80320 100644 --- a/frontend/src/Components/Page/Sidebar/PageSidebar.css +++ b/frontend/src/Components/Page/Sidebar/PageSidebar.css @@ -19,13 +19,13 @@ .sidebarContainer { position: fixed; top: 0; - z-index: 1; + z-index: 2; height: 100vh; } .sidebar { position: fixed; - z-index: 1; + z-index: 2; overflow-y: auto; width: 100%; height: 100%; diff --git a/frontend/src/Components/SignalRConnector.js b/frontend/src/Components/SignalRConnector.js index 06cece1da..8ff1c4ce8 100644 --- a/frontend/src/Components/SignalRConnector.js +++ b/frontend/src/Components/SignalRConnector.js @@ -173,7 +173,10 @@ class SignalRConnector extends Component { const resource = body.resource; const state = resource.state; - if (state === 'completed') { + // Both sucessful and failed commands need to be + // completed, otherwise they spin until they timeout. + + if (state === 'completed' || state === 'failed') { this.props.finishCommand(resource); } else { this.props.updateCommand(resource); diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js index b3c111d5f..346222373 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js @@ -49,6 +49,12 @@ const columns = [ isSortable: true, isVisible: true }, + { + name: 'language', + label: 'Language', + isSortable: true, + isVisible: true + }, { name: 'size', label: 'Size', diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js index 234ed7921..d0a702de0 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js @@ -74,7 +74,8 @@ class InteractiveImportModalContentConnector extends Component { artist, album, tracks, - quality + quality, + language } = item; if (!artist) { @@ -98,6 +99,7 @@ class InteractiveImportModalContentConnector extends Component { albumId: album.id, trackIds: _.map(tracks, 'id'), quality, + language, downloadId: this.props.downloadId }); } diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js index f20c7ebd7..f28fe503a 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js @@ -9,10 +9,12 @@ 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 EpisodeLanguage from 'Episode/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'; @@ -28,7 +30,8 @@ class InteractiveImportRow extends Component { isSelectArtistModalOpen: false, isSelectAlbumModalOpen: false, isSelectTrackModalOpen: false, - isSelectQualityModalOpen: false + isSelectQualityModalOpen: false, + isSelectLanguageModalOpen: false }; } @@ -38,10 +41,17 @@ class InteractiveImportRow extends Component { artist, album, tracks, - quality + quality, + language } = this.props; - if (artist && album !== undefined && tracks.length && quality) { + if ( + artist && + album != null && + tracks.length && + quality && + language + ) { this.props.onSelectedChange({ id, value: true }); } } @@ -53,6 +63,7 @@ class InteractiveImportRow extends Component { album, tracks, quality, + language, isSelected, onValidRowChange } = this.props; @@ -61,7 +72,13 @@ class InteractiveImportRow extends Component { return; } - const isValid = !!(artist && album != null && tracks.length && quality); + const isValid = !!( + artist && + album != null && + tracks.length && + quality && + language + ); if (isSelected && !isValid) { onValidRowChange(id, false); @@ -103,6 +120,10 @@ class InteractiveImportRow extends Component { this.setState({ isSelectQualityModalOpen: true }); } + onSelectLanguagePress = () => { + this.setState({ isSelectLanguageModalOpen: true }); + } + onSelectArtistModalClose = (changed) => { this.setState({ isSelectArtistModalOpen: false }); this.selectRowAfterChange(changed); @@ -123,6 +144,11 @@ class InteractiveImportRow extends Component { this.selectRowAfterChange(changed); } + onSelectLanguageModalClose = (changed) => { + this.setState({ isSelectLanguageModalOpen: false }); + this.selectRowAfterChange(changed); + } + // // Render @@ -134,6 +160,7 @@ class InteractiveImportRow extends Component { album, tracks, quality, + language, size, rejections, isSelected, @@ -144,7 +171,8 @@ class InteractiveImportRow extends Component { isSelectArtistModalOpen, isSelectAlbumModalOpen, isSelectTrackModalOpen, - isSelectQualityModalOpen + isSelectQualityModalOpen, + isSelectLanguageModalOpen } = this.state; const artistName = artist ? artist.artistName : ''; @@ -206,6 +234,15 @@ class InteractiveImportRow extends Component { /> + + + + {formatBytes(size)} @@ -268,6 +305,13 @@ class InteractiveImportRow extends Component { real={quality.revision.real > 0} onModalClose={this.onSelectQualityModalClose} /> + + ); } @@ -281,6 +325,7 @@ InteractiveImportRow.propTypes = { album: PropTypes.object, tracks: PropTypes.arrayOf(PropTypes.object).isRequired, quality: PropTypes.object, + language: PropTypes.object, size: PropTypes.number.isRequired, rejections: PropTypes.arrayOf(PropTypes.object).isRequired, isSelected: PropTypes.bool, diff --git a/frontend/src/InteractiveImport/Language/SelectLanguageModal.js b/frontend/src/InteractiveImport/Language/SelectLanguageModal.js new file mode 100644 index 000000000..938d26a6d --- /dev/null +++ b/frontend/src/InteractiveImport/Language/SelectLanguageModal.js @@ -0,0 +1,37 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Modal from 'Components/Modal/Modal'; +import SelectLanguageModalContentConnector from './SelectLanguageModalContentConnector'; + +class SelectLanguageModal extends Component { + + // + // Render + + render() { + const { + isOpen, + onModalClose, + ...otherProps + } = this.props; + + return ( + + + + ); + } +} + +SelectLanguageModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default SelectLanguageModal; diff --git a/frontend/src/InteractiveImport/Language/SelectLanguageModalContent.js b/frontend/src/InteractiveImport/Language/SelectLanguageModalContent.js new file mode 100644 index 000000000..ff99ce6bf --- /dev/null +++ b/frontend/src/InteractiveImport/Language/SelectLanguageModalContent.js @@ -0,0 +1,87 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { inputTypes } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; + +function SelectLanguageModalContent(props) { + const { + languageId, + isFetching, + isPopulated, + error, + items, + onModalClose, + onLanguageSelect + } = props; + + const languageOptions = items.map(({ language }) => { + return { + key: language.id, + value: language.name + }; + }); + + return ( + + + Manual Import - Select Language + + + + { + isFetching && + + } + + { + !isFetching && !!error && +
Unable to load languages
+ } + + { + isPopulated && !error && +
+ + Language + + + +
+ } +
+ + + + +
+ ); +} + +SelectLanguageModalContent.propTypes = { + languageId: PropTypes.number.isRequired, + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + onLanguageSelect: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default SelectLanguageModalContent; diff --git a/frontend/src/InteractiveImport/Language/SelectLanguageModalContentConnector.js b/frontend/src/InteractiveImport/Language/SelectLanguageModalContentConnector.js new file mode 100644 index 000000000..a3b7277e7 --- /dev/null +++ b/frontend/src/InteractiveImport/Language/SelectLanguageModalContentConnector.js @@ -0,0 +1,87 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchLanguageProfileSchema } from 'Store/Actions/settingsActions'; +import { updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions'; +import SelectLanguageModalContent from './SelectLanguageModalContent'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.languageProfiles, + (languageProfiles) => { + const { + isFetchingSchema: isFetching, + schemaPopulated: isPopulated, + schemaError: error, + schema + } = languageProfiles; + + return { + isFetching, + isPopulated, + error, + items: schema.languages || [] + }; + } + ); +} + +const mapDispatchToProps = { + fetchLanguageProfileSchema, + updateInteractiveImportItem +}; + +class SelectLanguageModalContentConnector extends Component { + + // + // Lifecycle + + componentDidMount = () => { + if (!this.props.isPopulated) { + this.props.fetchLanguageProfileSchema(); + } + } + + // + // Listeners + + onLanguageSelect = ({ value }) => { + const languageId = parseInt(value); + const language = _.find(this.props.items, + (item) => item.language.id === languageId).language; + + this.props.updateInteractiveImportItem({ + id: this.props.id, + language + }); + + this.props.onModalClose(true); + } + + // + // Render + + render() { + return ( + + ); + } +} + +SelectLanguageModalContentConnector.propTypes = { + id: PropTypes.number.isRequired, + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + fetchLanguageProfileSchema: PropTypes.func.isRequired, + updateInteractiveImportItem: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(SelectLanguageModalContentConnector); diff --git a/frontend/src/Store/Reducers/releaseReducers.js b/frontend/src/Store/Reducers/releaseReducers.js index 2f55929d6..237049578 100644 --- a/frontend/src/Store/Reducers/releaseReducers.js +++ b/frontend/src/Store/Reducers/releaseReducers.js @@ -49,6 +49,12 @@ const releaseReducers = handleActions({ const guid = payload.guid; const newState = Object.assign({}, state); const items = newState.items; + + // Return early if there aren't any items (the user closed the modal) + if (!items.length) { + return; + } + const index = _.findIndex(items, { guid }); const item = Object.assign({}, items[index], payload); diff --git a/frontend/src/System/Updates/Updates.css b/frontend/src/System/Updates/Updates.css index c7561fdd2..307028426 100644 --- a/frontend/src/System/Updates/Updates.css +++ b/frontend/src/System/Updates/Updates.css @@ -20,10 +20,10 @@ .info { display: flex; + align-items: center; margin-bottom: 10px; padding-bottom: 5px; border-bottom: 1px solid #e5e5e5; - line-height: 21px; } .version { diff --git a/frontend/src/System/Updates/Updates.js b/frontend/src/System/Updates/Updates.js index d88f44abd..08a12e6d1 100644 --- a/frontend/src/System/Updates/Updates.js +++ b/frontend/src/System/Updates/Updates.js @@ -139,7 +139,7 @@ class Updates extends Component { Updates.propTypes = { isPopulated: PropTypes.bool.isRequired, - error: PropTypes.object.isRequired, + error: PropTypes.object, items: PropTypes.array.isRequired, isInstallingUpdate: PropTypes.bool.isRequired, shortDateFormat: PropTypes.string.isRequired, diff --git a/src/Lidarr.Api.V3/ManualImport/ManualImportResource.cs b/src/Lidarr.Api.V3/ManualImport/ManualImportResource.cs index 75b63e606..40d77cadb 100644 --- a/src/Lidarr.Api.V3/ManualImport/ManualImportResource.cs +++ b/src/Lidarr.Api.V3/ManualImport/ManualImportResource.cs @@ -2,6 +2,7 @@ using NzbDrone.Common.Crypto; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.MediaFiles.TrackImport.Manual; using NzbDrone.Core.Qualities; +using NzbDrone.Core.Languages; using Lidarr.Api.V3.Artist; using Lidarr.Api.V3.Albums; using Lidarr.Api.V3.Tracks; @@ -21,6 +22,7 @@ namespace Lidarr.Api.V3.ManualImport public AlbumResource Album { get; set; } public List Tracks { get; set; } public QualityModel Quality { get; set; } + public Language Language { get; set; } public int QualityWeight { get; set; } public string DownloadId { get; set; } public IEnumerable Rejections { get; set; } @@ -43,6 +45,7 @@ namespace Lidarr.Api.V3.ManualImport Album = model.Album.ToResource(), Tracks = model.Tracks.ToResource(), Quality = model.Quality, + Language = model.Language, //QualityWeight DownloadId = model.DownloadId, Rejections = model.Rejections diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportFile.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportFile.cs index 2db3f8ec1..ed784afbd 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportFile.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportFile.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using NzbDrone.Core.Qualities; +using NzbDrone.Core.Languages; namespace NzbDrone.Core.MediaFiles.TrackImport.Manual { @@ -10,6 +11,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual public int AlbumId { get; set; } public List TrackIds { get; set; } public QualityModel Quality { get; set; } + public Language Language { get; set; } public string DownloadId { get; set; } } } diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportItem.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportItem.cs index 4e52ee645..2a5ed6b02 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportItem.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportItem.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Qualities; +using NzbDrone.Core.Languages; using NzbDrone.Core.Music; namespace NzbDrone.Core.MediaFiles.TrackImport.Manual @@ -15,6 +16,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual public Album Album { get; set; } public List Tracks { get; set; } public QualityModel Quality { get; set; } + public Language Language { get; set; } public string DownloadId { get; set; } public IEnumerable Rejections { get; set; } } diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs index 9c194abd2..070626974 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs @@ -147,6 +147,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual var localTrack = new LocalTrack(); localTrack.Path = file; localTrack.Quality = QualityParser.ParseQuality(file); + localEpisode.Language = LanguageParser.ParseLanguage(file); localTrack.Size = _diskProvider.GetFileSize(file); return MapItem(new ImportDecision(localTrack, new Rejection("Unknown Artist")), folder, downloadId); @@ -183,6 +184,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual } item.Quality = decision.LocalTrack.Quality; + item.Language = decision.LocalTrack.Language; item.Size = _diskProvider.GetFileSize(decision.LocalTrack.Path); item.Rejections = decision.Rejections; @@ -216,6 +218,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual ParsedTrackInfo = parsedTrackInfo, Path = file.Path, Quality = file.Quality, + Language = file.Language, Artist = artist, Album = album, Size = 0