1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2024-12-10 11:10:40 +02:00

New: Ability to select Plex Media Server from plex.tv

Closes #6887
This commit is contained in:
Mark McDowall 2024-06-19 15:50:36 -07:00 committed by Mark McDowall
parent fb060730c7
commit 4c622fd412
13 changed files with 252 additions and 38 deletions

View File

@ -271,26 +271,32 @@ class EnhancedSelectInput extends Component {
this.setState({ isOpen: !this.state.isOpen });
};
onSelect = (value) => {
if (Array.isArray(this.props.value)) {
let newValue = null;
const index = this.props.value.indexOf(value);
onSelect = (newValue) => {
const { name, value, values, onChange } = this.props;
const additionalProperties = values.find((v) => v.key === newValue)?.additionalProperties;
if (Array.isArray(value)) {
let arrayValue = null;
const index = value.indexOf(newValue);
if (index === -1) {
newValue = this.props.values.map((v) => v.key).filter((v) => (v === value) || this.props.value.includes(v));
arrayValue = values.map((v) => v.key).filter((v) => (v === newValue) || value.includes(v));
} else {
newValue = [...this.props.value];
newValue.splice(index, 1);
arrayValue = [...value];
arrayValue.splice(index, 1);
}
this.props.onChange({
name: this.props.name,
value: newValue
onChange({
name,
value: arrayValue,
additionalProperties
});
} else {
this.setState({ isOpen: false });
this.props.onChange({
name: this.props.name,
value
onChange({
name,
value: newValue,
additionalProperties
});
}
};
@ -485,7 +491,7 @@ class EnhancedSelectInput extends Component {
values.map((v, index) => {
const hasParent = v.parentKey !== undefined;
const depth = hasParent ? 1 : 0;
const parentSelected = hasParent && value.includes(v.parentKey);
const parentSelected = hasParent && Array.isArray(value) && value.includes(v.parentKey);
return (
<OptionComponent
key={v.key}

View File

@ -9,7 +9,8 @@ import EnhancedSelectInput from './EnhancedSelectInput';
const importantFieldNames = [
'baseUrl',
'apiPath',
'apiKey'
'apiKey',
'authToken'
];
function getProviderDataKey(providerData) {
@ -34,7 +35,9 @@ function getSelectOptions(items) {
key: option.value,
value: option.name,
hint: option.hint,
parentKey: option.parentValue
parentKey: option.parentValue,
isDisabled: option.isDisabled,
additionalProperties: option.additionalProperties
};
});
}
@ -147,7 +150,7 @@ EnhancedSelectInputConnector.propTypes = {
provider: PropTypes.string.isRequired,
providerData: PropTypes.object.isRequired,
name: PropTypes.string.isRequired,
value: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string])).isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.arrayOf(PropTypes.string), PropTypes.arrayOf(PropTypes.number)]).isRequired,
values: PropTypes.arrayOf(PropTypes.object).isRequired,
selectOptionsProviderAction: PropTypes.string,
onChange: PropTypes.func.isRequired,

View File

@ -4,7 +4,7 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import {
saveNotification,
setNotificationFieldValue,
setNotificationFieldValues,
setNotificationValue,
testNotification,
toggleAdvancedSettings
@ -27,7 +27,7 @@ function createMapStateToProps() {
const mapDispatchToProps = {
setNotificationValue,
setNotificationFieldValue,
setNotificationFieldValues,
saveNotification,
testNotification,
toggleAdvancedSettings
@ -51,8 +51,8 @@ class EditNotificationModalContentConnector extends Component {
this.props.setNotificationValue({ name, value });
};
onFieldChange = ({ name, value }) => {
this.props.setNotificationFieldValue({ name, value });
onFieldChange = ({ name, value, additionalProperties = {} }) => {
this.props.setNotificationFieldValues({ properties: { ...additionalProperties, [name]: value } });
};
onSavePress = () => {
@ -91,7 +91,7 @@ EditNotificationModalContentConnector.propTypes = {
saveError: PropTypes.object,
item: PropTypes.object.isRequired,
setNotificationValue: PropTypes.func.isRequired,
setNotificationFieldValue: PropTypes.func.isRequired,
setNotificationFieldValues: PropTypes.func.isRequired,
saveNotification: PropTypes.func.isRequired,
testNotification: PropTypes.func.isRequired,
toggleAdvancedSettings: PropTypes.func.isRequired,

View File

@ -0,0 +1,25 @@
import getSectionState from 'Utilities/State/getSectionState';
import updateSectionState from 'Utilities/State/updateSectionState';
function createSetProviderFieldValuesReducer(section) {
return (state, { payload }) => {
if (section === payload.section) {
const { properties } = payload;
const newState = getSectionState(state, section);
newState.pendingChanges = Object.assign({}, newState.pendingChanges);
const fields = Object.assign({}, newState.pendingChanges.fields || {});
Object.keys(properties).forEach((name) => {
fields[name] = properties[name];
});
newState.pendingChanges.fields = fields;
return updateSectionState(state, section, newState);
}
return state;
};
}
export default createSetProviderFieldValuesReducer;

View File

@ -5,6 +5,7 @@ import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHand
import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler';
import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler';
import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
import createSetProviderFieldValuesReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValuesReducer';
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
import { createThunk } from 'Store/thunks';
import selectProviderSchema from 'Utilities/State/selectProviderSchema';
@ -22,6 +23,7 @@ export const FETCH_NOTIFICATION_SCHEMA = 'settings/notifications/fetchNotificati
export const SELECT_NOTIFICATION_SCHEMA = 'settings/notifications/selectNotificationSchema';
export const SET_NOTIFICATION_VALUE = 'settings/notifications/setNotificationValue';
export const SET_NOTIFICATION_FIELD_VALUE = 'settings/notifications/setNotificationFieldValue';
export const SET_NOTIFICATION_FIELD_VALUES = 'settings/notifications/setNotificationFieldValues';
export const SAVE_NOTIFICATION = 'settings/notifications/saveNotification';
export const CANCEL_SAVE_NOTIFICATION = 'settings/notifications/cancelSaveNotification';
export const DELETE_NOTIFICATION = 'settings/notifications/deleteNotification';
@ -55,6 +57,13 @@ export const setNotificationFieldValue = createAction(SET_NOTIFICATION_FIELD_VAL
};
});
export const setNotificationFieldValues = createAction(SET_NOTIFICATION_FIELD_VALUES, (payload) => {
return {
section,
...payload
};
});
//
// Details
@ -99,6 +108,7 @@ export default {
reducers: {
[SET_NOTIFICATION_VALUE]: createSetSettingValueReducer(section),
[SET_NOTIFICATION_FIELD_VALUE]: createSetProviderFieldValueReducer(section),
[SET_NOTIFICATION_FIELD_VALUES]: createSetProviderFieldValuesReducer(section),
[SELECT_NOTIFICATION_SCHEMA]: (state, { payload }) => {
return selectProviderSchema(state, section, payload, (selectedSchema) => {

View File

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
namespace NzbDrone.Core.Annotations
@ -59,13 +60,27 @@ public FieldTokenAttribute(TokenField field, string label = "", string token = "
public string Value { get; set; }
}
public class FieldSelectOption
public class FieldSelectOption<T>
where T : struct
{
public int Value { get; set; }
public T Value { get; set; }
public string Name { get; set; }
public int Order { get; set; }
public string Hint { get; set; }
public int? ParentValue { get; set; }
public T? ParentValue { get; set; }
public bool? IsDisabled { get; set; }
public Dictionary<string, object> AdditionalProperties { get; set; }
}
public class FieldSelectStringOption
{
public string Value { get; set; }
public string Name { get; set; }
public int Order { get; set; }
public string Hint { get; set; }
public string ParentValue { get; set; }
public bool? IsDisabled { get; set; }
public Dictionary<string, object> AdditionalProperties { get; set; }
}
public enum FieldType

View File

@ -6,7 +6,7 @@ namespace NzbDrone.Core.Indexers.Newznab
{
public static class NewznabCategoryFieldOptionsConverter
{
public static List<FieldSelectOption> GetFieldSelectOptions(List<NewznabCategory> categories)
public static List<FieldSelectOption<int>> GetFieldSelectOptions(List<NewznabCategory> categories)
{
// Categories not relevant for Sonarr
var ignoreCategories = new[] { 1000, 3000, 4000, 6000, 7000 };
@ -14,7 +14,7 @@ public static List<FieldSelectOption> GetFieldSelectOptions(List<NewznabCategory
// And maybe relevant for specific users
var unimportantCategories = new[] { 0, 2000 };
var result = new List<FieldSelectOption>();
var result = new List<FieldSelectOption<int>>();
if (categories == null)
{
@ -41,7 +41,7 @@ public static List<FieldSelectOption> GetFieldSelectOptions(List<NewznabCategory
foreach (var category in categories.Where(cat => !ignoreCategories.Contains(cat.Id)).OrderBy(cat => unimportantCategories.Contains(cat.Id)).ThenBy(cat => cat.Id))
{
result.Add(new FieldSelectOption
result.Add(new FieldSelectOption<int>
{
Value = category.Id,
Name = category.Name,
@ -52,7 +52,7 @@ public static List<FieldSelectOption> GetFieldSelectOptions(List<NewznabCategory
{
foreach (var subcat in category.Subcategories.OrderBy(cat => cat.Id))
{
result.Add(new FieldSelectOption
result.Add(new FieldSelectOption<int>
{
Value = subcat.Id,
Name = subcat.Name,

View File

@ -1361,6 +1361,8 @@
"NotificationsNtfyValidationAuthorizationRequired": "Authorization is required",
"NotificationsPlexSettingsAuthToken": "Auth Token",
"NotificationsPlexSettingsAuthenticateWithPlexTv": "Authenticate with Plex.tv",
"NotificationsPlexSettingsServer": "Server",
"NotificationsPlexSettingsServerHelpText": "Select server from plex.tv account after authenticating",
"NotificationsPlexValidationNoTvLibraryFound": "At least one TV library is required",
"NotificationsPushBulletSettingSenderId": "Sender ID",
"NotificationsPushBulletSettingSenderIdHelpText": "The device ID to send notifications from, use device_iden in the device's URL on pushbullet.com (leave blank to send from yourself)",

View File

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Net;
using NLog;
using NzbDrone.Common.EnvironmentInfo;
@ -12,6 +13,7 @@ public interface IPlexTvProxy
{
string GetAuthToken(string clientIdentifier, int pinId);
bool Ping(string clientIdentifier, string authToken);
List<PlexTvResource> GetResources(string clientIdentifier, string authToken);
}
public class PlexTvProxy : IPlexTvProxy
@ -62,6 +64,33 @@ public bool Ping(string clientIdentifier, string authToken)
return false;
}
public List<PlexTvResource> GetResources(string clientIdentifier, string authToken)
{
try
{
// Allows us to tell plex.tv that we're still active and tokens should not be expired.
var request = BuildRequest(clientIdentifier);
request.ResourceUrl = "/api/v2/resources";
request.AddQueryParam("includeHttps", 1);
request.AddQueryParam("clientID", clientIdentifier);
request.AddQueryParam("X-Plex-Token", authToken);
if (Json.TryDeserialize<List<PlexTvResource>>(ProcessRequest(request), out var response))
{
return response;
}
}
catch (Exception e)
{
// Catch all exceptions and log at trace, this information could be interesting in debugging, but expired tokens will be handled elsewhere.
_logger.Trace(e, "Unable to ping plex.tv");
}
return new List<PlexTvResource>();
}
private HttpRequestBuilder BuildRequest(string clientIdentifier)
{
var requestBuilder = new HttpRequestBuilder("https://plex.tv")

View File

@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using NzbDrone.Common.Extensions;
namespace NzbDrone.Core.Notifications.Plex.PlexTv
{
public class PlexTvResource
{
public string Name { get; set; }
public bool Owned { get; set; }
public List<PlexTvResourceConnection> Connections { get; set; }
[JsonProperty("provides")]
public string ProvidesRaw { get; set; }
[JsonIgnore]
public List<string> Provides => ProvidesRaw.Split(",").ToList();
}
public class PlexTvResourceConnection
{
public string Uri { get; set; }
public string Protocol { get; set; }
public string Address { get; set; }
public int Port { get; set; }
public bool Local { get; set; }
public string Host => Uri.IsNullOrWhiteSpace() ? Address : new Uri(Uri).Host;
}
}

View File

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using NzbDrone.Common.Cache;
@ -14,6 +15,7 @@ public interface IPlexTvService
PlexTvSignInUrlResponse GetSignInUrl(string callbackUrl, int pinId, string pinCode);
string GetAuthToken(int pinId);
void Ping(string authToken);
List<PlexTvResource> GetServers(string authToken);
HttpRequest GetWatchlist(string authToken, int pageSize, int pageOffset);
}
@ -93,6 +95,16 @@ public void Ping(string authToken)
_cache.Get(authToken, () => _proxy.Ping(_configService.PlexClientIdentifier, authToken), TimeSpan.FromHours(24));
}
public List<PlexTvResource> GetServers(string authToken)
{
Ping(authToken);
var clientIdentifier = _configService.PlexClientIdentifier;
var resources = _proxy.GetResources(clientIdentifier, authToken);
return resources.Where(r => r.Owned && r.Provides.Contains("server")).ToList();
}
public HttpRequest GetWatchlist(string authToken, int pageSize, int pageOffset)
{
Ping(authToken);

View File

@ -5,6 +5,7 @@
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Exceptions;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Notifications.Plex.PlexTv;
@ -193,6 +194,79 @@ public override object RequestAction(string action, IDictionary<string, string>
};
}
if (action == "servers")
{
Settings.Validate().Filter("AuthToken").ThrowOnError();
if (Settings.AuthToken.IsNullOrWhiteSpace())
{
return new { };
}
var servers = _plexTvService.GetServers(Settings.AuthToken);
var options = servers.SelectMany(s =>
{
var result = new List<FieldSelectStringOption>();
// result.Add(new FieldSelectStringOption
// {
// Value = s.Name,
// Name = s.Name,
// IsDisabled = true
// });
s.Connections.ForEach(c =>
{
var isSecure = c.Protocol == "https";
var additionalProperties = new Dictionary<string, object>();
var hints = new List<string>();
additionalProperties.Add("host", c.Host);
additionalProperties.Add("port", c.Port);
additionalProperties.Add("useSsl", isSecure);
hints.Add(c.Local ? "Local" : "Remote");
if (isSecure)
{
hints.Add("Secure");
}
result.Add(new FieldSelectStringOption
{
Value = c.Uri,
Name = $"{s.Name} ({c.Host})",
Hint = string.Join(", ", hints),
AdditionalProperties = additionalProperties
});
if (isSecure)
{
var uri = $"http://{c.Address}:{c.Port}";
var insecureAdditionalProperties = new Dictionary<string, object>();
insecureAdditionalProperties.Add("host", c.Address);
insecureAdditionalProperties.Add("port", c.Port);
insecureAdditionalProperties.Add("useSsl", false);
result.Add(new FieldSelectStringOption
{
Value = uri,
Name = $"{s.Name} ({c.Address})",
Hint = c.Local ? "Local" : "Remote",
AdditionalProperties = insecureAdditionalProperties
});
}
});
return result;
});
return new
{
options
};
}
return new { };
}
}

View File

@ -1,4 +1,5 @@
using FluentValidation;
using Newtonsoft.Json;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Validation;
@ -22,40 +23,45 @@ public class PlexServerSettings : NotificationSettingsBase<PlexServerSettings>
public PlexServerSettings()
{
Host = "";
Port = 32400;
UpdateLibrary = true;
SignIn = "startOAuth";
}
[FieldDefinition(0, Label = "Host")]
[JsonIgnore]
[FieldDefinition(0, Label = "NotificationsPlexSettingsServer", Type = FieldType.Select, SelectOptionsProviderAction = "servers", HelpText = "NotificationsPlexSettingsServerHelpText")]
public string Server { get; set; }
[FieldDefinition(1, Label = "Host")]
public string Host { get; set; }
[FieldDefinition(1, Label = "Port")]
[FieldDefinition(2, Label = "Port")]
public int Port { get; set; }
[FieldDefinition(2, Label = "UseSsl", Type = FieldType.Checkbox, HelpText = "NotificationsSettingsUseSslHelpText")]
[FieldDefinition(3, Label = "UseSsl", Type = FieldType.Checkbox, HelpText = "NotificationsSettingsUseSslHelpText")]
[FieldToken(TokenField.HelpText, "UseSsl", "serviceName", "Plex")]
public bool UseSsl { get; set; }
[FieldDefinition(3, Label = "UrlBase", Type = FieldType.Textbox, Advanced = true, HelpText = "ConnectionSettingsUrlBaseHelpText")]
[FieldDefinition(4, Label = "UrlBase", Type = FieldType.Textbox, Advanced = true, HelpText = "ConnectionSettingsUrlBaseHelpText")]
[FieldToken(TokenField.HelpText, "UrlBase", "connectionName", "Plex")]
[FieldToken(TokenField.HelpText, "UrlBase", "url", "http://[host]:[port]/[urlBase]/plex")]
public string UrlBase { get; set; }
[FieldDefinition(4, Label = "NotificationsPlexSettingsAuthToken", Type = FieldType.Textbox, Privacy = PrivacyLevel.ApiKey, Advanced = true)]
[FieldDefinition(5, Label = "NotificationsPlexSettingsAuthToken", Type = FieldType.Textbox, Privacy = PrivacyLevel.ApiKey, Advanced = true)]
public string AuthToken { get; set; }
[FieldDefinition(5, Label = "NotificationsPlexSettingsAuthenticateWithPlexTv", Type = FieldType.OAuth)]
[FieldDefinition(6, Label = "NotificationsPlexSettingsAuthenticateWithPlexTv", Type = FieldType.OAuth)]
public string SignIn { get; set; }
[FieldDefinition(6, Label = "NotificationsSettingsUpdateLibrary", Type = FieldType.Checkbox)]
[FieldDefinition(7, Label = "NotificationsSettingsUpdateLibrary", Type = FieldType.Checkbox)]
public bool UpdateLibrary { get; set; }
[FieldDefinition(7, Label = "NotificationsSettingsUpdateMapPathsFrom", Type = FieldType.Textbox, Advanced = true, HelpText = "NotificationsSettingsUpdateMapPathsFromHelpText")]
[FieldDefinition(8, Label = "NotificationsSettingsUpdateMapPathsFrom", Type = FieldType.Textbox, Advanced = true, HelpText = "NotificationsSettingsUpdateMapPathsFromHelpText")]
[FieldToken(TokenField.HelpText, "NotificationsSettingsUpdateMapPathsFrom", "serviceName", "Plex")]
public string MapFrom { get; set; }
[FieldDefinition(8, Label = "NotificationsSettingsUpdateMapPathsTo", Type = FieldType.Textbox, Advanced = true, HelpText = "NotificationsSettingsUpdateMapPathsToHelpText")]
[FieldDefinition(9, Label = "NotificationsSettingsUpdateMapPathsTo", Type = FieldType.Textbox, Advanced = true, HelpText = "NotificationsSettingsUpdateMapPathsToHelpText")]
[FieldToken(TokenField.HelpText, "NotificationsSettingsUpdateMapPathsTo", "serviceName", "Plex")]
public string MapTo { get; set; }