mirror of
https://github.com/mjrode/WhatToWatch.git
synced 2025-12-30 10:09:44 -06:00
Add popular list for Sonarr import
This commit is contained in:
@@ -7,6 +7,7 @@ import Header from './Header';
|
||||
import Hero from './Hero';
|
||||
import Plex from './plex/Plex';
|
||||
import SimilarList from './SimilarList';
|
||||
import PopularList from './PopularList';
|
||||
|
||||
class App extends Component {
|
||||
componentDidMount() {
|
||||
@@ -26,6 +27,7 @@ class App extends Component {
|
||||
path="/similar/:show"
|
||||
render={props => <SimilarList {...props} />}
|
||||
/>
|
||||
<Route path="/popular" component={PopularList} />
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {Link} from 'react-router-dom';
|
||||
|
||||
import '../css/materialize.css';
|
||||
class Header extends Component {
|
||||
renderContent() {
|
||||
const isMobile = window.innerWidth < 480;
|
||||
switch (this.props.auth) {
|
||||
case null:
|
||||
return;
|
||||
@@ -14,16 +15,21 @@ class Header extends Component {
|
||||
</li>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<div>
|
||||
<li key="1" style={{margin: '0 10px'}}>
|
||||
<Link to={'/most-watched'}>Most Watched</Link>
|
||||
</li>
|
||||
<li key="2" style={{margin: '0 10px'}}>
|
||||
<a href="/api/auth/logout">Logout</a>
|
||||
</li>
|
||||
</div>
|
||||
);
|
||||
if (!isMobile) {
|
||||
return (
|
||||
<div className="hide-mobile">
|
||||
<li key="1" style={{margin: '0 10px'}}>
|
||||
<Link to={'/most-watched'}>Most Watched</Link>
|
||||
</li>
|
||||
<li key="2" style={{margin: '0 10px'}}>
|
||||
<Link to={'/popular'}>Popular</Link>
|
||||
</li>
|
||||
<li key="3" style={{margin: '0 10px'}}>
|
||||
<a href="/api/auth/logout">Logout</a>
|
||||
</li>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
render() {
|
||||
|
||||
@@ -5,8 +5,8 @@ import {connect} from 'react-redux';
|
||||
import Header from './helpers/Header';
|
||||
import styles from '../css/materialize.css';
|
||||
import '../css/materialize.css';
|
||||
|
||||
import {Link} from 'react-router-dom';
|
||||
|
||||
class MediaCard extends Component {
|
||||
render() {
|
||||
const show = this.props.media;
|
||||
|
||||
143
client/src/components/PopularCard.js
Normal file
143
client/src/components/PopularCard.js
Normal file
@@ -0,0 +1,143 @@
|
||||
import React, {Component} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {withStyles} from '@material-ui/core/styles';
|
||||
import {connect} from 'react-redux';
|
||||
import Header from './helpers/Header';
|
||||
import styles from '../css/materialize.css';
|
||||
import {ToastContainer} from 'react-toastify';
|
||||
import * as actions from '../actions';
|
||||
import {Link} from 'react-router-dom';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
|
||||
class PopularCard extends Component {
|
||||
renderContent() {
|
||||
if (
|
||||
this.props.loading &&
|
||||
this.props.currentShow === this.props.media.name
|
||||
) {
|
||||
return (
|
||||
<div className="progress">
|
||||
<div className="indeterminate" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
renderToast() {
|
||||
if (this.props.currentShow === this.props.media.name) {
|
||||
return <ToastContainer />;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const show = this.props.media;
|
||||
const isMobile = window.innerWidth < 480;
|
||||
if (!isMobile) {
|
||||
return (
|
||||
<div>
|
||||
{this.renderToast()}
|
||||
{this.renderContent()}
|
||||
<div className="row hide-mobile">
|
||||
<div className="col s12 ">
|
||||
<div className="card medium horizontal">
|
||||
<div
|
||||
className="card-image"
|
||||
style={{
|
||||
boxShadow:
|
||||
'0 2px 2px 0 rgba(0,0,0,0.14), 0 3px 1px -2px rgba(0,0,0,0.12), 0 1px 5px 0 rgba(0,0,0,0.2)',
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={`https://image.tmdb.org/t/p/w500/${show.poster_path}`}
|
||||
alt="pic"
|
||||
className="circle"
|
||||
/>
|
||||
</div>
|
||||
<div className="card-stacked">
|
||||
<div className="card-content">
|
||||
<div className="header">
|
||||
<Header text={show.name} />
|
||||
</div>
|
||||
<p>{show.overview}</p>
|
||||
</div>
|
||||
<div className="card-action">
|
||||
<h6 className="robots abs">
|
||||
Rating: {` ${show.vote_average} `}| Popularity:{' '}
|
||||
{` ${show.popularity}`}
|
||||
</h6>
|
||||
|
||||
<button
|
||||
className="waves-effect waves-light btn-large right Button margin-left"
|
||||
style={{backgroundColor: '#f9a1bc'}}
|
||||
type="submit"
|
||||
name="action"
|
||||
key={show.name}
|
||||
onClick={() =>
|
||||
this.props.addSeries({showName: show.name})
|
||||
}
|
||||
>
|
||||
Add to Sonarr
|
||||
<i className="material-icons right">send</i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="row hide-desktop">
|
||||
<ToastContainer />
|
||||
<div className="col s12 m12">
|
||||
<div className="card ">
|
||||
<div
|
||||
className="card-image"
|
||||
style={{
|
||||
boxShadow:
|
||||
'0 2px 2px 0 rgba(0,0,0,0.14), 0 3px 1px -2px rgba(0,0,0,0.12), 0 1px 5px 0 rgba(0,0,0,0.2)',
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={`https://image.tmdb.org/t/p/w500/${show.poster_path}`}
|
||||
alt="pic"
|
||||
className="circle"
|
||||
/>
|
||||
</div>
|
||||
<div className="card-content">
|
||||
<p>{show.overview}</p>
|
||||
</div>
|
||||
<div className="card-action flex-center">
|
||||
<Link
|
||||
to={`/similar/${show.title}`}
|
||||
className="waves-effect waves-light btn-large center Button"
|
||||
style={{backgroundColor: '#f9a1bc'}}
|
||||
key={show.name}
|
||||
onClick={() => this.props.addSeries({showName: show.name})}
|
||||
>
|
||||
<i className="material-icons right">send</i>Add to Sonarr
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PopularCard.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
loading: state.sonarr.loading,
|
||||
sonarrAddSeries: state.sonarr.sonarrAddSeries,
|
||||
currentShow: state.plex.currentShow,
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
actions,
|
||||
)(withStyles(styles)(PopularCard));
|
||||
50
client/src/components/PopularList.js
Normal file
50
client/src/components/PopularList.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import React, {Component} from 'react';
|
||||
import {Redirect} from 'react-router-dom';
|
||||
import {withStyles} from '@material-ui/core/styles';
|
||||
import {connect} from 'react-redux';
|
||||
import styles from '../css/materialize.css.js';
|
||||
import axios from 'axios';
|
||||
import PopularCard from './PopularCard';
|
||||
import * as actions from '../actions';
|
||||
|
||||
class PopularList extends Component {
|
||||
state = {
|
||||
shows: [],
|
||||
};
|
||||
componentDidMount() {
|
||||
this.getSimilar();
|
||||
}
|
||||
|
||||
getSimilar = async () => {
|
||||
const res = await axios.get('/api/moviedb/tv/popular');
|
||||
const shows = res.data;
|
||||
this.setState({shows: shows});
|
||||
};
|
||||
|
||||
render() {
|
||||
if (!this.props.auth) {
|
||||
return <Redirect to="/" />;
|
||||
}
|
||||
if (this.state.shows.length > 0) {
|
||||
const mediaList = this.state.shows.map((show, index) => {
|
||||
return <PopularCard media={show} key={index} />;
|
||||
});
|
||||
return <div>{mediaList}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="progress">
|
||||
<div className="indeterminate" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps({plex, auth, sonarr}) {
|
||||
return {loading: plex.loading, auth, sonarrAddSeries: sonarr.sonarrAddSeries};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
actions,
|
||||
)(withStyles(styles)(PopularList));
|
||||
24
client/src/components/PrivateRoute.js
Normal file
24
client/src/components/PrivateRoute.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import {Redirect, Route} from 'react-router-dom';
|
||||
import {connect} from 'react-redux';
|
||||
// Utils
|
||||
|
||||
const PrivateRoute = ({component: Component, ...rest}) => (
|
||||
<Route
|
||||
{...rest}
|
||||
render={props =>
|
||||
props.auth !== null ? (
|
||||
<Component {...props} />
|
||||
) : (
|
||||
<Redirect
|
||||
to={{
|
||||
pathname: '/login',
|
||||
state: {from: props.location},
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
export default PrivateRoute;
|
||||
@@ -7,6 +7,7 @@ import styles from '../css/materialize.css';
|
||||
import {ToastContainer} from 'react-toastify';
|
||||
import * as actions from '../actions';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import {Link} from 'react-router-dom';
|
||||
|
||||
class MediaCard extends Component {
|
||||
renderContent() {
|
||||
@@ -106,23 +107,16 @@ class MediaCard extends Component {
|
||||
<div className="card-content">
|
||||
<p>{show.overview}</p>
|
||||
</div>
|
||||
<div className="card-action">
|
||||
{' '}
|
||||
<div className="card-action">
|
||||
<i className="material-icons left">live_tv</i>Rating:
|
||||
{` ${show.vote_average}`} Popularity: {` ${show.popularity}`}
|
||||
<button
|
||||
className="waves-effect waves-light btn-large right Button margin-left"
|
||||
style={{backgroundColor: '#f9a1bc'}}
|
||||
type="submit"
|
||||
name="action"
|
||||
key={show.name}
|
||||
onClick={() => this.props.addSeries({showName: show.name})}
|
||||
>
|
||||
Add to Sonarr
|
||||
<i className="material-icons right">send</i>
|
||||
</button>
|
||||
</div>
|
||||
<div className="card-action flex-center">
|
||||
<Link
|
||||
to={`/similar/${show.title}`}
|
||||
className="waves-effect waves-light btn-large center Button"
|
||||
style={{backgroundColor: '#f9a1bc'}}
|
||||
key={show.name}
|
||||
onClick={() => this.props.addSeries({showName: show.name})}
|
||||
>
|
||||
<i className="material-icons right">send</i>Add to Sonarr
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import PlexTokenForm from './PlexTokenForm';
|
||||
import {Link} from 'react-router-dom';
|
||||
import ImportPlexLibrary from './ImportPlexLibrary';
|
||||
import MediaList from '../MediaList';
|
||||
|
||||
@@ -20,6 +21,14 @@ class Plex extends Component {
|
||||
<div>
|
||||
<ImportPlexLibrary />
|
||||
<MediaList />
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,14 @@ import TextHeader from '../helpers/Header';
|
||||
import styles from '../../css/materialize.css';
|
||||
|
||||
class PlexTokenForm extends React.Component {
|
||||
state = {email: '', password: '', sonarrUrl: '', sonarrApiKey: ''};
|
||||
state = {
|
||||
email: '',
|
||||
password: '',
|
||||
sonarrUrl: '',
|
||||
sonarrApiKey: '',
|
||||
errorMessage: '',
|
||||
redirect: false,
|
||||
};
|
||||
|
||||
onFormSubmit = event => {
|
||||
event.preventDefault();
|
||||
@@ -18,8 +25,13 @@ class PlexTokenForm extends React.Component {
|
||||
};
|
||||
|
||||
getPlexToken = async params => {
|
||||
await axios.get('/api/plex/token', {params});
|
||||
window.location.reload();
|
||||
const res = await axios.get('/api/plex/token', {params});
|
||||
console.log('response', res.data);
|
||||
if (res.data.includes('Invalid')) {
|
||||
this.setState({errorMessage: res.data});
|
||||
} else {
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
@@ -37,6 +49,9 @@ class PlexTokenForm extends React.Component {
|
||||
<TextHeader text="Connect to Plex and Sonarr" />
|
||||
</div>
|
||||
<hr />
|
||||
<div className="flex-center">
|
||||
<h5 className="robots center ">{this.state.errorMessage}</h5>
|
||||
</div>
|
||||
</div>
|
||||
<div className="section center-align">
|
||||
<div className="row">
|
||||
|
||||
@@ -3,6 +3,7 @@ import {types} from '../actions/index';
|
||||
export default function(state = {}, action) {
|
||||
switch (action.type) {
|
||||
case types.FETCH_USER:
|
||||
console.log('actionpayload', action.payload);
|
||||
return action.payload || false;
|
||||
default:
|
||||
return state;
|
||||
|
||||
@@ -5,5 +5,6 @@ const router = Router();
|
||||
|
||||
router.get('/tv/search', movieDbService.searchTv);
|
||||
router.get('/tv/similar', movieDbService.similarTv);
|
||||
router.get('/tv/popular', movieDbService.popularTv);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -10,6 +10,17 @@ const searchTv = async (req, res) => {
|
||||
res.json(response);
|
||||
};
|
||||
|
||||
const popularTv = async (req, res) => {
|
||||
const response = await movieDbApi.popularTv();
|
||||
const library = await sonarrService.getSeries(req.user);
|
||||
const jsonLibrary = JSON.parse(library);
|
||||
const libraryTitles = jsonLibrary.map(show => show.title.toLowerCase());
|
||||
const filteredResponse = response.results.filter(
|
||||
show => !libraryTitles.includes(show.name.toLowerCase()),
|
||||
);
|
||||
res.json(filteredResponse);
|
||||
};
|
||||
|
||||
const similarTv = async (req, res) => {
|
||||
const {showName} = req.query;
|
||||
const searchResponse = await movieDbApi.searchTv(showName);
|
||||
@@ -22,7 +33,6 @@ const similarTv = async (req, res) => {
|
||||
// });
|
||||
// Use Sonarr list instead
|
||||
const libraryTitles = jsonLibrary.map(show => show.title.toLowerCase());
|
||||
console.log('titles', libraryTitles);
|
||||
const filteredResponse = similarResponse.results.filter(
|
||||
show => !libraryTitles.includes(show.name.toLowerCase()),
|
||||
);
|
||||
@@ -32,4 +42,5 @@ const similarTv = async (req, res) => {
|
||||
export default {
|
||||
searchTv,
|
||||
similarTv,
|
||||
popularTv,
|
||||
};
|
||||
|
||||
@@ -4,6 +4,15 @@ import models from '../../db/models';
|
||||
import MovieDb from 'moviedb-promise';
|
||||
const mdb = new MovieDb(config.server.movieApiKey);
|
||||
|
||||
const popularTv = async () => {
|
||||
try {
|
||||
const response = await mdb.miscPopularTvs();
|
||||
return response;
|
||||
} catch (error) {
|
||||
helpers.handleError(error, 'popularTv');
|
||||
}
|
||||
};
|
||||
|
||||
const searchTv = async showName => {
|
||||
try {
|
||||
const response = await mdb.searchTv({
|
||||
@@ -29,4 +38,4 @@ const similarTV = async showId => {
|
||||
}
|
||||
};
|
||||
|
||||
export default {searchTv, similarTV};
|
||||
export default {searchTv, similarTV, popularTv};
|
||||
|
||||
@@ -8,6 +8,10 @@ const getAuthToken = async (req, res) => {
|
||||
try {
|
||||
const {email, password, sonarrUrl, sonarrApiKey} = req.query;
|
||||
const plexToken = await auth.fetchToken(email, password);
|
||||
console.log('plex token', plexToken.includes('401'));
|
||||
if (plexToken.includes('401')) {
|
||||
return res.json('Invalid username or password.');
|
||||
}
|
||||
const plexUrl = await auth.plexUrl(plexToken);
|
||||
const [rowsUpdate, updatedUser] = await models.User.update(
|
||||
{plexUrl, plexToken, sonarrUrl, sonarrApiKey},
|
||||
@@ -16,6 +20,7 @@ const getAuthToken = async (req, res) => {
|
||||
|
||||
return res.json(updatedUser);
|
||||
} catch (error) {
|
||||
console.log('error in auth', error);
|
||||
return res.status(201).json(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user