mirror of
https://github.com/lidarr/lidarr.git
synced 2025-07-14 00:53:57 -07:00
Added: Device load support for Pushbullet
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
This commit is contained in:
parent
e41f884153
commit
23bc5b11cf
19 changed files with 439 additions and 27 deletions
8
frontend/src/Components/Form/DeviceInput.css
Normal file
8
frontend/src/Components/Form/DeviceInput.css
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
.deviceInputWrapper {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputContainer {
|
||||||
|
composes: inputContainer from './TagInput.css';
|
||||||
|
composes: hasButton from 'Components/Form/Input.css';
|
||||||
|
}
|
103
frontend/src/Components/Form/DeviceInput.js
Normal file
103
frontend/src/Components/Form/DeviceInput.js
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import FormInputButton from './FormInputButton';
|
||||||
|
import TagInput, { tagShape } from './TagInput';
|
||||||
|
import styles from './DeviceInput.css';
|
||||||
|
|
||||||
|
class DeviceInput extends Component {
|
||||||
|
|
||||||
|
onTagAdd = (device) => {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
onChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
// New tags won't have an ID, only a name.
|
||||||
|
const deviceId = device.id || device.name;
|
||||||
|
|
||||||
|
onChange({
|
||||||
|
name,
|
||||||
|
value: [...value, deviceId]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onTagDelete = ({ index }) => {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
onChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const newValue = value.slice();
|
||||||
|
newValue.splice(index, 1);
|
||||||
|
|
||||||
|
onChange({
|
||||||
|
name,
|
||||||
|
value: newValue
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
className,
|
||||||
|
items,
|
||||||
|
selectedDevices,
|
||||||
|
hasError,
|
||||||
|
hasWarning,
|
||||||
|
isFetching,
|
||||||
|
onRefreshPress
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<TagInput
|
||||||
|
className={styles.inputContainer}
|
||||||
|
tags={selectedDevices}
|
||||||
|
tagList={items}
|
||||||
|
allowNew={true}
|
||||||
|
minQueryLength={0}
|
||||||
|
hasError={hasError}
|
||||||
|
hasWarning={hasWarning}
|
||||||
|
onTagAdd={this.onTagAdd}
|
||||||
|
onTagDelete={this.onTagDelete}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormInputButton
|
||||||
|
onPress={onRefreshPress}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name={icons.REFRESH}
|
||||||
|
isSpinning={isFetching}
|
||||||
|
/>
|
||||||
|
</FormInputButton>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DeviceInput.propTypes = {
|
||||||
|
className: PropTypes.string.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
value: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string])).isRequired,
|
||||||
|
items: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired,
|
||||||
|
selectedDevices: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired,
|
||||||
|
hasError: PropTypes.bool,
|
||||||
|
hasWarning: PropTypes.bool,
|
||||||
|
isFetching: PropTypes.bool.isRequired,
|
||||||
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
onRefreshPress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
DeviceInput.defaultProps = {
|
||||||
|
className: styles.deviceInputWrapper,
|
||||||
|
inputClassName: styles.input
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DeviceInput;
|
99
frontend/src/Components/Form/DeviceInputConnector.js
Normal file
99
frontend/src/Components/Form/DeviceInputConnector.js
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { fetchDevices, clearDevices } from 'Store/Actions/deviceActions';
|
||||||
|
import DeviceInput from './DeviceInput';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state, { value }) => value,
|
||||||
|
(state) => state.devices,
|
||||||
|
(value, devices) => {
|
||||||
|
|
||||||
|
return {
|
||||||
|
...devices,
|
||||||
|
selectedDevices: value.map((valueDevice) => {
|
||||||
|
// Disable equality ESLint rule so we don't need to worry about
|
||||||
|
// a type mismatch between the value items and the device ID.
|
||||||
|
// eslint-disable-next-line eqeqeq
|
||||||
|
const device = devices.items.find((d) => d.id == valueDevice);
|
||||||
|
|
||||||
|
if (device) {
|
||||||
|
return {
|
||||||
|
id: device.id,
|
||||||
|
name: `${device.name} (${device.id})`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: valueDevice,
|
||||||
|
name: `Unknown (${valueDevice})`
|
||||||
|
};
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
dispatchFetchDevices: fetchDevices,
|
||||||
|
dispatchClearDevices: clearDevices
|
||||||
|
};
|
||||||
|
|
||||||
|
class DeviceInputConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidMount = () => {
|
||||||
|
this._populate();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount = () => {
|
||||||
|
// this.props.dispatchClearDevices();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Control
|
||||||
|
|
||||||
|
_populate() {
|
||||||
|
const {
|
||||||
|
provider,
|
||||||
|
providerData,
|
||||||
|
dispatchFetchDevices
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
dispatchFetchDevices({ provider, providerData });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onRefreshPress = () => {
|
||||||
|
this._populate();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<DeviceInput
|
||||||
|
{...this.props}
|
||||||
|
onRefreshPress={this.onRefreshPress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DeviceInputConnector.propTypes = {
|
||||||
|
provider: PropTypes.string.isRequired,
|
||||||
|
providerData: PropTypes.object.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
dispatchFetchDevices: PropTypes.func.isRequired,
|
||||||
|
dispatchClearDevices: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(DeviceInputConnector);
|
|
@ -4,6 +4,7 @@ import { inputTypes } from 'Helpers/Props';
|
||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
import CaptchaInputConnector from './CaptchaInputConnector';
|
import CaptchaInputConnector from './CaptchaInputConnector';
|
||||||
import CheckInput from './CheckInput';
|
import CheckInput from './CheckInput';
|
||||||
|
import DeviceInputConnector from './DeviceInputConnector';
|
||||||
import MonitorAlbumsSelectInput from './MonitorAlbumsSelectInput';
|
import MonitorAlbumsSelectInput from './MonitorAlbumsSelectInput';
|
||||||
import NumberInput from './NumberInput';
|
import NumberInput from './NumberInput';
|
||||||
import OAuthInputConnector from './OAuthInputConnector';
|
import OAuthInputConnector from './OAuthInputConnector';
|
||||||
|
@ -30,6 +31,9 @@ function getComponent(type) {
|
||||||
case inputTypes.CHECK:
|
case inputTypes.CHECK:
|
||||||
return CheckInput;
|
return CheckInput;
|
||||||
|
|
||||||
|
case inputTypes.DEVICE:
|
||||||
|
return DeviceInputConnector;
|
||||||
|
|
||||||
case inputTypes.MONITOR_ALBUMS_SELECT:
|
case inputTypes.MONITOR_ALBUMS_SELECT:
|
||||||
return MonitorAlbumsSelectInput;
|
return MonitorAlbumsSelectInput;
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,8 @@ function getType(type) {
|
||||||
return inputTypes.CAPTCHA;
|
return inputTypes.CAPTCHA;
|
||||||
case 'checkbox':
|
case 'checkbox':
|
||||||
return inputTypes.CHECK;
|
return inputTypes.CHECK;
|
||||||
|
case 'device':
|
||||||
|
return inputTypes.DEVICE;
|
||||||
case 'password':
|
case 'password':
|
||||||
return inputTypes.PASSWORD;
|
return inputTypes.PASSWORD;
|
||||||
case 'number':
|
case 'number':
|
||||||
|
@ -20,6 +22,8 @@ function getType(type) {
|
||||||
return inputTypes.PATH;
|
return inputTypes.PATH;
|
||||||
case 'select':
|
case 'select':
|
||||||
return inputTypes.SELECT;
|
return inputTypes.SELECT;
|
||||||
|
case 'tag':
|
||||||
|
return inputTypes.TEXT_TAG;
|
||||||
case 'textbox':
|
case 'textbox':
|
||||||
return inputTypes.TEXT;
|
return inputTypes.TEXT;
|
||||||
case 'oauth':
|
case 'oauth':
|
||||||
|
|
|
@ -54,7 +54,7 @@ class TagInput extends Component {
|
||||||
return value.length >= this.props.minQueryLength;
|
return value.length >= this.props.minQueryLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderSuggestion({ name }, { query }) {
|
renderSuggestion({ name }) {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -202,6 +202,8 @@ class TagInput extends Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
|
className,
|
||||||
|
inputClassName,
|
||||||
placeholder,
|
placeholder,
|
||||||
hasError,
|
hasError,
|
||||||
hasWarning
|
hasWarning
|
||||||
|
@ -214,7 +216,7 @@ class TagInput extends Component {
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const inputProps = {
|
const inputProps = {
|
||||||
className: styles.input,
|
className: inputClassName,
|
||||||
name,
|
name,
|
||||||
value,
|
value,
|
||||||
placeholder,
|
placeholder,
|
||||||
|
@ -228,7 +230,7 @@ class TagInput extends Component {
|
||||||
|
|
||||||
const theme = {
|
const theme = {
|
||||||
container: classNames(
|
container: classNames(
|
||||||
styles.inputContainer,
|
className,
|
||||||
isFocused && styles.isFocused,
|
isFocused && styles.isFocused,
|
||||||
hasError && styles.hasError,
|
hasError && styles.hasError,
|
||||||
hasWarning && styles.hasWarning,
|
hasWarning && styles.hasWarning,
|
||||||
|
@ -266,6 +268,8 @@ export const tagShape = {
|
||||||
};
|
};
|
||||||
|
|
||||||
TagInput.propTypes = {
|
TagInput.propTypes = {
|
||||||
|
className: PropTypes.string.isRequired,
|
||||||
|
inputClassName: PropTypes.string.isRequired,
|
||||||
tags: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired,
|
tags: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired,
|
||||||
tagList: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired,
|
tagList: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired,
|
||||||
allowNew: PropTypes.bool.isRequired,
|
allowNew: PropTypes.bool.isRequired,
|
||||||
|
@ -281,6 +285,8 @@ TagInput.propTypes = {
|
||||||
};
|
};
|
||||||
|
|
||||||
TagInput.defaultProps = {
|
TagInput.defaultProps = {
|
||||||
|
className: styles.inputContainer,
|
||||||
|
inputClassName: styles.input,
|
||||||
allowNew: true,
|
allowNew: true,
|
||||||
kind: kinds.INFO,
|
kind: kinds.INFO,
|
||||||
placeholder: '',
|
placeholder: '',
|
||||||
|
|
|
@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
import isString from 'Utilities/String/isString';
|
||||||
import split from 'Utilities/String/split';
|
import split from 'Utilities/String/split';
|
||||||
import TagInput from './TagInput';
|
import TagInput from './TagInput';
|
||||||
|
|
||||||
|
@ -10,8 +11,11 @@ function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state, { value }) => value,
|
(state, { value }) => value,
|
||||||
(tags) => {
|
(tags) => {
|
||||||
|
const isArray = !isString(tags);
|
||||||
|
const tagsArray = isArray ? tags :split(tags);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tags: split(tags).reduce((result, tag) => {
|
tags: tagsArray.reduce((result, tag) => {
|
||||||
if (tag) {
|
if (tag) {
|
||||||
result.push({
|
result.push({
|
||||||
id: tag,
|
id: tag,
|
||||||
|
@ -20,7 +24,8 @@ function createMapStateToProps() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}, [])
|
}, []),
|
||||||
|
isArray
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -35,10 +40,11 @@ class TextTagInputConnector extends Component {
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
value,
|
value,
|
||||||
|
isArray,
|
||||||
onChange
|
onChange
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const newValue = split(value);
|
const newValue = isArray ? [...value] : split(value);
|
||||||
newValue.push(tag.name);
|
newValue.push(tag.name);
|
||||||
|
|
||||||
onChange({ name, value: newValue.join(',') });
|
onChange({ name, value: newValue.join(',') });
|
||||||
|
@ -48,10 +54,11 @@ class TextTagInputConnector extends Component {
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
value,
|
value,
|
||||||
|
isArray,
|
||||||
onChange
|
onChange
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const newValue = split(value);
|
const newValue = isArray ? [...value] : split(value);
|
||||||
newValue.splice(index, 1);
|
newValue.splice(index, 1);
|
||||||
|
|
||||||
onChange({
|
onChange({
|
||||||
|
@ -77,7 +84,8 @@ class TextTagInputConnector extends Component {
|
||||||
|
|
||||||
TextTagInputConnector.propTypes = {
|
TextTagInputConnector.propTypes = {
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
value: PropTypes.string,
|
value: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]),
|
||||||
|
isArray: PropTypes.bool.isRequired,
|
||||||
onChange: PropTypes.func.isRequired
|
onChange: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
export const CAPTCHA = 'captcha';
|
export const CAPTCHA = 'captcha';
|
||||||
export const CHECK = 'check';
|
export const CHECK = 'check';
|
||||||
|
export const DEVICE = 'device';
|
||||||
export const MONITOR_ALBUMS_SELECT = 'monitorAlbumsSelect';
|
export const MONITOR_ALBUMS_SELECT = 'monitorAlbumsSelect';
|
||||||
export const NUMBER = 'number';
|
export const NUMBER = 'number';
|
||||||
export const OAUTH = 'oauth';
|
export const OAUTH = 'oauth';
|
||||||
|
@ -19,6 +20,7 @@ export const TEXT_TAG = 'textTag';
|
||||||
export const all = [
|
export const all = [
|
||||||
CAPTCHA,
|
CAPTCHA,
|
||||||
CHECK,
|
CHECK,
|
||||||
|
DEVICE,
|
||||||
MONITOR_ALBUMS_SELECT,
|
MONITOR_ALBUMS_SELECT,
|
||||||
NUMBER,
|
NUMBER,
|
||||||
OAUTH,
|
OAUTH,
|
||||||
|
|
83
frontend/src/Store/Actions/deviceActions.js
Normal file
83
frontend/src/Store/Actions/deviceActions.js
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
import { createAction } from 'redux-actions';
|
||||||
|
import requestAction from 'Utilities/requestAction';
|
||||||
|
import updateSectionState from 'Utilities/State/updateSectionState';
|
||||||
|
import { createThunk, handleThunks } from 'Store/thunks';
|
||||||
|
import createHandleActions from './Creators/createHandleActions';
|
||||||
|
import { set } from './baseActions';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Variables
|
||||||
|
|
||||||
|
export const section = 'devices';
|
||||||
|
|
||||||
|
//
|
||||||
|
// State
|
||||||
|
|
||||||
|
export const defaultState = {
|
||||||
|
items: [],
|
||||||
|
isFetching: false,
|
||||||
|
isPopulated: false,
|
||||||
|
error: false
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Actions Types
|
||||||
|
|
||||||
|
export const FETCH_DEVICES = 'devices/fetchDevices';
|
||||||
|
export const CLEAR_DEVICES = 'devices/clearDevices';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Action Creators
|
||||||
|
|
||||||
|
export const fetchDevices = createThunk(FETCH_DEVICES);
|
||||||
|
export const clearDevices = createAction(CLEAR_DEVICES);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Action Handlers
|
||||||
|
|
||||||
|
export const actionHandlers = handleThunks({
|
||||||
|
|
||||||
|
[FETCH_DEVICES]: function(getState, payload, dispatch) {
|
||||||
|
const actionPayload = {
|
||||||
|
action: 'getDevices',
|
||||||
|
...payload
|
||||||
|
};
|
||||||
|
|
||||||
|
dispatch(set({
|
||||||
|
section,
|
||||||
|
isFetching: true
|
||||||
|
}));
|
||||||
|
|
||||||
|
const promise = requestAction(actionPayload);
|
||||||
|
|
||||||
|
promise.done((data) => {
|
||||||
|
dispatch(set({
|
||||||
|
section,
|
||||||
|
isFetching: false,
|
||||||
|
isPopulated: true,
|
||||||
|
error: null,
|
||||||
|
items: data.devices || []
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
promise.fail((xhr) => {
|
||||||
|
dispatch(set({
|
||||||
|
section,
|
||||||
|
isFetching: false,
|
||||||
|
isPopulated: false,
|
||||||
|
error: xhr
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//
|
||||||
|
// Reducers
|
||||||
|
|
||||||
|
export const reducers = createHandleActions({
|
||||||
|
|
||||||
|
[CLEAR_DEVICES]: function(state) {
|
||||||
|
return updateSectionState(state, section, defaultState);
|
||||||
|
}
|
||||||
|
|
||||||
|
}, defaultState, section);
|
|
@ -2,6 +2,7 @@ import * as addArtist from './addArtistActions';
|
||||||
import * as app from './appActions';
|
import * as app from './appActions';
|
||||||
import * as blacklist from './blacklistActions';
|
import * as blacklist from './blacklistActions';
|
||||||
import * as captcha from './captchaActions';
|
import * as captcha from './captchaActions';
|
||||||
|
import * as devices from './deviceActions';
|
||||||
import * as calendar from './calendarActions';
|
import * as calendar from './calendarActions';
|
||||||
import * as commands from './commandActions';
|
import * as commands from './commandActions';
|
||||||
import * as albums from './albumActions';
|
import * as albums from './albumActions';
|
||||||
|
@ -34,6 +35,7 @@ export default [
|
||||||
captcha,
|
captcha,
|
||||||
calendar,
|
calendar,
|
||||||
commands,
|
commands,
|
||||||
|
devices,
|
||||||
albums,
|
albums,
|
||||||
trackFiles,
|
trackFiles,
|
||||||
albumHistory,
|
albumHistory,
|
||||||
|
|
|
@ -34,6 +34,7 @@ namespace NzbDrone.Core.Annotations
|
||||||
Action,
|
Action,
|
||||||
Url,
|
Url,
|
||||||
Captcha,
|
Captcha,
|
||||||
OAuth
|
OAuth,
|
||||||
|
Device
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using FluentValidation.Results;
|
using FluentValidation.Results;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
|
using NzbDrone.Core.Validation;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Notifications.PushBullet
|
namespace NzbDrone.Core.Notifications.PushBullet
|
||||||
{
|
{
|
||||||
|
@ -16,7 +19,6 @@ namespace NzbDrone.Core.Notifications.PushBullet
|
||||||
public override string Name => "Pushbullet";
|
public override string Name => "Pushbullet";
|
||||||
public override string Link => "https://www.pushbullet.com/";
|
public override string Link => "https://www.pushbullet.com/";
|
||||||
|
|
||||||
|
|
||||||
public override void OnGrab(GrabMessage grabMessage)
|
public override void OnGrab(GrabMessage grabMessage)
|
||||||
{
|
{
|
||||||
_proxy.SendNotification(ALBUM_GRABBED_TITLE_BRANDED, grabMessage.Message, Settings);
|
_proxy.SendNotification(ALBUM_GRABBED_TITLE_BRANDED, grabMessage.Message, Settings);
|
||||||
|
@ -40,5 +42,36 @@ namespace NzbDrone.Core.Notifications.PushBullet
|
||||||
|
|
||||||
return new ValidationResult(failures);
|
return new ValidationResult(failures);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override object RequestAction(string action, IDictionary<string, string> query)
|
||||||
|
{
|
||||||
|
if (action == "getDevices")
|
||||||
|
{
|
||||||
|
// Return early if there is not an API key
|
||||||
|
if (Settings.ApiKey.IsNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
devices = new List<object>()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Settings.Validate().Filter("ApiKey").ThrowOnError();
|
||||||
|
var devices = _proxy.GetDevices(Settings);
|
||||||
|
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
devices = devices.Where(d => d.Nickname.IsNotNullOrWhiteSpace())
|
||||||
|
.OrderBy(d => d.Nickname, StringComparer.InvariantCultureIgnoreCase)
|
||||||
|
.Select(d => new
|
||||||
|
{
|
||||||
|
id = d.Id,
|
||||||
|
name = d.Nickname
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return new { };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Notifications.PushBullet
|
||||||
|
{
|
||||||
|
public class PushBulletDevice
|
||||||
|
{
|
||||||
|
[JsonProperty(PropertyName = "Iden")]
|
||||||
|
public string Id { get; set; }
|
||||||
|
|
||||||
|
public string Nickname { get; set; }
|
||||||
|
public string Manufacturer { get; set; }
|
||||||
|
public string Model { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Notifications.PushBullet
|
||||||
|
{
|
||||||
|
public class PushBulletDevicesResponse
|
||||||
|
{
|
||||||
|
public List<PushBulletDevice> Devices { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using FluentValidation.Results;
|
using FluentValidation.Results;
|
||||||
|
@ -6,6 +7,7 @@ using NLog;
|
||||||
using RestSharp;
|
using RestSharp;
|
||||||
using NzbDrone.Core.Rest;
|
using NzbDrone.Core.Rest;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
|
using NzbDrone.Common.Serializer;
|
||||||
using RestSharp.Authenticators;
|
using RestSharp.Authenticators;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Notifications.PushBullet
|
namespace NzbDrone.Core.Notifications.PushBullet
|
||||||
|
@ -13,13 +15,15 @@ namespace NzbDrone.Core.Notifications.PushBullet
|
||||||
public interface IPushBulletProxy
|
public interface IPushBulletProxy
|
||||||
{
|
{
|
||||||
void SendNotification(string title, string message, PushBulletSettings settings);
|
void SendNotification(string title, string message, PushBulletSettings settings);
|
||||||
|
List<PushBulletDevice> GetDevices(PushBulletSettings settings);
|
||||||
ValidationFailure Test(PushBulletSettings settings);
|
ValidationFailure Test(PushBulletSettings settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class PushBulletProxy : IPushBulletProxy
|
public class PushBulletProxy : IPushBulletProxy
|
||||||
{
|
{
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
private const string URL = "https://api.pushbullet.com/v2/pushes";
|
private const string PUSH_URL = "https://api.pushbullet.com/v2/pushes";
|
||||||
|
private const string DEVICE_URL = "https://api.pushbullet.com/v2/devices";
|
||||||
|
|
||||||
public PushBulletProxy(Logger logger)
|
public PushBulletProxy(Logger logger)
|
||||||
{
|
{
|
||||||
|
@ -88,6 +92,30 @@ namespace NzbDrone.Core.Notifications.PushBullet
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<PushBulletDevice> GetDevices(PushBulletSettings settings)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var client = RestClientFactory.BuildClient(DEVICE_URL);
|
||||||
|
var request = new RestRequest(Method.GET);
|
||||||
|
|
||||||
|
client.Authenticator = new HttpBasicAuthenticator(settings.ApiKey, string.Empty);
|
||||||
|
var response = client.ExecuteAndValidate(request);
|
||||||
|
|
||||||
|
return Json.Deserialize<PushBulletDevicesResponse>(response.Content).Devices;
|
||||||
|
}
|
||||||
|
catch (RestException ex)
|
||||||
|
{
|
||||||
|
if (ex.Response.StatusCode == HttpStatusCode.Unauthorized)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, "Access token is invalid");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new List<PushBulletDevice>();
|
||||||
|
}
|
||||||
|
|
||||||
public ValidationFailure Test(PushBulletSettings settings)
|
public ValidationFailure Test(PushBulletSettings settings)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
@ -147,7 +175,7 @@ namespace NzbDrone.Core.Notifications.PushBullet
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var client = RestClientFactory.BuildClient(URL);
|
var client = RestClientFactory.BuildClient(PUSH_URL);
|
||||||
|
|
||||||
request.AddParameter("type", "note");
|
request.AddParameter("type", "note");
|
||||||
request.AddParameter("title", title);
|
request.AddParameter("title", title);
|
||||||
|
@ -165,7 +193,7 @@ namespace NzbDrone.Core.Notifications.PushBullet
|
||||||
{
|
{
|
||||||
if (ex.Response.StatusCode == HttpStatusCode.Unauthorized)
|
if (ex.Response.StatusCode == HttpStatusCode.Unauthorized)
|
||||||
{
|
{
|
||||||
_logger.Error(ex, "API Key is invalid");
|
_logger.Error(ex, "Access token is invalid");
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
using NzbDrone.Core.Annotations;
|
using NzbDrone.Core.Annotations;
|
||||||
using NzbDrone.Core.ThingiProvider;
|
using NzbDrone.Core.ThingiProvider;
|
||||||
|
@ -20,14 +20,14 @@ namespace NzbDrone.Core.Notifications.PushBullet
|
||||||
|
|
||||||
public PushBulletSettings()
|
public PushBulletSettings()
|
||||||
{
|
{
|
||||||
DeviceIds = new string[]{};
|
DeviceIds = new string[] { };
|
||||||
ChannelTags = new string[]{};
|
ChannelTags = new string[] { };
|
||||||
}
|
}
|
||||||
|
|
||||||
[FieldDefinition(0, Label = "API Key", HelpLink = "https://www.pushbullet.com/")]
|
[FieldDefinition(0, Label = "Access Token", HelpLink = "https://www.pushbullet.com/#settings/account")]
|
||||||
public string ApiKey { get; set; }
|
public string ApiKey { get; set; }
|
||||||
|
|
||||||
[FieldDefinition(1, Label = "Device IDs", HelpText = "List of device IDs, use device_iden in the device's URL on pushbullet.com (leave blank to send to all devices)", Type = FieldType.Tag)]
|
[FieldDefinition(1, Label = "Device IDs", HelpText = "List of device IDs (leave blank to send to all devices)", Type = FieldType.Device)]
|
||||||
public IEnumerable<string> DeviceIds { get; set; }
|
public IEnumerable<string> DeviceIds { get; set; }
|
||||||
|
|
||||||
[FieldDefinition(2, Label = "Channel Tags", HelpText = "List of Channel Tags to send notifications to", Type = FieldType.Tag)]
|
[FieldDefinition(2, Label = "Channel Tags", HelpText = "List of Channel Tags to send notifications to", Type = FieldType.Tag)]
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using FluentValidation.Results;
|
using FluentValidation.Results;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
|
@ -29,6 +29,7 @@ namespace NzbDrone.Core.Notifications.Pushover
|
||||||
var request = new RestRequest(Method.POST);
|
var request = new RestRequest(Method.POST);
|
||||||
request.AddParameter("token", settings.ApiKey);
|
request.AddParameter("token", settings.ApiKey);
|
||||||
request.AddParameter("user", settings.UserKey);
|
request.AddParameter("user", settings.UserKey);
|
||||||
|
request.AddParameter("device", string.Join(",", settings.Devices));
|
||||||
request.AddParameter("title", title);
|
request.AddParameter("title", title);
|
||||||
request.AddParameter("message", message);
|
request.AddParameter("message", message);
|
||||||
request.AddParameter("priority", settings.Priority);
|
request.AddParameter("priority", settings.Priority);
|
|
@ -1,7 +1,8 @@
|
||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
using NzbDrone.Core.Annotations;
|
using NzbDrone.Core.Annotations;
|
||||||
using NzbDrone.Core.ThingiProvider;
|
using NzbDrone.Core.ThingiProvider;
|
||||||
using NzbDrone.Core.Validation;
|
using NzbDrone.Core.Validation;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Notifications.Pushover
|
namespace NzbDrone.Core.Notifications.Pushover
|
||||||
{
|
{
|
||||||
|
@ -22,25 +23,29 @@ namespace NzbDrone.Core.Notifications.Pushover
|
||||||
public PushoverSettings()
|
public PushoverSettings()
|
||||||
{
|
{
|
||||||
Priority = 0;
|
Priority = 0;
|
||||||
|
Devices = new string[] { };
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: Get Pushover to change our app name (or create a new app) when we have a new logo
|
//TODO: Get Pushover to change our app name (or create a new app) when we have a new logo
|
||||||
[FieldDefinition(0, Label = "API Key", HelpLink = "https://pushover.net/apps/clone/nzbdrone")]
|
[FieldDefinition(0, Label = "API Key", HelpLink = "https://pushover.net/apps/clone/lidarr")]
|
||||||
public string ApiKey { get; set; }
|
public string ApiKey { get; set; }
|
||||||
|
|
||||||
[FieldDefinition(1, Label = "User Key", HelpLink = "https://pushover.net/")]
|
[FieldDefinition(1, Label = "User Key", HelpLink = "https://pushover.net/")]
|
||||||
public string UserKey { get; set; }
|
public string UserKey { get; set; }
|
||||||
|
|
||||||
[FieldDefinition(2, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(PushoverPriority) )]
|
[FieldDefinition(2, Label = "Devices", HelpText = "List of device names (leave blank to send to all devices)", Type = FieldType.Tag)]
|
||||||
|
public IEnumerable<string> Devices { get; set; }
|
||||||
|
|
||||||
|
[FieldDefinition(3, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(PushoverPriority))]
|
||||||
public int Priority { get; set; }
|
public int Priority { get; set; }
|
||||||
|
|
||||||
[FieldDefinition(3, Label = "Retry", Type = FieldType.Textbox, HelpText = "Interval to retry Emergency alerts, minimum 30 seconds")]
|
[FieldDefinition(4, Label = "Retry", Type = FieldType.Textbox, HelpText = "Interval to retry Emergency alerts, minimum 30 seconds")]
|
||||||
public int Retry { get; set; }
|
public int Retry { get; set; }
|
||||||
|
|
||||||
[FieldDefinition(4, Label = "Expire", Type = FieldType.Textbox, HelpText = "Maximum time to retry Emergency alerts, maximum 86400 seconds")]
|
[FieldDefinition(5, Label = "Expire", Type = FieldType.Textbox, HelpText = "Maximum time to retry Emergency alerts, maximum 86400 seconds")]
|
||||||
public int Expire { get; set; }
|
public int Expire { get; set; }
|
||||||
|
|
||||||
[FieldDefinition(5, Label = "Sound", Type = FieldType.Textbox, HelpText = "Notification sound, leave blank to use the default", HelpLink = "https://pushover.net/api#sounds")]
|
[FieldDefinition(6, Label = "Sound", Type = FieldType.Textbox, HelpText = "Notification sound, leave blank to use the default", HelpLink = "https://pushover.net/api#sounds")]
|
||||||
public string Sound { get; set; }
|
public string Sound { get; set; }
|
||||||
|
|
||||||
public bool IsValid => !string.IsNullOrWhiteSpace(UserKey) && Priority >= -1 && Priority <= 2;
|
public bool IsValid => !string.IsNullOrWhiteSpace(UserKey) && Priority >= -1 && Priority <= 2;
|
||||||
|
|
|
@ -900,7 +900,10 @@
|
||||||
<Compile Include="Notifications\Plex\HomeTheater\PlexClientService.cs" />
|
<Compile Include="Notifications\Plex\HomeTheater\PlexClientService.cs" />
|
||||||
<Compile Include="Notifications\Plex\Server\PlexServer.cs" />
|
<Compile Include="Notifications\Plex\Server\PlexServer.cs" />
|
||||||
<Compile Include="Notifications\Plex\Server\PlexServerProxy.cs" />
|
<Compile Include="Notifications\Plex\Server\PlexServerProxy.cs" />
|
||||||
|
<Compile Include="Notifications\PushBullet\PushBulletDevice.cs" />
|
||||||
|
<Compile Include="Notifications\PushBullet\PushBulletDevicesResponse.cs" />
|
||||||
<Compile Include="Notifications\PushBullet\PushBulletException.cs" />
|
<Compile Include="Notifications\PushBullet\PushBulletException.cs" />
|
||||||
|
<Compile Include="Notifications\Pushover\PushoverProxy.cs" />
|
||||||
<Compile Include="Notifications\Slack\Payloads\Attachment.cs" />
|
<Compile Include="Notifications\Slack\Payloads\Attachment.cs" />
|
||||||
<Compile Include="Notifications\Slack\Payloads\SlackPayload.cs" />
|
<Compile Include="Notifications\Slack\Payloads\SlackPayload.cs" />
|
||||||
<Compile Include="Notifications\Slack\Slack.cs" />
|
<Compile Include="Notifications\Slack\Slack.cs" />
|
||||||
|
@ -1027,7 +1030,6 @@
|
||||||
<Compile Include="Notifications\Pushover\InvalidResponseException.cs" />
|
<Compile Include="Notifications\Pushover\InvalidResponseException.cs" />
|
||||||
<Compile Include="Notifications\Pushover\Pushover.cs" />
|
<Compile Include="Notifications\Pushover\Pushover.cs" />
|
||||||
<Compile Include="Notifications\Pushover\PushoverPriority.cs" />
|
<Compile Include="Notifications\Pushover\PushoverPriority.cs" />
|
||||||
<Compile Include="Notifications\Pushover\PushoverService.cs" />
|
|
||||||
<Compile Include="Notifications\Pushover\PushoverSettings.cs" />
|
<Compile Include="Notifications\Pushover\PushoverSettings.cs" />
|
||||||
<Compile Include="Notifications\Xbmc\XbmcJsonException.cs" />
|
<Compile Include="Notifications\Xbmc\XbmcJsonException.cs" />
|
||||||
<Compile Include="Notifications\Xbmc\HttpApiProvider.cs" />
|
<Compile Include="Notifications\Xbmc\HttpApiProvider.cs" />
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue