New: Movie Certifications

pull/2/head
Qstick 5 years ago
parent 770e3379fb
commit dd52760095

@ -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';

@ -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,

@ -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 = [

@ -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';
function MetadataSettings() { class MetadataSettings extends Component {
return (
<PageContent title="Metadata Settings"> //
<SettingsToolbarConnector // Lifecycle
showSave={false}
/> constructor(props, context) {
super(props, context);
<PageContentBodyConnector>
<MetadatasConnector /> this._saveCallback = null;
</PageContentBodyConnector>
</PageContent> 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;
return (
<PageContent title="Metadata Settings">
<SettingsToolbarConnector
isSaving={isSaving}
hasPendingChanges={hasPendingChanges}
onSavePress={this.onSavePress}
/>
<PageContentBodyConnector>
<MetadataOptionsConnector
onChildMounted={this.onChildMounted}
onChildStateChange={this.onChildStateChange}
/>
<MetadatasConnector />
</PageContentBodyConnector>
</PageContent>
);
}
} }
export default MetadataSettings; export default MetadataSettings;

@ -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;

@ -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);

@ -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)
}
};

@ -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',

@ -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,

@ -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); }

@ -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; }

@ -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)

@ -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

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

@ -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;

@ -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;

@ -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)

@ -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;

@ -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

@ -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
{ {

@ -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);
}
}
}

@ -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,
};
}
}
}
Loading…
Cancel
Save