New: Movie Certifications

This commit is contained in:
Qstick
2020-04-02 20:57:36 -04:00
parent 770e3379fb
commit dd52760095
22 changed files with 412 additions and 36 deletions

View File

@@ -121,6 +121,13 @@
margin-right: 15px; margin-right: 15px;
} }
.certification {
margin-right: 15px;
padding: 0 5px;
border: 1px solid;
border-radius: 5px;
}
.detailsLabel { .detailsLabel {
composes: label from '~Components/Label.css'; composes: label from '~Components/Label.css';

View File

@@ -162,6 +162,7 @@ class MovieDetails extends Component {
title, title,
year, year,
runtime, runtime,
certification,
ratings, ratings,
path, path,
sizeOnDisk, sizeOnDisk,
@@ -329,6 +330,13 @@ class MovieDetails extends Component {
<div className={styles.details}> <div className={styles.details}>
<div> <div>
{
!!certification &&
<span className={styles.certification}>
{certification}
</span>
}
{ {
year > 0 && year > 0 &&
<span className={styles.year}> <span className={styles.year}>
@@ -616,6 +624,7 @@ MovieDetails.propTypes = {
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired,
year: PropTypes.number.isRequired, year: PropTypes.number.isRequired,
runtime: PropTypes.number.isRequired, runtime: PropTypes.number.isRequired,
certification: PropTypes.string,
ratings: PropTypes.object.isRequired, ratings: PropTypes.object.isRequired,
path: PropTypes.string.isRequired, path: PropTypes.string.isRequired,
sizeOnDisk: PropTypes.number.isRequired, sizeOnDisk: PropTypes.number.isRequired,

View File

@@ -116,8 +116,8 @@ class NamingModal extends Component {
const movieTokens = [ const movieTokens = [
{ token: '{Movie Title}', example: 'Movie Title!' }, { token: '{Movie Title}', example: 'Movie Title!' },
{ token: '{Movie CleanTitle}', example: 'Movie Title' }, { token: '{Movie CleanTitle}', example: 'Movie Title' },
{ token: '{Movie TitleThe}', example: 'Movie Title, The' } { token: '{Movie TitleThe}', example: 'Movie Title, The' },
{ token: '{Movie Certification}', example: 'R' }
]; ];
const movieIdTokens = [ const movieIdTokens = [

View File

@@ -1,21 +1,68 @@
import React from 'react'; import React, { Component } from 'react';
import PageContent from 'Components/Page/PageContent'; import PageContent from 'Components/Page/PageContent';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import MetadatasConnector from './Metadata/MetadatasConnector'; import MetadatasConnector from './Metadata/MetadatasConnector';
import MetadataOptionsConnector from './Options/MetadataOptionsConnector';
class MetadataSettings extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this._saveCallback = null;
this.state = {
isSaving: false,
hasPendingChanges: false
};
}
//
// Listeners
onChildMounted = (saveCallback) => {
this._saveCallback = saveCallback;
}
onChildStateChange = (payload) => {
this.setState(payload);
}
onSavePress = () => {
if (this._saveCallback) {
this._saveCallback();
}
}
render() {
const {
isSaving,
hasPendingChanges
} = this.state;
function MetadataSettings() {
return ( return (
<PageContent title="Metadata Settings"> <PageContent title="Metadata Settings">
<SettingsToolbarConnector <SettingsToolbarConnector
showSave={false} isSaving={isSaving}
hasPendingChanges={hasPendingChanges}
onSavePress={this.onSavePress}
/> />
<PageContentBodyConnector> <PageContentBodyConnector>
<MetadataOptionsConnector
onChildMounted={this.onChildMounted}
onChildStateChange={this.onChildStateChange}
/>
<MetadatasConnector /> <MetadatasConnector />
</PageContentBodyConnector> </PageContentBodyConnector>
</PageContent> </PageContent>
); );
} }
}
export default MetadataSettings; export default MetadataSettings;

View File

@@ -0,0 +1,66 @@
import PropTypes from 'prop-types';
import React from 'react';
import { inputTypes } from 'Helpers/Props';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FieldSet from 'Components/FieldSet';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormLabel from 'Components/Form/FormLabel';
import FormInputGroup from 'Components/Form/FormInputGroup';
export const certificationCountryOptions = [
{ key: 'us', value: 'United States' },
{ key: 'gb', value: 'Great Britain' }
];
function MetadataOptions(props) {
const {
isFetching,
error,
settings,
hasSettings,
onInputChange
} = props;
return (
<FieldSet legend="Options">
{
isFetching &&
<LoadingIndicator />
}
{
!isFetching && error &&
<div>Unable to load indexer options</div>
}
{
hasSettings && !isFetching && !error &&
<Form>
<FormGroup>
<FormLabel>Certification Country</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="certificationCountry"
values={certificationCountryOptions}
onChange={onInputChange}
helpText="Select Country for Movie Certifications"
{...settings.certificationCountry}
/>
</FormGroup>
</Form>
}
</FieldSet>
);
}
MetadataOptions.propTypes = {
isFetching: PropTypes.bool.isRequired,
error: PropTypes.object,
settings: PropTypes.object.isRequired,
hasSettings: PropTypes.bool.isRequired,
onInputChange: PropTypes.func.isRequired
};
export default MetadataOptions;

View File

@@ -0,0 +1,101 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
import { fetchMetadataOptions, setMetadataOptionsValue, saveMetadataOptions } from 'Store/Actions/settingsActions';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import MetadataOptions from './MetadataOptions';
const SECTION = 'metadataOptions';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.advancedSettings,
createSettingsSectionSelector(SECTION),
(advancedSettings, sectionSettings) => {
return {
advancedSettings,
...sectionSettings
};
}
);
}
const mapDispatchToProps = {
dispatchFetchMetadataOptions: fetchMetadataOptions,
dispatchSetMetadataOptionsValue: setMetadataOptionsValue,
dispatchSaveMetadataOptions: saveMetadataOptions,
dispatchClearPendingChanges: clearPendingChanges
};
class MetadataOptionsConnector extends Component {
//
// Lifecycle
componentDidMount() {
const {
dispatchFetchMetadataOptions,
dispatchSaveMetadataOptions,
onChildMounted
} = this.props;
dispatchFetchMetadataOptions();
onChildMounted(dispatchSaveMetadataOptions);
}
componentDidUpdate(prevProps) {
const {
hasPendingChanges,
isSaving,
onChildStateChange
} = this.props;
if (
prevProps.isSaving !== isSaving ||
prevProps.hasPendingChanges !== hasPendingChanges
) {
onChildStateChange({
isSaving,
hasPendingChanges
});
}
}
componentWillUnmount() {
this.props.dispatchClearPendingChanges({ section: SECTION });
}
//
// Listeners
onInputChange = ({ name, value }) => {
this.props.dispatchSetMetadataOptionsValue({ name, value });
}
//
// Render
render() {
return (
<MetadataOptions
onInputChange={this.onInputChange}
{...this.props}
/>
);
}
}
MetadataOptionsConnector.propTypes = {
isSaving: PropTypes.bool.isRequired,
hasPendingChanges: PropTypes.bool.isRequired,
dispatchFetchMetadataOptions: PropTypes.func.isRequired,
dispatchSetMetadataOptionsValue: PropTypes.func.isRequired,
dispatchSaveMetadataOptions: PropTypes.func.isRequired,
dispatchClearPendingChanges: PropTypes.func.isRequired,
onChildMounted: PropTypes.func.isRequired,
onChildStateChange: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(MetadataOptionsConnector);

View File

@@ -0,0 +1,64 @@
import { createAction } from 'redux-actions';
import { createThunk } from 'Store/thunks';
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
import createSaveHandler from 'Store/Actions/Creators/createSaveHandler';
//
// Variables
const section = 'settings.metadataOptions';
//
// Actions Types
export const FETCH_METADATA_OPTIONS = 'settings/metadataOptions/fetchMetadataOptions';
export const SAVE_METADATA_OPTIONS = 'settings/metadataOptions/saveMetadataOptions';
export const SET_METADATA_OPTIONS_VALUE = 'settings/metadataOptions/setMetadataOptionsValue';
//
// Action Creators
export const fetchMetadataOptions = createThunk(FETCH_METADATA_OPTIONS);
export const saveMetadataOptions = createThunk(SAVE_METADATA_OPTIONS);
export const setMetadataOptionsValue = createAction(SET_METADATA_OPTIONS_VALUE, (payload) => {
return {
section,
...payload
};
});
//
// Details
export default {
//
// State
defaultState: {
isFetching: false,
isPopulated: false,
error: null,
pendingChanges: {},
isSaving: false,
saveError: null,
item: {}
},
//
// Action Handlers
actionHandlers: {
[FETCH_METADATA_OPTIONS]: createFetchHandler(section, '/config/metadata'),
[SAVE_METADATA_OPTIONS]: createSaveHandler(section, '/config/metadata')
},
//
// Reducers
reducers: {
[SET_METADATA_OPTIONS_VALUE]: createSetSettingValueReducer(section)
}
};

View File

@@ -158,7 +158,7 @@ export const defaultState = {
{ {
name: 'certification', name: 'certification',
label: 'Certification', label: 'Certification',
isSortable: false, isSortable: true,
isVisible: false isVisible: false
}, },
{ {
@@ -320,7 +320,21 @@ export const defaultState = {
{ {
name: 'certification', name: 'certification',
label: 'Certification', label: 'Certification',
type: filterBuilderTypes.EXACT type: filterBuilderTypes.EXACT,
optionsSelector: function(items) {
const certificationList = items.reduce((acc, movie) => {
if (movie.certification) {
acc.push({
id: movie.certification,
name: movie.certification
});
}
return acc;
}, []);
return certificationList.sort(sortByName);
}
}, },
{ {
name: 'tags', name: 'tags',

View File

@@ -15,6 +15,7 @@ import netImportOptions from './Settings/netImportOptions';
import netImports from './Settings/netImports'; import netImports from './Settings/netImports';
import mediaManagement from './Settings/mediaManagement'; import mediaManagement from './Settings/mediaManagement';
import metadata from './Settings/metadata'; import metadata from './Settings/metadata';
import metadataOptions from './Settings/metadataOptions';
import naming from './Settings/naming'; import naming from './Settings/naming';
import namingExamples from './Settings/namingExamples'; import namingExamples from './Settings/namingExamples';
import notifications from './Settings/notifications'; import notifications from './Settings/notifications';
@@ -38,6 +39,7 @@ export * from './Settings/netImportOptions';
export * from './Settings/netImports'; export * from './Settings/netImports';
export * from './Settings/mediaManagement'; export * from './Settings/mediaManagement';
export * from './Settings/metadata'; export * from './Settings/metadata';
export * from './Settings/metadataOptions';
export * from './Settings/naming'; export * from './Settings/naming';
export * from './Settings/namingExamples'; export * from './Settings/namingExamples';
export * from './Settings/notifications'; export * from './Settings/notifications';
@@ -72,6 +74,7 @@ export const defaultState = {
netImports: netImports.defaultState, netImports: netImports.defaultState,
mediaManagement: mediaManagement.defaultState, mediaManagement: mediaManagement.defaultState,
metadata: metadata.defaultState, metadata: metadata.defaultState,
metadataOptions: metadataOptions.defaultState,
naming: naming.defaultState, naming: naming.defaultState,
namingExamples: namingExamples.defaultState, namingExamples: namingExamples.defaultState,
notifications: notifications.defaultState, notifications: notifications.defaultState,
@@ -114,6 +117,7 @@ export const actionHandlers = handleThunks({
...netImports.actionHandlers, ...netImports.actionHandlers,
...mediaManagement.actionHandlers, ...mediaManagement.actionHandlers,
...metadata.actionHandlers, ...metadata.actionHandlers,
...metadataOptions.actionHandlers,
...naming.actionHandlers, ...naming.actionHandlers,
...namingExamples.actionHandlers, ...namingExamples.actionHandlers,
...notifications.actionHandlers, ...notifications.actionHandlers,
@@ -147,6 +151,7 @@ export const reducers = createHandleActions({
...netImports.reducers, ...netImports.reducers,
...mediaManagement.reducers, ...mediaManagement.reducers,
...metadata.reducers, ...metadata.reducers,
...metadataOptions.reducers,
...naming.reducers, ...naming.reducers,
...namingExamples.reducers, ...namingExamples.reducers,
...notifications.reducers, ...notifications.reducers,

View File

@@ -8,6 +8,7 @@ using NzbDrone.Common.Http.Proxy;
using NzbDrone.Core.Configuration.Events; using NzbDrone.Core.Configuration.Events;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.MetadataSource.SkyHook.Resource;
using NzbDrone.Core.Parser; using NzbDrone.Core.Parser;
using NzbDrone.Core.Security; using NzbDrone.Core.Security;
@@ -134,6 +135,13 @@ namespace NzbDrone.Core.Configuration
set { SetValue("ImportExclusions", value); } set { SetValue("ImportExclusions", value); }
} }
public TMDbCountryCode CertificationCountry
{
get { return GetValueEnum("CertificationCountry", TMDbCountryCode.US); }
set { SetValue("CertificationCountry", value); }
}
public int MaximumSize public int MaximumSize
{ {
get { return GetValueInt("MaximumSize", 0); } get { return GetValueInt("MaximumSize", 0); }

View File

@@ -1,6 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using NzbDrone.Common.Http.Proxy; using NzbDrone.Common.Http.Proxy;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MetadataSource.SkyHook.Resource;
using NzbDrone.Core.Parser; using NzbDrone.Core.Parser;
using NzbDrone.Core.Security; using NzbDrone.Core.Security;
@@ -66,6 +67,9 @@ namespace NzbDrone.Core.Configuration
string ListSyncLevel { get; set; } string ListSyncLevel { get; set; }
string ImportExclusions { get; set; } string ImportExclusions { get; set; }
//Metadata Provider
TMDbCountryCode CertificationCountry { get; set; }
//UI //UI
int FirstDayOfWeek { get; set; } int FirstDayOfWeek { get; set; }
string CalendarWeekColumnHeader { get; set; } string CalendarWeekColumnHeader { get; set; }

View File

@@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@@ -150,6 +150,11 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
uniqueId.SetAttributeValue("type", "tmdb"); uniqueId.SetAttributeValue("type", "tmdb");
details.Add(uniqueId); details.Add(uniqueId);
if (movie.Certification.IsNotNullOrWhiteSpace())
{
details.Add(new XElement("mpaa", movie.Certification));
}
details.Add(new XElement("year", movie.Year)); details.Add(new XElement("year", movie.Year));
if (movie.InCinemas.HasValue) if (movie.InCinemas.HasValue)

View File

@@ -1,5 +1,4 @@
using System; using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq; using System.Linq;
namespace NzbDrone.Core.Languages namespace NzbDrone.Core.Languages

View File

@@ -0,0 +1,8 @@
namespace NzbDrone.Core.MetadataSource.SkyHook.Resource
{
public enum TMDbCountryCode
{
US,
GB
}
}

View File

@@ -8,6 +8,7 @@ using NLog;
using NzbDrone.Common.Cloud; using NzbDrone.Common.Cloud;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Exceptions; using NzbDrone.Core.Exceptions;
using NzbDrone.Core.Languages; using NzbDrone.Core.Languages;
using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaCover;
@@ -29,30 +30,30 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
private readonly Logger _logger; private readonly Logger _logger;
private readonly IHttpRequestBuilderFactory _movieBuilder; private readonly IHttpRequestBuilderFactory _movieBuilder;
private readonly ITmdbConfigService _configService; private readonly ITmdbConfigService _tmdbConfigService;
private readonly IConfigService _configService;
private readonly IMovieService _movieService; private readonly IMovieService _movieService;
private readonly IPreDBService _predbService; private readonly IPreDBService _predbService;
private readonly IImportExclusionsService _exclusionService; private readonly IImportExclusionsService _exclusionService;
private readonly IAlternativeTitleService _altTitleService;
private readonly IRadarrAPIClient _radarrAPI; private readonly IRadarrAPIClient _radarrAPI;
public SkyHookProxy(IHttpClient httpClient, public SkyHookProxy(IHttpClient httpClient,
IRadarrCloudRequestBuilder requestBuilder, IRadarrCloudRequestBuilder requestBuilder,
ITmdbConfigService configService, ITmdbConfigService tmdbConfigService,
IConfigService configService,
IMovieService movieService, IMovieService movieService,
IPreDBService predbService, IPreDBService predbService,
IImportExclusionsService exclusionService, IImportExclusionsService exclusionService,
IAlternativeTitleService altTitleService,
IRadarrAPIClient radarrAPI, IRadarrAPIClient radarrAPI,
Logger logger) Logger logger)
{ {
_httpClient = httpClient; _httpClient = httpClient;
_movieBuilder = requestBuilder.TMDB; _movieBuilder = requestBuilder.TMDB;
_tmdbConfigService = tmdbConfigService;
_configService = configService; _configService = configService;
_movieService = movieService; _movieService = movieService;
_predbService = predbService; _predbService = predbService;
_exclusionService = exclusionService; _exclusionService = exclusionService;
_altTitleService = altTitleService;
_radarrAPI = radarrAPI; _radarrAPI = radarrAPI;
_logger = logger; _logger = logger;
@@ -185,13 +186,9 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
movie.Images.AddIfNotNull(MapImage(resource.backdrop_path, MediaCoverTypes.Fanart)); movie.Images.AddIfNotNull(MapImage(resource.backdrop_path, MediaCoverTypes.Fanart));
movie.Runtime = resource.runtime; movie.Runtime = resource.runtime;
//foreach(Title title in resource.alternative_titles.titles) foreach (var releaseDates in resource.release_dates.results)
//{
// movie.AlternativeTitles.Add(title.title);
//}
foreach (ReleaseDates releaseDates in resource.release_dates.results)
{ {
foreach (ReleaseDate releaseDate in releaseDates.release_dates) foreach (var releaseDate in releaseDates.release_dates)
{ {
if (releaseDate.type == 5 || releaseDate.type == 4) if (releaseDate.type == 5 || releaseDate.type == 4)
{ {
@@ -209,6 +206,12 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
movie.PhysicalReleaseNote = releaseDate.note; movie.PhysicalReleaseNote = releaseDate.note;
} }
} }
// Set Certification from Theatrical Release
if (releaseDate.type == 3 && releaseDates.iso_3166_1 == _configService.CertificationCountry.ToString())
{
movie.Certification = releaseDate.certification;
}
} }
} }
@@ -216,7 +219,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
movie.Ratings.Votes = resource.vote_count; movie.Ratings.Votes = resource.vote_count;
movie.Ratings.Value = (decimal)resource.vote_average; movie.Ratings.Value = (decimal)resource.vote_average;
foreach (Genre genre in resource.genres) foreach (var genre in resource.genres)
{ {
movie.Genres.Add(genre.name); movie.Genres.Add(genre.name);
} }
@@ -708,7 +711,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
{ {
if (path.IsNotNullOrWhiteSpace()) if (path.IsNotNullOrWhiteSpace())
{ {
return _configService.GetCoverForURL(path, type); return _tmdbConfigService.GetCoverForURL(path, type);
} }
return null; return null;

View File

@@ -4,7 +4,6 @@ using System.Web;
using FluentValidation.Results; using FluentValidation.Results;
using NLog; using NLog;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http.Proxy;
using NzbDrone.Common.Serializer; using NzbDrone.Common.Serializer;
using NzbDrone.Core.Rest; using NzbDrone.Core.Rest;
using RestSharp; using RestSharp;

View File

@@ -216,6 +216,7 @@ namespace NzbDrone.Core.Organizer
tokenHandlers["{Movie Title}"] = m => movie.Title; tokenHandlers["{Movie Title}"] = m => movie.Title;
tokenHandlers["{Movie CleanTitle}"] = m => CleanTitle(movie.Title); tokenHandlers["{Movie CleanTitle}"] = m => CleanTitle(movie.Title);
tokenHandlers["{Movie Title The}"] = m => TitleThe(movie.Title); tokenHandlers["{Movie Title The}"] = m => TitleThe(movie.Title);
tokenHandlers["{Movie Certification}"] = mbox => movie.Certification;
} }
private void AddTagsTokens(Dictionary<string, Func<TokenMatch, string>> tokenHandlers, MovieFile movieFile) private void AddTagsTokens(Dictionary<string, Func<TokenMatch, string>> tokenHandlers, MovieFile movieFile)

View File

@@ -1,5 +1,4 @@
using System; using System;
using NzbDrone.Core.CustomFormats;
using NzbDrone.Core.MediaFiles.MediaInfo; using NzbDrone.Core.MediaFiles.MediaInfo;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Qualities; using NzbDrone.Core.Qualities;

View File

@@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NzbDrone.Core.CustomFormats;
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.Qualities namespace NzbDrone.Core.Qualities

View File

@@ -1,7 +1,6 @@
using System.Linq; using System.Linq;
using NLog; using NLog;
using NzbDrone.Common.Instrumentation; using NzbDrone.Common.Instrumentation;
using NzbDrone.Core.CustomFormats;
namespace NzbDrone.Core.Qualities namespace NzbDrone.Core.Qualities
{ {

View File

@@ -0,0 +1,17 @@
using NzbDrone.Core.Configuration;
namespace Radarr.Api.V3.Config
{
public class MetadataConfigModule : RadarrConfigModule<MetadataConfigResource>
{
public MetadataConfigModule(IConfigService configService)
: base(configService)
{
}
protected override MetadataConfigResource ToResource(IConfigService model)
{
return MetadataConfigResourceMapper.ToResource(model);
}
}
}

View File

@@ -0,0 +1,22 @@
using NzbDrone.Core.Configuration;
using NzbDrone.Core.MetadataSource.SkyHook.Resource;
using Radarr.Http.REST;
namespace Radarr.Api.V3.Config
{
public class MetadataConfigResource : RestResource
{
public TMDbCountryCode CertificationCountry { get; set; }
}
public static class MetadataConfigResourceMapper
{
public static MetadataConfigResource ToResource(IConfigService model)
{
return new MetadataConfigResource
{
CertificationCountry = model.CertificationCountry,
};
}
}
}