New: Spotify integration

Import playlists, followed artists and saved albums
This commit is contained in:
ta264 2019-07-24 21:40:30 +01:00
parent 2f1290d488
commit d075ea3625
18 changed files with 892 additions and 1 deletions

View file

@ -6,6 +6,7 @@ import AutoCompleteInput from './AutoCompleteInput';
import CaptchaInputConnector from './CaptchaInputConnector';
import CheckInput from './CheckInput';
import DeviceInputConnector from './DeviceInputConnector';
import PlaylistInputConnector from './PlaylistInputConnector';
import KeyValueListInput from './KeyValueListInput';
import MonitorAlbumsSelectInput from './MonitorAlbumsSelectInput';
import NumberInput from './NumberInput';
@ -39,6 +40,9 @@ function getComponent(type) {
case inputTypes.DEVICE:
return DeviceInputConnector;
case inputTypes.PLAYLIST:
return PlaylistInputConnector;
case inputTypes.KEY_VALUE_LIST:
return KeyValueListInput;

View file

@ -0,0 +1,9 @@
.playlistInputWrapper {
display: flex;
flex-direction: column;
}
.input {
composes: input from '~./TagInput.css';
composes: hasButton from '~Components/Form/Input.css';
}

View file

@ -0,0 +1,186 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import tagShape from 'Helpers/Props/Shapes/tagShape';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import TableRow from 'Components/Table/TableRow';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import styles from './PlaylistInput.css';
const columns = [
{
name: 'name',
label: 'Playlist',
isSortable: false,
isVisible: true
}
];
class PlaylistInput extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
const initialSelection = _.mapValues(_.keyBy(props.value), () => true);
this.state = {
allSelected: false,
allUnselected: false,
selectedState: initialSelection
};
}
componentDidUpdate(prevProps, prevState) {
const {
name,
onChange
} = this.props;
const oldSelected = getSelectedIds(prevState.selectedState, { parseIds: false }).sort();
const newSelected = this.getSelectedIds().sort();
if (!_.isEqual(oldSelected, newSelected)) {
onChange({
name,
value: newSelected
});
}
}
//
// Control
getSelectedIds = () => {
return getSelectedIds(this.state.selectedState, { parseIds: false });
}
//
// Listeners
onSelectAllChange = ({ value }) => {
this.setState(selectAll(this.state.selectedState, value));
}
onSelectedChange = ({ id, value, shiftKey = false }) => {
this.setState((state, props) => {
return toggleSelected(state, props.items, id, value, shiftKey);
});
}
//
// Render
render() {
const {
className,
items,
user,
isFetching,
isPopulated
} = this.props;
const {
allSelected,
allUnselected,
selectedState
} = this.state;
return (
<div className={className}>
{
isFetching &&
<LoadingIndicator />
}
{
!isPopulated && !isFetching &&
<div>
Authenticate with spotify to retrieve playlists to import.
</div>
}
{
isPopulated && !isFetching && !user &&
<div>
Could not retrieve data from Spotify. Try re-authenticating.
</div>
}
{
isPopulated && !isFetching && user && !items.length &&
<div>
No playlists found for Spotify user {user}.
</div>
}
{
isPopulated && !isFetching && user && !!items.length &&
<div className={className}>
Select playlists to import from Spotify user {user}.
<Table
columns={columns}
selectAll={true}
allSelected={allSelected}
allUnselected={allUnselected}
onSelectAllChange={this.onSelectAllChange}
>
<TableBody>
{
items.map((item) => {
return (
<TableRow
key={item.id}
>
<TableSelectCell
id={item.id}
isSelected={selectedState[item.id]}
onSelectedChange={this.onSelectedChange}
/>
<TableRowCell
className={styles.relativePath}
title={item.name}
>
{item.name}
</TableRowCell>
</TableRow>
);
})
}
</TableBody>
</Table>
</div>
}
</div>
);
}
}
PlaylistInput.propTypes = {
className: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
value: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string])).isRequired,
user: PropTypes.string.isRequired,
items: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired,
hasError: PropTypes.bool,
hasWarning: PropTypes.bool,
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired
};
PlaylistInput.defaultProps = {
className: styles.playlistInputWrapper,
inputClassName: styles.input
};
export default PlaylistInput;

View file

@ -0,0 +1,97 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchOptions, clearOptions } from 'Store/Actions/providerOptionActions';
import PlaylistInput from './PlaylistInput';
function createMapStateToProps() {
return createSelector(
(state) => state.providerOptions,
(state) => {
const {
items,
...otherState
} = state;
return ({
user: items.user ? items.user : '',
items: items.playlists ? items.playlists : [],
...otherState
});
}
);
}
const mapDispatchToProps = {
dispatchFetchOptions: fetchOptions,
dispatchClearOptions: clearOptions
};
class PlaylistInputConnector extends Component {
//
// Lifecycle
componentDidMount = () => {
if (this._getAccessToken(this.props)) {
this._populate();
}
}
componentDidUpdate(prevProps, prevState) {
const newToken = this._getAccessToken(this.props);
const oldToken = this._getAccessToken(prevProps);
if (newToken && newToken !== oldToken) {
this._populate();
}
}
componentWillUnmount = () => {
this.props.dispatchClearOptions();
}
//
// Control
_populate() {
const {
provider,
providerData,
dispatchFetchOptions
} = this.props;
dispatchFetchOptions({
action: 'getPlaylists',
provider,
providerData
});
}
_getAccessToken(props) {
return _.filter(props.providerData.fields, { name: 'accessToken' })[0].value;
}
//
// Render
render() {
return (
<PlaylistInput
{...this.props}
onRefreshPress={this.onRefreshPress}
/>
);
}
}
PlaylistInputConnector.propTypes = {
provider: PropTypes.string.isRequired,
providerData: PropTypes.object.isRequired,
name: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
dispatchFetchOptions: PropTypes.func.isRequired,
dispatchClearOptions: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(PlaylistInputConnector);

View file

@ -14,6 +14,8 @@ function getType(type) {
return inputTypes.CHECK;
case 'device':
return inputTypes.DEVICE;
case 'playlist':
return inputTypes.PLAYLIST;
case 'password':
return inputTypes.PASSWORD;
case 'number':