mirror of
https://github.com/mjrode/WhatToWatch.git
synced 2025-12-29 17:49:39 -06:00
Add ability to link Plex account using Pin
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
51
client/src/components/HeroSimple.js
Normal file
51
client/src/components/HeroSimple.js
Normal 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));
|
||||
76
client/src/components/plex/PlexPin.js
Normal file
76
client/src/components/plex/PlexPin.js
Normal 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
|
||||
<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);
|
||||
5
client/src/css/materialize.css
vendored
5
client/src/css/materialize.css
vendored
@@ -25,6 +25,11 @@
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.code {
|
||||
padding: 1rem;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
.margin-bottom-button {
|
||||
margin-bottom: 2em
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -26,6 +26,9 @@ module.exports = {
|
||||
plexToken: {
|
||||
type: Sequelize.STRING,
|
||||
},
|
||||
plexPinId: {
|
||||
type: Sequelize.STRING,
|
||||
},
|
||||
sonarrUrl: {
|
||||
type: Sequelize.STRING,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user