diff --git a/frontend/src/Settings/Metadata/Metadata/Metadatas.js b/frontend/src/Settings/Metadata/Metadata/Metadatas.js index 03343ad82..9f015369d 100644 --- a/frontend/src/Settings/Metadata/Metadata/Metadatas.js +++ b/frontend/src/Settings/Metadata/Metadata/Metadatas.js @@ -14,7 +14,7 @@ function Metadatas(props) { return (
+ { + isFetching && + + } + + { + !isFetching && error && +
Unable to load Metadata Provider settings
+ } + + { + hasSettings && !isFetching && !error && +
+ { + advancedSettings && +
+ + Metadata Source + + + +
+ } +
+ } + + + ); +} + +MetadataProvider.propTypes = { + advancedSettings: PropTypes.bool.isRequired, + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + settings: PropTypes.object.isRequired, + hasSettings: PropTypes.bool.isRequired, + onInputChange: PropTypes.func.isRequired +}; + +export default MetadataProvider; diff --git a/frontend/src/Settings/Metadata/MetadataProvider/MetadataProviderConnector.js b/frontend/src/Settings/Metadata/MetadataProvider/MetadataProviderConnector.js new file mode 100644 index 000000000..153a929e9 --- /dev/null +++ b/frontend/src/Settings/Metadata/MetadataProvider/MetadataProviderConnector.js @@ -0,0 +1,92 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { createSelector } from 'reselect'; +import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector'; +import { setMetadataProviderValue, saveMetadataProvider, fetchMetadataProvider } from 'Store/Actions/settingsActions'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import connectSection from 'Store/connectSection'; +import MetadataProvider from './MetadataProvider'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.advancedSettings, + createSettingsSectionSelector(), + (advancedSettings, sectionSettings) => { + return { + advancedSettings, + ...sectionSettings + }; + } + ); +} + +const mapDispatchToProps = { + setMetadataProviderValue, + saveMetadataProvider, + fetchMetadataProvider, + clearPendingChanges +}; + +class MetadataProviderConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchMetadataProvider(); + } + + componentDidUpdate(prevProps) { + if (this.props.hasPendingChanges !== prevProps.hasPendingChanges) { + this.props.onHasPendingChange(this.props.hasPendingChanges); + } + } + + componentWillUnmount() { + this.props.clearPendingChanges({ section: this.props.section }); + } + + // + // Control + + save = () => { + this.props.saveMetadataProvider(); + } + + // + // Listeners + + onInputChange = ({ name, value }) => { + this.props.setMetadataProviderValue({ name, value }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +MetadataProviderConnector.propTypes = { + section: PropTypes.string.isRequired, + hasPendingChanges: PropTypes.bool.isRequired, + setMetadataProviderValue: PropTypes.func.isRequired, + saveMetadataProvider: PropTypes.func.isRequired, + fetchMetadataProvider: PropTypes.func.isRequired, + clearPendingChanges: PropTypes.func.isRequired, + onHasPendingChange: PropTypes.func.isRequired +}; + +export default connectSection( + createMapStateToProps, + mapDispatchToProps, + undefined, + { withRef: true }, + { section: 'metadataProvider' } + )(MetadataProviderConnector); diff --git a/frontend/src/Settings/Metadata/MetadataSettings.js b/frontend/src/Settings/Metadata/MetadataSettings.js index 001936ab7..3170787b2 100644 --- a/frontend/src/Settings/Metadata/MetadataSettings.js +++ b/frontend/src/Settings/Metadata/MetadataSettings.js @@ -1,21 +1,60 @@ -import React from 'react'; +import React, { Component } from 'react'; import PageContent from 'Components/Page/PageContent'; import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; import MetadatasConnector from './Metadata/MetadatasConnector'; +import MetadataProviderConnector from './MetadataProvider/MetadataProviderConnector'; -function MetadataSettings() { - return ( - - - - - - - - ); +class MetadataSettings extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + hasPendingChanges: false + }; + } + + // + // Listeners + + setMetadataProviderRef = (ref) => { + this._metadataProvider = ref; + } + + onHasPendingChange = (hasPendingChanges) => { + this.setState({ + hasPendingChanges + }); + } + + onSavePress = () => { + this._metadataProvider.getWrappedInstance().save(); + } + + // + // Render + render() { + return ( + + + + + + + + + ); + } } export default MetadataSettings; diff --git a/frontend/src/Store/Actions/actionTypes.js b/frontend/src/Store/Actions/actionTypes.js index 30ee48ad1..a88c1b8b8 100644 --- a/frontend/src/Store/Actions/actionTypes.js +++ b/frontend/src/Store/Actions/actionTypes.js @@ -297,6 +297,10 @@ export const SET_METADATA_VALUE = 'SET_METADATA_VALUE'; export const SET_METADATA_FIELD_VALUE = 'SET_METADATA_FIELD_VALUE'; export const SAVE_METADATA = 'SAVE_METADATA'; +export const FETCH_METADATA_PROVIDER = 'FETCH_METADATA_PROVIDER'; +export const SET_METADATA_PROVIDER_VALUE = 'SET_METADATA_PROVIDER_VALUE'; +export const SAVE_METADATA_PROVIDER = 'SAVE_METADATA_PROVIDER'; + // // System diff --git a/frontend/src/Store/Actions/settingsActionHandlers.js b/frontend/src/Store/Actions/settingsActionHandlers.js index d88d322b9..39b704c7a 100644 --- a/frontend/src/Store/Actions/settingsActionHandlers.js +++ b/frontend/src/Store/Actions/settingsActionHandlers.js @@ -243,6 +243,9 @@ const settingsActionHandlers = { '/metadata', (state) => state.settings.metadata), + [types.FETCH_METADATA_PROVIDER]: createFetchHandler('metadataProvider', '/config/metadataProvider'), + [types.SAVE_METADATA_PROVIDER]: createSaveHandler('metadataProvider', '/config/metadataProvider', (state) => state.settings.metadataProvider), + [types.FETCH_GENERAL_SETTINGS]: createFetchHandler('general', '/config/host'), [types.SAVE_GENERAL_SETTINGS]: createSaveHandler('general', '/config/host', (state) => state.settings.general) }; diff --git a/frontend/src/Store/Actions/settingsActions.js b/frontend/src/Store/Actions/settingsActions.js index a8743de99..64acc7926 100644 --- a/frontend/src/Store/Actions/settingsActions.js +++ b/frontend/src/Store/Actions/settingsActions.js @@ -203,6 +203,15 @@ export const setMetadataFieldValue = createAction(types.SET_METADATA_FIELD_VALUE }; }); +export const fetchMetadataProvider = settingsActionHandlers[types.FETCH_METADATA_PROVIDER]; +export const saveMetadataProvider = settingsActionHandlers[types.SAVE_METADATA_PROVIDER]; +export const setMetadataProviderValue = createAction(types.SET_METADATA_PROVIDER_VALUE, (payload) => { + return { + section: 'metadataProvider', + ...payload + }; +}); + export const fetchGeneralSettings = settingsActionHandlers[types.FETCH_GENERAL_SETTINGS]; export const saveGeneralSettings = settingsActionHandlers[types.SAVE_GENERAL_SETTINGS]; export const setGeneralSettingsValue = createAction(types.SET_GENERAL_SETTINGS_VALUE, (payload) => { diff --git a/frontend/src/Store/Reducers/settingsReducers.js b/frontend/src/Store/Reducers/settingsReducers.js index 73b4b0055..2e92ef6c1 100644 --- a/frontend/src/Store/Reducers/settingsReducers.js +++ b/frontend/src/Store/Reducers/settingsReducers.js @@ -201,6 +201,16 @@ export const defaultState = { pendingChanges: {} }, + metadataProvider: { + isFetching: false, + isPopulated: false, + error: null, + pendingChanges: {}, + isSaving: false, + saveError: null, + item: {} + }, + general: { isFetching: false, isPopulated: false, @@ -226,7 +236,8 @@ const propertyNames = [ 'qualityDefinitions', 'indexerOptions', 'downloadClientOptions', - 'general' + 'general', + 'metadataProvider' ]; const providerPropertyNames = [ @@ -330,6 +341,8 @@ const settingsReducers = handleActions({ [types.SET_METADATA_VALUE]: createSetSettingValueReducer('metadata'), [types.SET_METADATA_FIELD_VALUE]: createSetProviderFieldValueReducer('metadata'), + [types.SET_METADATA_PROVIDER_VALUE]: createSetSettingValueReducer('metadataProvider'), + [types.SET_GENERAL_SETTINGS_VALUE]: createSetSettingValueReducer('general') }, defaultState); diff --git a/src/Lidarr.Api.V3/Config/MetadataProviderConfigModule.cs b/src/Lidarr.Api.V3/Config/MetadataProviderConfigModule.cs new file mode 100644 index 000000000..3a9c217a4 --- /dev/null +++ b/src/Lidarr.Api.V3/Config/MetadataProviderConfigModule.cs @@ -0,0 +1,21 @@ +using System.Linq; +using System.Reflection; +using NzbDrone.Core.Configuration; +using Lidarr.Http; + +namespace Lidarr.Api.V3.Config +{ + public class MetadataProviderConfigModule : SonarrConfigModule + { + public MetadataProviderConfigModule(IConfigService configService) + : base(configService) + { + + } + + protected override MetadataProviderConfigResource ToResource(IConfigService model) + { + return MetadataProviderConfigResourceMapper.ToResource(model); + } + } +} diff --git a/src/Lidarr.Api.V3/Config/MetadataProviderConfigResource.cs b/src/Lidarr.Api.V3/Config/MetadataProviderConfigResource.cs new file mode 100644 index 000000000..f14ff6ce8 --- /dev/null +++ b/src/Lidarr.Api.V3/Config/MetadataProviderConfigResource.cs @@ -0,0 +1,24 @@ +using NzbDrone.Core.Configuration; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.Config +{ + public class MetadataProviderConfigResource : RestResource + { + //Calendar + public string MetadataSource { get; set; } + + } + + public static class MetadataProviderConfigResourceMapper + { + public static MetadataProviderConfigResource ToResource(IConfigService model) + { + return new MetadataProviderConfigResource + { + MetadataSource = model.MetadataSource, + + }; + } + } +} diff --git a/src/Lidarr.Api.V3/Lidarr.Api.V3.csproj b/src/Lidarr.Api.V3/Lidarr.Api.V3.csproj index 71265644c..953710af6 100644 --- a/src/Lidarr.Api.V3/Lidarr.Api.V3.csproj +++ b/src/Lidarr.Api.V3/Lidarr.Api.V3.csproj @@ -95,6 +95,8 @@ + + diff --git a/src/NzbDrone.Common/Cloud/SonarrCloudRequestBuilder.cs b/src/NzbDrone.Common/Cloud/LidarrCloudRequestBuilder.cs similarity index 75% rename from src/NzbDrone.Common/Cloud/SonarrCloudRequestBuilder.cs rename to src/NzbDrone.Common/Cloud/LidarrCloudRequestBuilder.cs index 35f93b8aa..96f771ed4 100644 --- a/src/NzbDrone.Common/Cloud/SonarrCloudRequestBuilder.cs +++ b/src/NzbDrone.Common/Cloud/LidarrCloudRequestBuilder.cs @@ -7,7 +7,6 @@ namespace NzbDrone.Common.Cloud IHttpRequestBuilderFactory Services { get; } IHttpRequestBuilderFactory Search { get; } IHttpRequestBuilderFactory InternalSearch { get; } - IHttpRequestBuilderFactory SkyHookTvdb { get; } } public class LidarrCloudRequestBuilder : ILidarrCloudRequestBuilder @@ -19,11 +18,6 @@ namespace NzbDrone.Common.Cloud Search = new HttpRequestBuilder("https://api.lidarr.audio/api/v0/{route}/") // TODO: Add {version} once LidarrAPI.Metadata is released. .CreateFactory(); - - - SkyHookTvdb = new HttpRequestBuilder("http://skyhook.lidarr.tv/v1/tvdb/{route}/{language}/") - .SetSegment("language", "en") - .CreateFactory(); } public IHttpRequestBuilderFactory Services { get; } @@ -31,7 +25,5 @@ namespace NzbDrone.Common.Cloud public IHttpRequestBuilderFactory Search { get; } public IHttpRequestBuilderFactory InternalSearch { get; } - - public IHttpRequestBuilderFactory SkyHookTvdb { get; } } } diff --git a/src/NzbDrone.Common/NzbDrone.Common.csproj b/src/NzbDrone.Common/NzbDrone.Common.csproj index 6325d63f8..5f3ba845c 100644 --- a/src/NzbDrone.Common/NzbDrone.Common.csproj +++ b/src/NzbDrone.Common/NzbDrone.Common.csproj @@ -78,7 +78,7 @@ - + diff --git a/src/NzbDrone.Core/Configuration/ConfigService.cs b/src/NzbDrone.Core/Configuration/ConfigService.cs index 0d9bede18..83817a5d2 100644 --- a/src/NzbDrone.Core/Configuration/ConfigService.cs +++ b/src/NzbDrone.Core/Configuration/ConfigService.cs @@ -238,6 +238,13 @@ namespace NzbDrone.Core.Configuration set { SetValue("ChownGroup", value); } } + public string MetadataSource + { + get { return GetValue("MetadataSource", ""); } + + set { SetValue("MetadataSource", value); } + } + public int FirstDayOfWeek { get { return GetValueInt("FirstDayOfWeek", (int)CultureInfo.CurrentCulture.DateTimeFormat.FirstDayOfWeek); } diff --git a/src/NzbDrone.Core/Configuration/IConfigService.cs b/src/NzbDrone.Core/Configuration/IConfigService.cs index 72822fb2f..ff39ae96f 100644 --- a/src/NzbDrone.Core/Configuration/IConfigService.cs +++ b/src/NzbDrone.Core/Configuration/IConfigService.cs @@ -58,6 +58,9 @@ namespace NzbDrone.Core.Configuration //Internal bool CleanupMetadataImages { get; set; } + //MetadataSource + string MetadataSource { get; set; } + //Forms Auth string RijndaelPassphrase { get; } diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs index 24001423a..6203d4ce9 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs @@ -13,6 +13,7 @@ using NzbDrone.Core.Tv; using Newtonsoft.Json.Linq; using NzbDrone.Core.Music; using Newtonsoft.Json; +using NzbDrone.Core.Configuration; namespace NzbDrone.Core.MetadataSource.SkyHook { @@ -23,15 +24,20 @@ namespace NzbDrone.Core.MetadataSource.SkyHook private readonly IArtistService _artistService; private readonly IHttpRequestBuilderFactory _requestBuilder; + private readonly IConfigService _configService; - public SkyHookProxy(IHttpClient httpClient, ILidarrCloudRequestBuilder requestBuilder, IArtistService artistService, Logger logger) + private IHttpRequestBuilderFactory customerRequestBuilder; + + public SkyHookProxy(IHttpClient httpClient, ILidarrCloudRequestBuilder requestBuilder, IArtistService artistService, Logger logger, IConfigService configService) { _httpClient = httpClient; + _configService = configService; _requestBuilder = requestBuilder.Search; _artistService = artistService; _logger = logger; } + [Obsolete("Used for Sonarr, not Lidarr")] public Tuple> GetSeriesInfo(int tvdbSeriesId) { throw new NotImplementedException(); @@ -42,8 +48,9 @@ namespace NzbDrone.Core.MetadataSource.SkyHook _logger.Debug("Getting Artist with LidarrAPI.MetadataID of {0}", foreignArtistId); - // We need to perform a direct lookup of the artist - var httpRequest = _requestBuilder.Create() + SetCustomProvider(); + + var httpRequest = customerRequestBuilder.Create() .SetSegment("route", "artists/" + foreignArtistId) .Build(); @@ -99,7 +106,9 @@ namespace NzbDrone.Core.MetadataSource.SkyHook } } - var httpRequest = _requestBuilder.Create() + SetCustomProvider(); + + var httpRequest = customerRequestBuilder.Create() .SetSegment("route", "search") .AddQueryParam("type", "artist") .AddQueryParam("query", title.ToLower().Trim()) @@ -113,12 +122,12 @@ namespace NzbDrone.Core.MetadataSource.SkyHook } catch (HttpException) { - throw new SkyHookException("Search for '{0}' failed. Unable to communicate with SkyHook.", title); + throw new SkyHookException("Search for '{0}' failed. Unable to communicate with LidarrAPI.", title); } catch (Exception ex) { _logger.Warn(ex, ex.Message); - throw new SkyHookException("Search for '{0}' failed. Invalid response received from SkyHook.", title); + throw new SkyHookException("Search for '{0}' failed. Invalid response received from LidarrAPI.", title); } } @@ -268,5 +277,17 @@ namespace NzbDrone.Core.MetadataSource.SkyHook return MediaCoverTypes.Unknown; } } + + private void SetCustomProvider() + { + if (_configService.MetadataSource.IsNotNullOrWhiteSpace()) + { + customerRequestBuilder = new HttpRequestBuilder(_configService.MetadataSource + "{route}/").CreateFactory(); + } + else + { + customerRequestBuilder = _requestBuilder; + } + } } }