Add ability to link Plex account using Pin

This commit is contained in:
mjrode
2019-04-14 16:31:33 -05:00
parent aca5531e24
commit f549251b49
14 changed files with 316 additions and 19 deletions

View File

@@ -7,21 +7,28 @@ export const types = {
GET_MOST_WATCHED: 'get_most_watched',
ADD_SERIES: 'add_series',
CURRENT_SHOW: 'current_show',
FETCH_PIN: 'fetch_pin',
CHECK_PLEX_PIN: 'check_plex_pin',
};
export const setLoading = loading => dispatch => {
dispatch({type: types.SET_LOADING, payload: {loading: loading}});
};
// Action Creators
export const fetchUser = () => async dispatch => {
const res = await axios.get('/api/auth/current_user');
dispatch({type: types.FETCH_USER, payload: res.data});
};
export const fetchPin = () => async dispatch => {
const res = await axios.get('/api/plex/plex-pin');
dispatch({type: types.FETCH_PIN, payload: res.data});
};
export const fetchMedia = () => async dispatch => {
dispatch({type: types.SET_LOADING, payload: true});
const res = await axios.get('/api/plex/import/all');
console.log('fetchMedia', res);
dispatch({type: types.SET_LOADING, payload: false});
dispatch({type: types.FETCH_MEDIA_RESPONSE, payload: res.data});
};
@@ -43,3 +50,37 @@ export const addSeries = params => async dispatch => {
: toast(res.data);
dispatch({type: types.ADD_SERIES, payload: res.data});
};
const createPoller = (interval, initialDelay) => {
let timeoutId = null;
let poller = () => {};
return fn => {
window.clearTimeout(timeoutId);
poller = () => {
timeoutId = window.setTimeout(poller, 2000);
return fn();
};
if (initialDelay) {
return (timeoutId = window.setTimeout(poller, 2000));
}
return poller();
};
};
export const createPollingAction = (action, interval, initialDelay) => {
const poll = createPoller(action, initialDelay);
return () => (dispatch, getState) => poll(() => action(dispatch, getState));
};
export const checkPlexPin = createPollingAction(dispatch => {
axios.get('/api/plex/check-plex-pin').then(res => {
if (res.data) {
var highestTimeoutId = setTimeout(';');
for (var i = 0; i < highestTimeoutId; i++) {
clearTimeout(i);
}
}
console.log('action res', res);
dispatch({type: types.CHECK_PLEX_PIN, payload: res.data});
});
}, 15000);

View File

@@ -7,6 +7,7 @@ import ReactGA from 'react-ga';
import Header from './Header';
import Hero from './Hero';
import Plex from './plex/Plex';
import PlexPin from './plex/PlexPin';
import SimilarList from './SimilarList';
import PopularList from './PopularList';
@@ -26,6 +27,7 @@ class App extends Component {
<div className="container">
<Header />
<Route exact path="/" component={Hero} />
<Route exact path="/plex-pin" component={PlexPin} />
<Route path="/most-watched" component={Plex} />
<Route
path="/similar/:show"

View File

@@ -0,0 +1,51 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import CssBaseline from '@material-ui/core/CssBaseline';
import Typography from '@material-ui/core/Typography';
import {withStyles} from '@material-ui/core/styles';
import {connect} from 'react-redux';
import styles from '../css/materialize.css';
import '../css/materialize.css';
class HeroSimple extends Component {
render() {
const {classes} = this.props;
return (
<React.Fragment>
<CssBaseline />
<main>
<div className={classes.heroUnit}>
<div className={classes.heroContent}>
<Typography
component="h1"
variant="h2"
align="center"
color="textPrimary"
gutterBottom
>
<img
className="responsive-img"
src={
process.env.PUBLIC_URL + '/icons/facebook_cover_photo_2.png'
}
alt="logo"
/>
</Typography>
</div>
</div>
</main>
</React.Fragment>
);
}
}
HeroSimple.propTypes = {
classes: PropTypes.object.isRequired,
};
function mapStateToProps({auth}) {
return {auth};
}
export default connect(mapStateToProps)(withStyles(styles)(HeroSimple));

View File

@@ -0,0 +1,76 @@
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {Link, Redirect} from 'react-router-dom';
import HeroSimple from '../HeroSimple';
import * as actions from '../../actions';
import Typography from '@material-ui/core/Typography';
class PlexPin extends Component {
async componentDidMount() {
await this.props.fetchPin();
const check = await this.props.checkPlexPin();
}
render() {
if (!this.props) {
return;
}
if (this.props.auth.plexToken) {
return <Redirect to="/plex" />;
}
if (!this.props.auth.plexToken) {
console.log(this.props.auth);
return (
<div>
<HeroSimple />
<div className="row flex-center">
<Typography
variant="h4"
align="center"
color="textSecondary"
paragraph
>
Enter the code below to link your account on &nbsp;
<a href="https://plex.tv/link" target="_blank">
plex.com/link.
</a>
</Typography>
</div>
<div className=" flex-center ">
<Typography
variant="h2"
align="center"
color="textSecondary"
paragraph
className="z-depth-2 code"
>
{this.props.auth.plexPin}
</Typography>
</div>
</div>
);
}
return (
<div>
<div className="row flex-center">
<Link
to="/"
className="waves-effect waves-light btn-large min-button-width"
>
<i className="material-icons left">send</i>Get Started
</Link>
</div>
</div>
);
}
}
function mapStateToProps({auth}) {
console.log('auth state to prop', auth);
return {auth};
}
export default connect(
mapStateToProps,
actions,
)(PlexPin);

View File

@@ -25,6 +25,11 @@
justify-content: center;
}
.code {
padding: 1rem;
border-radius: 1rem;
}
.margin-bottom-button {
margin-bottom: 2em
}

View File

@@ -1,10 +1,20 @@
import {types} from '../actions/index';
export const initialState = {
loading: false,
plexPin: '',
user: '',
};
export default function(state = {}, action) {
console.log('action - payload', action.payload);
switch (action.type) {
case types.FETCH_USER:
console.log('actionpayload', action.payload);
return action.payload || false;
case types.FETCH_PIN:
return {...state, plexPin: action.payload};
case types.CHECK_PLEX_PIN:
return {...state, plexToken: action.payload};
default:
return state;
}

View File

@@ -14,7 +14,7 @@ router.get(
);
router.get('/google/callback', passport.authenticate('google'), (req, res) => {
res.redirect('/most-watched');
res.redirect('/plex-pin');
});
router.get('/current_user', (req, res) => {

View File

@@ -4,6 +4,8 @@ import plexService from '../services/plex';
const router = Router();
router.get('/token', plexService.getAuthToken);
router.get('/plex-pin', plexService.getPlexPin);
router.get('/check-plex-pin', plexService.checkPlexPin);
router.get('/users', plexService.getUsers);

View File

@@ -26,6 +26,9 @@ module.exports = {
plexToken: {
type: Sequelize.STRING,
},
plexPinId: {
type: Sequelize.STRING,
},
sonarrUrl: {
type: Sequelize.STRING,
},

View File

@@ -7,6 +7,7 @@ module.exports = (sequelize, DataTypes) => {
googleId: DataTypes.STRING,
email: {type: DataTypes.STRING, unique: true},
plexUrl: DataTypes.STRING,
plexPinId: DataTypes.STRING,
plexToken: DataTypes.STRING,
sonarrUrl: DataTypes.STRING,
sonarrApiKey: DataTypes.STRING,

View File

@@ -12,8 +12,12 @@ const searchTv = async (req, res) => {
const popularTv = async (req, res) => {
const response = await movieDbApi.popularTv();
const library = await sonarrService.getSeries(req.user);
const jsonLibrary = JSON.parse(library);
// const library = await sonarrService.getSeries(req.user);
// const jsonLibrary = JSON.parse(library);
const jsonLibrary = await models.PlexLibrary.findAll({
userId: req.user.id,
type: 'show',
});
const libraryTitles = jsonLibrary.map(show => show.title.toLowerCase());
const filteredResponse = response.results.filter(
show => !libraryTitles.includes(show.name.toLowerCase()),
@@ -25,12 +29,13 @@ const similarTv = async (req, res) => {
const {showName} = req.query;
const searchResponse = await movieDbApi.searchTv(showName);
const similarResponse = await movieDbApi.similarTV(searchResponse.id);
const library = await sonarrService.getSeries(req.user);
const jsonLibrary = JSON.parse(library);
// const library = await models.PlexLibrary.findAll({
// userId: req.user.id,
// type: 'show',
// });
console.log('TCL: similarTv -> similarResponse', similarResponse);
// const library = await sonarrService.getSeries(req.user);
// const jsonLibrary = JSON.parse(library);
const jsonLibrary = await models.PlexLibrary.findAll({
userId: req.user.id,
type: 'show',
});
// Use Sonarr list instead
const libraryTitles = jsonLibrary.map(show => show.title.toLowerCase());
const filteredResponse = similarResponse.results.filter(

View File

@@ -2,6 +2,7 @@ import parser from 'xml2json';
import uuid from 'uuid';
import btoa from 'btoa';
import request from 'request-promise';
import models from '../../db/models';
const rxAuthToken = /authenticationToken="([^"]+)"/;
@@ -36,19 +37,85 @@ const plexUrlParams = plexToken => ({
},
});
const plexUrl = async plexToken => {
const getPlexPin = async user => {
try {
const res = await request.get(plexUrlParams(plexToken));
const params = {
url: 'https://plex.tv/pins.xml',
headers: {
'X-Plex-Client-Identifier': user.googleId,
},
};
const res = await request.post(params);
const formattedResponse = JSON.parse(parser.toJson(res));
const server = formattedResponse.MediaContainer.Server.filter(
server => server.port === '32400',
);
console.log(server);
return `http://${server[0].address}:${server[0].port}`;
return formattedResponse;
} catch (error) {
console.log(error);
return error.message;
}
};
export default {fetchToken, plexUrl};
const checkPlexPin = async (pinId, user) => {
try {
const params = {
url: `https://plex.tv/pins/${pinId}.xml`,
headers: {
'X-Plex-Client-Identifier': user.googleId,
},
};
const res = await request.get(params);
const formattedResponse = JSON.parse(parser.toJson(res));
console.log(
'TCL: checkPlexPin -> formattedResponse',
formattedResponse.pin.auth_token,
);
return formattedResponse.pin.auth_token;
} catch (error) {
console.log(error);
return error.message;
}
};
const getPlexUrl = async (user, token) => {
const params = {
url: `https://plex.tv/pms/:/ip`,
headers: {
'X-Plex-Client-Identifier': user.googleId,
},
};
const plexUrl = await request.get(params);
const fullPlexUrl = await plexPort(token, plexUrl, user);
return fullPlexUrl;
};
const plexPort = async (plexToken, plexUrl, user) => {
try {
const res = await request.get(plexUrlParams(plexToken));
let formattedResponse = JSON.parse(parser.toJson(res)).MediaContainer
.Server;
if (!Array.isArray(formattedResponse)) {
formattedResponse = [formattedResponse];
}
console.log('formatted response', formattedResponse);
console.log('url--', plexUrl);
const server = formattedResponse.filter(
server => server.address.toString().trim() === plexUrl.toString().trim(),
);
console.log('server', server);
await models.User.update(
{
plexToken: plexToken.trim(),
plexUrl: `http://${server[0].address}:${server[0].port}`.trim(),
},
{where: {googleId: user.googleId}},
);
console.log('server--', server);
return `http://${server[0].address}:${server[0].port}`;
} catch (error) {
console.log(error.message);
return error.message;
}
};
export default {fetchToken, plexPort, getPlexPin, checkPlexPin, getPlexUrl};

View File

@@ -3,6 +3,7 @@ import importData from './importData';
import auth from './auth';
import models from '../../db/models';
import helpers from '../helpers';
import request from 'request-promise';
const getAuthToken = async (req, res) => {
try {
@@ -25,6 +26,36 @@ const getAuthToken = async (req, res) => {
}
};
const getPlexPin = async (req, res) => {
try {
const pinRes = await auth.getPlexPin(req.user);
const plexPinId = pinRes.pin.id['$t'];
await models.User.update(
{plexPinId},
{where: {googleId: req.user.googleId}},
);
const pinCode = pinRes.pin.code;
return res.json(pinCode);
} catch (error) {
console.log('error in auth', error);
return res.status(201).json(error.message);
}
};
const checkPlexPin = async (req, res) => {
try {
const token = await auth.checkPlexPin(req.user.plexPinId, req.user);
if (token.nil) {
return res.json(null);
}
await auth.getPlexUrl(req.user, token);
return res.json(token);
} catch (error) {
console.log('error in auth', error);
return res.status(201).json(error.message);
}
};
const getUsers = (req, res) => {
plexApi
.getUsers(req.user)
@@ -101,4 +132,6 @@ export default {
importLibraries,
importMostWatched,
importAll,
getPlexPin,
checkPlexPin,
};

View File

@@ -78,6 +78,7 @@ const getSections = async function(user) {
try {
const urlParams = getSectionsUrlParams(user);
const getSectionsUrl = helpers.buildUrl(urlParams);
console.log('sec -url', getSectionsUrl);
const response = await helpers.request(getSectionsUrl);
console.log('mike-', response);
return response.MediaContainer.Directory;