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 &&
+
+ }
+
+
+
+
+
+
+ );
+}
+
+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