diff --git a/client/src/actions/index.js b/client/src/actions/index.js index ccf8b8a..67771c7 100644 --- a/client/src/actions/index.js +++ b/client/src/actions/index.js @@ -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); diff --git a/client/src/components/App.js b/client/src/components/App.js index 659f2d9..6f58255 100644 --- a/client/src/components/App.js +++ b/client/src/components/App.js @@ -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 {
+ + +
+
+
+ + logo + +
+
+
+ + ); + } +} + +HeroSimple.propTypes = { + classes: PropTypes.object.isRequired, +}; + +function mapStateToProps({auth}) { + return {auth}; +} + +export default connect(mapStateToProps)(withStyles(styles)(HeroSimple)); diff --git a/client/src/components/plex/PlexPin.js b/client/src/components/plex/PlexPin.js new file mode 100644 index 0000000..6173ef8 --- /dev/null +++ b/client/src/components/plex/PlexPin.js @@ -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 ; + } + if (!this.props.auth.plexToken) { + console.log(this.props.auth); + return ( +
+ + +
+ + Enter the code below to link your account on   + + plex.com/link. + + +
+
+ + {this.props.auth.plexPin} + +
+
+ ); + } + return ( +
+
+ + sendGet Started + +
+
+ ); + } +} + +function mapStateToProps({auth}) { + console.log('auth state to prop', auth); + return {auth}; +} + +export default connect( + mapStateToProps, + actions, +)(PlexPin); diff --git a/client/src/css/materialize.css b/client/src/css/materialize.css index ce3c612..aee7f97 100644 --- a/client/src/css/materialize.css +++ b/client/src/css/materialize.css @@ -25,6 +25,11 @@ justify-content: center; } +.code { + padding: 1rem; + border-radius: 1rem; +} + .margin-bottom-button { margin-bottom: 2em } diff --git a/client/src/reducers/authReducer.js b/client/src/reducers/authReducer.js index f93f6c3..2402bed 100644 --- a/client/src/reducers/authReducer.js +++ b/client/src/reducers/authReducer.js @@ -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; } diff --git a/server/controllers/auth.controller.js b/server/controllers/auth.controller.js index 2d298ff..341408e 100644 --- a/server/controllers/auth.controller.js +++ b/server/controllers/auth.controller.js @@ -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) => { diff --git a/server/controllers/plex.controller.js b/server/controllers/plex.controller.js index 146cc23..3eb5751 100644 --- a/server/controllers/plex.controller.js +++ b/server/controllers/plex.controller.js @@ -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); diff --git a/server/db/migrations/20190224043920-create-user.js b/server/db/migrations/20190224043920-create-user.js index 3e8d031..7976d9e 100644 --- a/server/db/migrations/20190224043920-create-user.js +++ b/server/db/migrations/20190224043920-create-user.js @@ -26,6 +26,9 @@ module.exports = { plexToken: { type: Sequelize.STRING, }, + plexPinId: { + type: Sequelize.STRING, + }, sonarrUrl: { type: Sequelize.STRING, }, diff --git a/server/db/models/user.js b/server/db/models/user.js index a963dfd..9a02673 100644 --- a/server/db/models/user.js +++ b/server/db/models/user.js @@ -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, diff --git a/server/services/moviedb/index.js b/server/services/moviedb/index.js index eeabf50..2443499 100644 --- a/server/services/moviedb/index.js +++ b/server/services/moviedb/index.js @@ -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( diff --git a/server/services/plex/auth.js b/server/services/plex/auth.js index 144e86a..a31f249 100644 --- a/server/services/plex/auth.js +++ b/server/services/plex/auth.js @@ -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}; diff --git a/server/services/plex/index.js b/server/services/plex/index.js index b56a850..6c69498 100644 --- a/server/services/plex/index.js +++ b/server/services/plex/index.js @@ -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, }; diff --git a/server/services/plex/plexApi.js b/server/services/plex/plexApi.js index 31196a1..5ce5434 100644 --- a/server/services/plex/plexApi.js +++ b/server/services/plex/plexApi.js @@ -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;