Fixed merge conflict regarding version of express
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,7 +1,6 @@
|
||||
.DS_Store
|
||||
|
||||
env
|
||||
shows.db
|
||||
|
||||
yarn.lock
|
||||
*/yarn.lock
|
||||
*/package-lock.json
|
||||
|
||||
14
.travis.yml
Normal file
14
.travis.yml
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
'dist': 'trusty',
|
||||
'language': 'node_js',
|
||||
'node_js': '8.7.0',
|
||||
'cache': 'yarn',
|
||||
'scripts': [
|
||||
'npm run test'
|
||||
],
|
||||
'before_install': [
|
||||
'cd seasoned_api',
|
||||
],
|
||||
'before_script': 'yarn',
|
||||
'os': 'linux',
|
||||
}
|
||||
13
README.md
13
README.md
@@ -1,9 +1,18 @@
|
||||
# 🌶 seasonedShows
|
||||
Your customly seasoned movie and show requester, downloader and organizer
|
||||
[](https://travis-ci.org/KevinMidboe/seasonedShows)
|
||||
[]()
|
||||
|
||||
Your customly *seasoned* movie and show requester, downloader and organizer.
|
||||
|
||||
## About
|
||||
The goal of this project is to create a full custom stack that can to everything surround downloading, organizing and notifiyng of new media. From the top down we have a website using [tmdb](https://www.themoviedb.com) api to search for from over 350k movies and 70k tv shows. Using [hjone72](https://github.com/hjone72/PlexAuth) great PHP reverse proxy we can have a secure way of allowing users to login with their plex credentials which limits request capabilites to only users that are authenticated to use your plex library.
|
||||
seasonedShows is a intelligent organizer for your tv show episodes. It is made to automate and simplify to process of renaming and moving newly downloaded tv show episodes following Plex file naming and placement.
|
||||
|
||||
So this is a multipart system that lets your plex users request movies, and then from the admin page the owner can.
|
||||
|
||||
## Installation
|
||||
There are two main ways of
|
||||
|
||||
## Architecture
|
||||
The flow of the system will first check for new folders in your tv shows directory, if a new file is found it's contents are analyzed, stored and tweets suggested changes to it's contents to use_admin.
|
||||
|
||||
@@ -27,4 +36,4 @@ After approval by user the files are modified and moved to folders in resptected
|
||||
+ Parse directories for new content.
|
||||
+ Extract and save in db information about stray item.
|
||||
+ Move a confirmed stray item.
|
||||
+ (+) Search for torrents matching new content.
|
||||
+ (+) Search for torrents matching new content.
|
||||
|
||||
1
app/torrent_search
Submodule
1
app/torrent_search
Submodule
Submodule app/torrent_search added at 3deaed48b7
@@ -1,124 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import Notifications, {notify} from 'react-notify-toast';
|
||||
|
||||
// StyleComponents
|
||||
import movieStyle from './styles/movieObjectStyle.jsx';
|
||||
|
||||
var MediaQuery = require('react-responsive');
|
||||
|
||||
import RequestButton from './buttons/request_button.jsx';
|
||||
|
||||
import { fetchJSON } from './http.jsx';
|
||||
|
||||
class MovieObject {
|
||||
constructor(object) {
|
||||
this.id = object.id;
|
||||
this.title = object.title;
|
||||
this.year = object.year;
|
||||
this.type = object.type;
|
||||
// Check if object.poster != undefined
|
||||
this.rating = object.rating;
|
||||
this.poster = object.poster;
|
||||
this.background = object.background;
|
||||
this.matchedInPlex = object.matchedInPlex;
|
||||
this.summary = object.summary;
|
||||
}
|
||||
|
||||
requestExisting(movie) {
|
||||
console.log('Exists', movie);
|
||||
}
|
||||
|
||||
requestMovie() {
|
||||
// fetch('http://localhost:31459/api/v1/plex/request/' + this.id + '?type='+this.type, {
|
||||
// fetch('https://apollo.kevinmidboe.com/api/v1/plex/request/' + this.id + '?type='+this.type, {
|
||||
// method: 'POST'
|
||||
// });
|
||||
fetchJSON('https://apollo.kevinmidboe.com/api/v1/plex/request/' + this.id + '?type='+this.type, 'POST')
|
||||
.then((response) => {
|
||||
console.log(response);
|
||||
})
|
||||
|
||||
notify.show(this.title + ' requested!', 'success', 3000);
|
||||
}
|
||||
|
||||
getElement(index) {
|
||||
const element_key = index + this.id;
|
||||
|
||||
// TODO set the poster image async by updating the dom after this is returned
|
||||
if (this.poster == null || this.poster == undefined) {
|
||||
var posterPath = 'https://openclipart.org/image/2400px/svg_to_png/211479/Simple-Image-Not-Found-Icon.png'
|
||||
} else {
|
||||
var posterPath = 'https://image.tmdb.org/t/p/w300' + this.poster;
|
||||
}
|
||||
var backgroundPath = 'https://image.tmdb.org/t/p/w640_and_h360_bestv2/' + this.background;
|
||||
|
||||
var foundInPlex;
|
||||
if (this.matchedInPlex) {
|
||||
foundInPlex = <button onClick={() => {this.requestExisting(this)}}
|
||||
style={movieStyle.requestButton}><span>Request Anyway</span></button>;
|
||||
} else {
|
||||
foundInPlex = <button onClick={() => {this.requestMovie()}}
|
||||
style={movieStyle.requestButton}><span>+ Request</span></button>;
|
||||
}
|
||||
|
||||
if (this.type === 'movie')
|
||||
var themoviedbLink = 'https://www.themoviedb.org/movie/' + this.id
|
||||
else if (this.type === 'show')
|
||||
var themoviedbLink = 'https://www.themoviedb.org/tv/' + this.id
|
||||
|
||||
|
||||
|
||||
// TODO add request button class
|
||||
return (
|
||||
<div key={element_key}>
|
||||
<Notifications />
|
||||
<div style={movieStyle.resultItem} key={this.id}>
|
||||
<MediaQuery minWidth={600}>
|
||||
<div style={movieStyle.resultPoster}>
|
||||
<img style={movieStyle.resultPosterImg} id='poster' src={posterPath}></img>
|
||||
</div>
|
||||
</MediaQuery>
|
||||
<div>
|
||||
<MediaQuery minWidth={600}>
|
||||
<span style={movieStyle.resultTitleLarge}>{this.title}</span>
|
||||
<br></br>
|
||||
<span style={movieStyle.yearRatingLarge}>Released: { this.year } | Rating: {this.rating}</span>
|
||||
<br></br>
|
||||
<span style={movieStyle.summary}>{this.summary}</span>
|
||||
<br></br>
|
||||
</MediaQuery>
|
||||
|
||||
|
||||
<MediaQuery maxWidth={600}>
|
||||
<img src={ backgroundPath } style={movieStyle.background}></img>
|
||||
<span style={movieStyle.resultTitleSmall}>{this.title}</span>
|
||||
<br></br>
|
||||
<span style={movieStyle.yearRatingSmall}>Released: {this.year} | Rating: {this.rating}</span>
|
||||
</MediaQuery>
|
||||
|
||||
|
||||
<span className='imdbLogo'>
|
||||
|
||||
</span>
|
||||
|
||||
<div style={movieStyle.buttons}>
|
||||
{foundInPlex}
|
||||
<a href={themoviedbLink}>
|
||||
<button style={movieStyle.tmdbButton}><span>Info</span></button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<MediaQuery maxWidth={600}>
|
||||
<br></br>
|
||||
</MediaQuery>
|
||||
<div style={movieStyle.row}>
|
||||
<div style={movieStyle.itemDivider}></div>
|
||||
</div>
|
||||
</div>)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export default MovieObject;
|
||||
126
client/app/components/SearchObject.jsx
Normal file
126
client/app/components/SearchObject.jsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import React from 'react';
|
||||
|
||||
import Notifications, {notify} from 'react-notify-toast';
|
||||
|
||||
// StyleComponents
|
||||
import searchObjectCSS from './styles/searchObject.jsx';
|
||||
import buttonsCSS from './styles/buttons.jsx';
|
||||
import InfoButton from './buttons/InfoButton.jsx';
|
||||
|
||||
var MediaQuery = require('react-responsive');
|
||||
|
||||
import { fetchJSON } from './http.jsx';
|
||||
|
||||
import Interactive from 'react-interactive';
|
||||
|
||||
|
||||
class SearchObject {
|
||||
constructor(object) {
|
||||
this.id = object.id;
|
||||
this.title = object.title;
|
||||
this.year = object.year;
|
||||
this.type = object.type;
|
||||
this.rating = object.rating;
|
||||
this.poster = object.poster;
|
||||
this.background = object.background;
|
||||
this.matchedInPlex = object.matchedInPlex;
|
||||
this.summary = object.summary;
|
||||
}
|
||||
|
||||
requestExisting(movie) {
|
||||
console.log('Exists', movie);
|
||||
}
|
||||
|
||||
requestMovie() {
|
||||
fetchJSON('https://apollo.kevinmidboe.com/api/v1/plex/request/' + this.id + '?type='+this.type, 'POST')
|
||||
.then((response) => {
|
||||
console.log(response);
|
||||
notify.show(this.title + ' requested!', 'success', 3000);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error('Request movie fetch went wrong: '+ e);
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
getElement(index) {
|
||||
const element_key = index + this.id;
|
||||
|
||||
if (this.poster == null || this.poster == undefined) {
|
||||
var posterPath = 'https://openclipart.org/image/2400px/svg_to_png/211479/Simple-Image-Not-Found-Icon.png'
|
||||
} else {
|
||||
var posterPath = 'https://image.tmdb.org/t/p/w185' + this.poster;
|
||||
}
|
||||
var backgroundPath = 'https://image.tmdb.org/t/p/w640_and_h360_bestv2/' + this.background;
|
||||
|
||||
var foundInPlex;
|
||||
if (this.matchedInPlex) {
|
||||
foundInPlex = <Interactive
|
||||
as='button'
|
||||
onClick={() => {this.requestExisting(this)}}
|
||||
style={buttonsCSS.submit}
|
||||
focus={buttonsCSS.submit_hover}
|
||||
hover={buttonsCSS.submit_hover}>
|
||||
|
||||
<span>Request Anyway</span>
|
||||
</Interactive>;
|
||||
} else {
|
||||
foundInPlex = <Interactive
|
||||
as='button'
|
||||
onClick={() => {this.requestMovie()}}
|
||||
style={buttonsCSS.submit}
|
||||
focus={buttonsCSS.submit_hover}
|
||||
hover={buttonsCSS.submit_hover}>
|
||||
|
||||
<span>+ Request</span>
|
||||
</Interactive>;
|
||||
}
|
||||
|
||||
// TODO go away from using mediaQuery, and create custom resizer
|
||||
return (
|
||||
<div key={element_key}>
|
||||
<Notifications />
|
||||
|
||||
<div style={searchObjectCSS.container} key={this.id}>
|
||||
<MediaQuery minWidth={600}>
|
||||
<div style={searchObjectCSS.posterContainer}>
|
||||
<img style={searchObjectCSS.posterImage} id='poster' src={posterPath}></img>
|
||||
</div>
|
||||
<span style={searchObjectCSS.title_large}>{this.title}</span>
|
||||
<br></br>
|
||||
<span style={searchObjectCSS.stats_large}>
|
||||
Released: { this.year } | Rating: {this.rating} | Type: {this.type}
|
||||
</span>
|
||||
<br></br>
|
||||
<span style={searchObjectCSS.summary}>{this.summary}</span>
|
||||
<br></br>
|
||||
</MediaQuery>
|
||||
|
||||
<MediaQuery maxWidth={600}>
|
||||
<img src={ backgroundPath } style={searchObjectCSS.backgroundImage}></img>
|
||||
<span style={searchObjectCSS.title_small}>{this.title}</span>
|
||||
<br></br>
|
||||
<span style={searchObjectCSS.stats_small}>Released: {this.year} | Rating: {this.rating}</span>
|
||||
</MediaQuery>
|
||||
|
||||
<div style={searchObjectCSS.buttons}>
|
||||
{foundInPlex}
|
||||
|
||||
<InfoButton id={this.id} type={this.type} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MediaQuery maxWidth={600}>
|
||||
<br />
|
||||
</MediaQuery>
|
||||
|
||||
<div style={searchObjectCSS.dividerRow}>
|
||||
<div style={searchObjectCSS.itemDivider}></div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export default SearchObject;
|
||||
@@ -1,14 +1,14 @@
|
||||
import React from 'react';
|
||||
|
||||
import MovieObject from './MovieObject.jsx';
|
||||
|
||||
// StyleComponents
|
||||
import searchStyle from './styles/searchRequestStyle.jsx';
|
||||
import movieStyle from './styles/movieObjectStyle.jsx';
|
||||
|
||||
import URI from 'urijs';
|
||||
import InfiniteScroll from 'react-infinite-scroller';
|
||||
|
||||
// StyleComponents
|
||||
import searchRequestCSS from './styles/searchRequestStyle.jsx';
|
||||
|
||||
import SearchObject from './SearchObject.jsx';
|
||||
import Loading from './images/loading.jsx'
|
||||
|
||||
import { fetchJSON } from './http.jsx';
|
||||
import { getCookie } from './Cookie.jsx';
|
||||
|
||||
@@ -29,7 +29,8 @@ class SearchRequest extends React.Component {
|
||||
page: 1,
|
||||
resultHeader: '',
|
||||
loadResults: false,
|
||||
scrollHasMore: true
|
||||
scrollHasMore: true,
|
||||
loading: false,
|
||||
}
|
||||
|
||||
this.allowedListTypes = ['discover', 'popular', 'nowplaying', 'upcoming']
|
||||
@@ -46,7 +47,7 @@ class SearchRequest extends React.Component {
|
||||
// this.setState({responseMovieList: null})
|
||||
this.resetPageNumber();
|
||||
this.state.loadResults = true;
|
||||
this.fetchTmdbList('upcoming');
|
||||
this.fetchTmdbList('discover');
|
||||
}
|
||||
|
||||
// Handles all errors of the response of a fetch call
|
||||
@@ -88,9 +89,9 @@ class SearchRequest extends React.Component {
|
||||
this.state.page = 1;
|
||||
}
|
||||
|
||||
writeLoading() {
|
||||
setLoading(value) {
|
||||
this.setState({
|
||||
responseMovieList: 'Loading...'
|
||||
loading: value
|
||||
});
|
||||
}
|
||||
|
||||
@@ -122,10 +123,7 @@ class SearchRequest extends React.Component {
|
||||
|
||||
// Here we first call api for a search with the input uri, handle any errors
|
||||
// and fill the reponseData from api into the state of reponseMovieList as movieObjects
|
||||
callSearchFillMovieList(uri) {
|
||||
// Write loading animation
|
||||
// this.writeLoading();
|
||||
|
||||
callSearchFillMovieList(uri) {
|
||||
Promise.resolve()
|
||||
.then(() => this.callURI(uri, 'GET'))
|
||||
.then(response => {
|
||||
@@ -152,7 +150,7 @@ class SearchRequest extends React.Component {
|
||||
}
|
||||
|
||||
// Convert to json and update the state of responseMovieList with the results of the api call
|
||||
// mapped as a movieObject.
|
||||
// mapped as a SearchObject.
|
||||
response.json()
|
||||
.then(responseData => {
|
||||
if (this.state.page === 1) {
|
||||
@@ -180,7 +178,6 @@ class SearchRequest extends React.Component {
|
||||
|
||||
callListFillMovieList(uri) {
|
||||
// Write loading animation
|
||||
// this.writeLoading();
|
||||
|
||||
Promise.resolve()
|
||||
.then(() => this.callURI(uri, 'GET', undefined))
|
||||
@@ -198,7 +195,7 @@ class SearchRequest extends React.Component {
|
||||
}
|
||||
|
||||
// Convert to json and update the state of responseMovieList with the results of the api call
|
||||
// mapped as a movieObject.
|
||||
// mapped as a SearchObject.
|
||||
response.json()
|
||||
.then(responseData => {
|
||||
if (this.state.page === 1) {
|
||||
@@ -218,6 +215,7 @@ class SearchRequest extends React.Component {
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log('Something went wrong when fetching query.', error)
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
@@ -287,10 +285,10 @@ class SearchRequest extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
// When called passes the variable to MovieObject and calls it's interal function for
|
||||
// When called passes the variable to SearchObject and calls it's interal function for
|
||||
// generating the wanted HTML
|
||||
createMovieObjects(item, index) {
|
||||
let movie = new MovieObject(item);
|
||||
let movie = new SearchObject(item);
|
||||
return movie.getElement(index);
|
||||
}
|
||||
|
||||
@@ -345,12 +343,12 @@ class SearchRequest extends React.Component {
|
||||
|
||||
movieToggle() {
|
||||
if (this.state.movieFilter)
|
||||
return <span style={searchStyle.searchFilterActive}
|
||||
return <span style={searchRequestCSS.searchFilterActive}
|
||||
className="search_category hvrUnderlineFromCenter"
|
||||
onClick={() => {this.toggleFilter('movies')}}
|
||||
id="category_active">Movies</span>
|
||||
else
|
||||
return <span style={searchStyle.searchFilterNotActive}
|
||||
return <span style={searchRequestCSS.searchFilterNotActive}
|
||||
className="search_category hvrUnderlineFromCenter"
|
||||
onClick={() => {this.toggleFilter('movies')}}
|
||||
id="category_active">Movies</span>
|
||||
@@ -358,12 +356,12 @@ class SearchRequest extends React.Component {
|
||||
|
||||
showToggle() {
|
||||
if (this.state.showFilter)
|
||||
return <span style={searchStyle.searchFilterActive}
|
||||
return <span style={searchRequestCSS.searchFilterActive}
|
||||
className="search_category hvrUnderlineFromCenter"
|
||||
onClick={() => {this.toggleFilter('shows')}}
|
||||
id="category_active">TV Shows</span>
|
||||
else
|
||||
return <span style={searchStyle.searchFilterNotActive}
|
||||
return <span style={searchRequestCSS.searchFilterNotActive}
|
||||
className="search_category hvrUnderlineFromCenter"
|
||||
onClick={() => {this.toggleFilter('shows')}}
|
||||
id="category_active">TV Shows</span>
|
||||
@@ -379,50 +377,53 @@ class SearchRequest extends React.Component {
|
||||
pageStart={0}
|
||||
loadMore={this.pageForwards.bind(this)}
|
||||
hasMore={this.state.scrollHasMore}
|
||||
loader={loader}
|
||||
loader={<Loading />}
|
||||
initialLoad={this.state.loadResults}>
|
||||
|
||||
<MediaQuery minWidth={600}>
|
||||
<div style={searchStyle.body}>
|
||||
<div className='backgroundHeader' style={searchStyle.backgroundLargeHeader}>
|
||||
<div className='pageTitle' style={searchStyle.pageTitle}>
|
||||
<span style={searchStyle.pageTitleLargeSpan}>Request new content</span>
|
||||
<div style={searchRequestCSS.body}>
|
||||
<div className='backgroundHeader' style={searchRequestCSS.backgroundLargeHeader}>
|
||||
<div className='pageTitle' style={searchRequestCSS.pageTitle}>
|
||||
<span style={searchRequestCSS.pageTitleLargeSpan}>Request new content for plex</span>
|
||||
</div>
|
||||
|
||||
<div className='box' style={searchStyle.box}>
|
||||
<div style={searchStyle.searchLargeContainer}>
|
||||
<span style={searchStyle.searchIcon}><i className="fa fa-search"></i></span>
|
||||
<div style={searchRequestCSS.searchLargeContainer}>
|
||||
<span style={searchRequestCSS.searchIcon}><i className="fa fa-search"></i></span>
|
||||
|
||||
<input style={searchStyle.searchLargeBar} type="text" id="search" placeholder="Search for new content..."
|
||||
onKeyPress={(event) => this._handleQueryKeyPress(event)}
|
||||
onChange={event => this.updateQueryState(event)}
|
||||
value={this.state.searchQuery}/>
|
||||
<input style={searchRequestCSS.searchLargeBar} type="text" id="search" placeholder="Search for new content..."
|
||||
onKeyPress={(event) => this._handleQueryKeyPress(event)}
|
||||
onChange={event => this.updateQueryState(event)}
|
||||
value={this.state.searchQuery}/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id='requestMovieList' ref='requestMovieList' style={searchStyle.requestWrapper}>
|
||||
<span style={searchStyle.resultLargeHeader}>{this.state.resultHeader}</span>
|
||||
<br></br><br></br>
|
||||
<div id='requestMovieList' ref='requestMovieList' style={searchRequestCSS.requestWrapper}>
|
||||
<div style={{marginLeft: '30px'}}>
|
||||
<div style={searchRequestCSS.resultLargeHeader}>{this.state.resultHeader}</div>
|
||||
<span style={{content: '', display: 'block', width: '2em', borderTop: '2px solid #000,'}}></span>
|
||||
|
||||
{this.state.responseMovieList}
|
||||
</div>
|
||||
|
||||
<br></br>
|
||||
|
||||
{this.state.responseMovieList}
|
||||
</div>
|
||||
</div>
|
||||
</MediaQuery>
|
||||
|
||||
<MediaQuery maxWidth={600}>
|
||||
<div style={searchStyle.body}>
|
||||
<div className='backgroundHeader' style={searchStyle.backgroundSmallHeader}>
|
||||
<div className='pageTitle' style={searchStyle.pageTitle}>
|
||||
<span style={searchStyle.pageTitleSmallSpan}>Request new content</span>
|
||||
<div style={searchRequestCSS.body}>
|
||||
<div className='backgroundHeader' style={searchRequestCSS.backgroundSmallHeader}>
|
||||
<div className='pageTitle' style={searchRequestCSS.pageTitle}>
|
||||
<span style={searchRequestCSS.pageTitleSmallSpan}>Request new content</span>
|
||||
</div>
|
||||
|
||||
<div className='box' style={searchStyle.box}>
|
||||
<div style={searchStyle.searchSmallContainer}>
|
||||
<span style={searchStyle.searchIcon}><i className="fa fa-search"></i></span>
|
||||
<div className='box' style={searchRequestCSS.box}>
|
||||
<div style={searchRequestCSS.searchSmallContainer}>
|
||||
<span style={searchRequestCSS.searchIcon}><i className="fa fa-search"></i></span>
|
||||
|
||||
<input style={searchStyle.searchSmallBar} type="text" id="search" placeholder="Search for new content..."
|
||||
<input style={searchRequestCSS.searchSmallBar} type="text" id="search" placeholder="Search for new content..."
|
||||
onKeyPress={(event) => this._handleQueryKeyPress(event)}
|
||||
onChange={event => this.updateQueryState(event)}
|
||||
value={this.state.searchQuery}/>
|
||||
@@ -431,11 +432,11 @@ class SearchRequest extends React.Component {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id='requestMovieList' ref='requestMovieList' style={searchStyle.requestWrapper}>
|
||||
<span style={searchStyle.resultSmallHeader}>{this.state.resultHeader}</span>
|
||||
<div id='requestMovieList' ref='requestMovieList' style={searchRequestCSS.requestWrapper}>
|
||||
<span style={searchRequestCSS.resultSmallHeader}>{this.state.resultHeader}</span>
|
||||
<br></br><br></br>
|
||||
|
||||
{this.state.responseMovieList}
|
||||
{this.state.responseMovieList}
|
||||
</div>
|
||||
</div>
|
||||
</MediaQuery>
|
||||
@@ -443,7 +444,21 @@ class SearchRequest extends React.Component {
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// <form style={searchRequestCSS.controls}>
|
||||
// <label style={searchRequestCSS.withData}>
|
||||
// <div style={searchRequestCSS.sortOptions}>Discover</div>
|
||||
// </label>
|
||||
// </form>
|
||||
|
||||
// <form style={searchRequestCSS.controls}>
|
||||
// <label style={searchRequestCSS.withData}>
|
||||
// <select style={searchRequestCSS.sortOptions}>
|
||||
// <option value="discover">All</option>
|
||||
// <option value="nowplaying">Movies</option>
|
||||
// <option value="nowplaying">TV Shows</option>
|
||||
// </select>
|
||||
// </label>
|
||||
// </form>
|
||||
}
|
||||
|
||||
export default SearchRequest;
|
||||
export default SearchRequest;
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
/*
|
||||
./app/components/App.jsx
|
||||
|
||||
<FetchData url={"https://apollo.kevinmidboe.com/api/v1/plex/playing"} />
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { HashRouter as Router, Route, Switch, IndexRoute } from 'react-router-dom';
|
||||
|
||||
import LoginForm from './LoginForm/LoginForm.jsx';
|
||||
import { Provider } from 'react-redux';
|
||||
@@ -16,6 +11,8 @@ import { fetchJSON } from '../http.jsx';
|
||||
import Sidebar from './Sidebar.jsx';
|
||||
import AdminRequestInfo from './AdminRequestInfo.jsx';
|
||||
|
||||
import adminCSS from '../styles/adminComponent.jsx'
|
||||
|
||||
|
||||
class AdminComponent extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -23,9 +20,16 @@ class AdminComponent extends React.Component {
|
||||
this.state = {
|
||||
requested_objects: '',
|
||||
}
|
||||
|
||||
this.updateHandler = this.updateHandler.bind(this)
|
||||
}
|
||||
|
||||
// Fetches all requested elements and updates the state with response
|
||||
componentWillMount() {
|
||||
this.fetchRequestedItems()
|
||||
}
|
||||
|
||||
fetchRequestedItems() {
|
||||
fetchJSON('https://apollo.kevinmidboe.com/api/v1/plex/requests/all', 'GET')
|
||||
.then(result => {
|
||||
this.setState({
|
||||
@@ -34,16 +38,13 @@ class AdminComponent extends React.Component {
|
||||
})
|
||||
}
|
||||
|
||||
verifyLoggedIn() {
|
||||
let adminComponentStyle = {
|
||||
sidebar: {
|
||||
float: 'left',
|
||||
},
|
||||
selectedObjectPanel: {
|
||||
float: 'left',
|
||||
}
|
||||
}
|
||||
updateHandler() {
|
||||
this.fetchRequestedItems()
|
||||
}
|
||||
|
||||
// Displays loginform if not logged in and passes response from
|
||||
// api call to sidebar and infoPanel through props
|
||||
verifyLoggedIn() {
|
||||
const logged_in = getCookie('logged_in');
|
||||
if (!logged_in) {
|
||||
return <LoginForm />
|
||||
@@ -53,25 +54,27 @@ class AdminComponent extends React.Component {
|
||||
let listItemSelected = undefined;
|
||||
|
||||
const requestParam = this.props.match.params.request;
|
||||
|
||||
if (requestParam && this.state.requested_objects !== '') {
|
||||
selectedRequest = this.state.requested_objects[requestParam]
|
||||
listItemSelected = requestParam
|
||||
listItemSelected = requestParam;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={adminComponentStyle.sidebar}>
|
||||
<Sidebar
|
||||
requested_objects={this.state.requested_objects}
|
||||
listItemSelected={listItemSelected}
|
||||
style={adminComponentStyle.sidebar} />
|
||||
</div>
|
||||
<div style={adminComponentStyle.selectedObjectPanel}>
|
||||
<div style={adminCSS.selectedObjectPanel}>
|
||||
<AdminRequestInfo
|
||||
selectedRequest={selectedRequest}
|
||||
listItemSelected={listItemSelected}
|
||||
updateHandler = {this.updateHandler}
|
||||
/>
|
||||
</div>
|
||||
<div style={adminCSS.sidebar}>
|
||||
<Sidebar
|
||||
requested_objects={this.state.requested_objects}
|
||||
listItemSelected={listItemSelected}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,46 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { fetchJSON } from '../http.jsx';
|
||||
|
||||
import PirateSearch from './PirateSearch.jsx'
|
||||
// No in use!
|
||||
import InfoButton from '../buttons/InfoButton.jsx';
|
||||
|
||||
// Stylesheets
|
||||
import requestInfoCSS from '../styles/adminRequestInfo.jsx'
|
||||
import buttonsCSS from '../styles/buttons.jsx';
|
||||
|
||||
|
||||
String.prototype.capitalize = function() {
|
||||
return this.charAt(0).toUpperCase() + this.slice(1);
|
||||
}
|
||||
|
||||
|
||||
class AdminRequestInfo extends Component {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
statusValue: '',
|
||||
movieInfo: undefined,
|
||||
expandedSummary: false,
|
||||
}
|
||||
|
||||
this.requestInfo = '';
|
||||
}
|
||||
|
||||
componentWillReceiveProps(props) {
|
||||
this.requestInfo = props.selectedRequest;
|
||||
this.state.statusValue = this.requestInfo.status;
|
||||
this.state.expandedSummary = false;
|
||||
this.fetchIteminfo()
|
||||
}
|
||||
|
||||
userAgent(agent) {
|
||||
if (agent) {
|
||||
try {
|
||||
return agent.split(" ")[1].replace(/[\(\;]/g, '');
|
||||
return agent.split(" ")[1].replace(/[\(\;]/g, '');
|
||||
}
|
||||
catch(e) {
|
||||
return agent;
|
||||
@@ -19,66 +49,170 @@ class AdminRequestInfo extends Component {
|
||||
return '';
|
||||
}
|
||||
|
||||
generateStatusDropdown() {
|
||||
return (
|
||||
<select onChange={ event => this.updateRequestStatus(event) } value={this.state.statusValue}>
|
||||
<option value='requested'>Requested</option>
|
||||
<option value='downloading'>Downloading</option>
|
||||
<option value='downloaded'>Downloaded</option>
|
||||
</select>
|
||||
)
|
||||
}
|
||||
|
||||
updateRequestStatus(event) {
|
||||
const eventValue = event.target.value;
|
||||
const itemID = this.requestInfo.id;
|
||||
|
||||
const apiData = {
|
||||
type: this.requestInfo.type,
|
||||
status: eventValue,
|
||||
}
|
||||
|
||||
fetchJSON('https://apollo.kevinmidboe.com/api/v1/plex/request/' + itemID, 'PUT', apiData)
|
||||
.then((response) => {
|
||||
console.log('Response, updateRequestStatus: ', response)
|
||||
this.props.updateHandler()
|
||||
})
|
||||
}
|
||||
|
||||
generateStatusIndicator(status) {
|
||||
switch (status) {
|
||||
case 'requested':
|
||||
// Yellow
|
||||
return 'linear-gradient(to right, rgb(63, 195, 243) 0, rgb(63, 195, 243) 10px, #fff 4px, #fff 100%) no-repeat'
|
||||
case 'downloading':
|
||||
// Blue
|
||||
return 'linear-gradient(to right, rgb(255, 225, 77) 0, rgb(255, 225, 77) 10px, #fff 4px, #fff 100%) no-repeat'
|
||||
case 'downloaded':
|
||||
// Green
|
||||
return 'linear-gradient(to right, #39aa56 0, #39aa56 10px, #fff 4px, #fff 100%) no-repeat'
|
||||
default:
|
||||
return 'linear-gradient(to right, grey 0, grey 10px, #fff 4px, #fff 100%) no-repeat'
|
||||
}
|
||||
}
|
||||
|
||||
generateTypeIcon(type) {
|
||||
if (type === 'show')
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="7" width="20" height="15" rx="2" ry="2"></rect><polyline points="17 2 12 7 7 2"></polyline></svg>
|
||||
)
|
||||
else if (type === 'movie')
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="2" width="20" height="20" rx="2.18" ry="2.18"></rect><line x1="7" y1="2" x2="7" y2="22"></line><line x1="17" y1="2" x2="17" y2="22"></line><line x1="2" y1="12" x2="22" y2="12"></line><line x1="2" y1="7" x2="7" y2="7"></line><line x1="2" y1="17" x2="7" y2="17"></line><line x1="17" y1="17" x2="22" y2="17"></line><line x1="17" y1="7" x2="22" y2="7"></line></svg>
|
||||
)
|
||||
else
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12" y2="16"></line></svg>
|
||||
)
|
||||
}
|
||||
|
||||
toggleSummmaryLength() {
|
||||
this.setState({
|
||||
expandedSummary: !this.state.expandedSummary
|
||||
})
|
||||
}
|
||||
|
||||
generateSummary() {
|
||||
// { this.state.movieInfo != undefined ? this.state.movieInfo.summary : 'Loading...' }
|
||||
const info = this.state.movieInfo;
|
||||
if (info !== undefined) {
|
||||
const summary = this.state.movieInfo.summary
|
||||
const summary_short = summary.slice(0, 180);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span><b>Matched: </b> {String(info.matchedInPlex)}</span> <br/>
|
||||
<span><b>Rating: </b> {info.rating}</span> <br/>
|
||||
<span><b>Popularity: </b> {info.popularity}</span> <br/>
|
||||
{
|
||||
(summary.length > 180 && this.state.expandedSummary === false) ?
|
||||
<span><b>Summary: </b> { summary_short }<span onClick = {() => this.toggleSummmaryLength()}>... <span style={{color: 'blue', cursor: 'pointer'}}>Show more</span></span></span>
|
||||
:
|
||||
<span><b>Summary: </b> { summary }<span onClick = {() => this.toggleSummmaryLength()}><span style={{color: 'blue', cursor: 'pointer'}}> Show less</span></span></span>
|
||||
|
||||
}
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return <span>Loading...</span>
|
||||
}
|
||||
}
|
||||
|
||||
requested_by_user(request_user) {
|
||||
if (request_user === 'NULL')
|
||||
return undefined
|
||||
|
||||
return (
|
||||
<span>Requested by: {request_user}</span>
|
||||
<span><b>Requested by:</b> {request_user}</span>
|
||||
)
|
||||
}
|
||||
|
||||
displayInfo() {
|
||||
let adminIndexStyle = {
|
||||
wrapper: {
|
||||
width: '100%',
|
||||
},
|
||||
headerWrapper: {
|
||||
width: '100%',
|
||||
},
|
||||
poster: {
|
||||
float: 'left',
|
||||
minHeight: '450px',
|
||||
},
|
||||
info: {
|
||||
float: 'left',
|
||||
minHeight: '450px',
|
||||
}
|
||||
}
|
||||
const request = this.props.selectedRequest;
|
||||
fetchIteminfo() {
|
||||
const itemID = this.requestInfo.id;
|
||||
const type = this.requestInfo.type;
|
||||
|
||||
if (request) {
|
||||
return (
|
||||
<div style={adminIndexStyle.wrapper}>
|
||||
<div style={adminIndexStyle.headerWrapper}>
|
||||
<span>{request.name} </span>
|
||||
<span>{request.year}</span>
|
||||
</div>
|
||||
<div style={adminIndexStyle.poster}>
|
||||
<img src={'https://image.tmdb.org/t/p/w300/' + request.image_path} />
|
||||
</div>
|
||||
<div style={adminIndexStyle.info}>
|
||||
<span>type: {request.type}</span><br />
|
||||
<span>status: {request.status}</span><br />
|
||||
<span>ip: {request.ip}</span><br />
|
||||
<span>user_agent: {this.userAgent(request.user_agent)}</span><br />
|
||||
<span>request_date: {request.requested_date}</span><br />
|
||||
{ this.requested_by_user(request.requested_by) }
|
||||
</div>
|
||||
|
||||
<PirateSearch
|
||||
name={request.name} />
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
fetchJSON('https://apollo.kevinmidboe.com/api/v1/tmdb/' + itemID +'&type='+type, 'GET')
|
||||
.then((response) => {
|
||||
console.log('Response, getInfo:', response)
|
||||
this.setState({
|
||||
movieInfo: response
|
||||
});
|
||||
console.log(this.state.movieInfo)
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>{this.displayInfo()}</div>
|
||||
);
|
||||
}
|
||||
displayInfo() {
|
||||
const request = this.props.selectedRequest;
|
||||
|
||||
if (request) {
|
||||
requestInfoCSS.info.background = this.generateStatusIndicator(request.status);
|
||||
|
||||
return (
|
||||
<div style={requestInfoCSS.wrapper}>
|
||||
|
||||
<div style={requestInfoCSS.stick}>
|
||||
<span style={requestInfoCSS.title}> {request.name} {request.year}</span>
|
||||
<span style={{marginLeft: '2em'}}>
|
||||
<span style={requestInfoCSS.type_icon}>{this.generateTypeIcon(request.type)}</span>
|
||||
{/*<span style={style.type_text}>{request.type.capitalize()}</span> <br />*/}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div style={requestInfoCSS.info}>
|
||||
<div style={requestInfoCSS.info_poster}>
|
||||
<img src={'https://image.tmdb.org/t/p/w185' + request.poster_path} style={requestInfoCSS.image} alt='Movie poster image'></img>
|
||||
</div>
|
||||
|
||||
<div style={requestInfoCSS.info_request}>
|
||||
<h3 style={requestInfoCSS.info_request_header}>Request info</h3>
|
||||
|
||||
<span><b>status:</b>{ request.status }</span><br />
|
||||
<span><b>ip:</b>{ request.ip }</span><br />
|
||||
<span><b>user_agent:</b>{ this.userAgent(request.user_agent) }</span><br />
|
||||
<span><b>request_date:</b>{ request.requested_date}</span><br />
|
||||
{ this.requested_by_user(request.requested_by) }<br />
|
||||
{ this.generateStatusDropdown() }<br />
|
||||
</div>
|
||||
|
||||
<div style={requestInfoCSS.info_movie}>
|
||||
<h3 style={requestInfoCSS.info_movie}>Movie info</h3>
|
||||
|
||||
{ this.generateSummary() }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PirateSearch style={requestInfoCSS.search} name={request.name} />
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>{this.displayInfo()}</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default AdminRequestInfo;
|
||||
@@ -1,57 +1,89 @@
|
||||
import React, { Component } from 'react';
|
||||
import { fetchJSON } from '../http.jsx';
|
||||
|
||||
// Components
|
||||
import TorrentTable from './TorrentTable.jsx'
|
||||
|
||||
// Stylesheets
|
||||
import btnStylesheet from '../styles/buttons.jsx';
|
||||
|
||||
// Interactive button
|
||||
import Interactive from 'react-interactive';
|
||||
|
||||
import Loading from '../images/loading.jsx'
|
||||
|
||||
class PirateSearch extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
response: [],
|
||||
name: '',
|
||||
}
|
||||
}
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
torrentResponse: undefined,
|
||||
name: '',
|
||||
loading: null,
|
||||
showButton: true,
|
||||
}
|
||||
}
|
||||
|
||||
sendToDownload(torrent) {
|
||||
console.log(torrent.magnet)
|
||||
componentWillReceiveProps(props) {
|
||||
if (props.name != this.state.name) {
|
||||
this.setState({
|
||||
torrentResponse: undefined,
|
||||
showButton: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let data = {magnet: torrent.magnet}
|
||||
fetchJSON('https://apollo.kevinmidboe.com/api/v1/pirate/add', 'POST', data)
|
||||
.then((response) => {
|
||||
console.log(response)
|
||||
})
|
||||
}
|
||||
searchTheBay() {
|
||||
const query = this.props.name;
|
||||
const type = this.props.type;
|
||||
|
||||
searchTheBay() {
|
||||
const query = this.props.name;
|
||||
const type = this.props.type;
|
||||
this.setState({
|
||||
showButton: false,
|
||||
loading: <Loading />,
|
||||
})
|
||||
|
||||
fetchJSON('https://apollo.kevinmidboe.com/api/v1/pirate/search?query='+query+'&type='+type, 'GET')
|
||||
.then((response) => {
|
||||
console.log(response.torrents)
|
||||
this.setState({
|
||||
response: response.torrents.map((torrent, index) => {
|
||||
return (
|
||||
<div key={index}>
|
||||
<span>{torrent.name}</span><br />
|
||||
<span>{torrent.size}</span><br />
|
||||
<span>{torrent.seed}</span><br />
|
||||
<button onClick={() => {this.sendToDownload(torrent)}}>Send to download</button>
|
||||
<br /><br />
|
||||
</div>
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
fetchJSON('https://apollo.kevinmidboe.com/api/v1/pirate/search?query='+query+'&type='+type, 'GET')
|
||||
// fetchJSON('http://localhost:31459/api/v1/pirate/search?query='+query+'&type='+type, 'GET')
|
||||
.then((response) => {
|
||||
this.setState({
|
||||
torrentResponse: response.torrents,
|
||||
loading: null,
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
this.setState({
|
||||
showButton: true,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<span>{this.props.name}</span>
|
||||
<button onClick={() => {this.searchTheBay(this)}}>Load shit</button>
|
||||
<span>{this.state.response}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
render() {
|
||||
btnStylesheet.submit.top = '50%'
|
||||
btnStylesheet.submit.position = 'absolute'
|
||||
btnStylesheet.submit.marginLeft = '-75px'
|
||||
|
||||
return (
|
||||
<div>
|
||||
{ this.state.showButton ?
|
||||
<div style={{textAlign:'center'}}>
|
||||
<Interactive
|
||||
as='button'
|
||||
onClick={() => {this.searchTheBay()}}
|
||||
style={btnStylesheet.submit}
|
||||
focus={btnStylesheet.submit_hover}
|
||||
hover={btnStylesheet.submit_hover}>
|
||||
|
||||
<span style={{whiteSpace: 'nowrap'}}>Search for torrents</span>
|
||||
</Interactive>
|
||||
</div>
|
||||
: null }
|
||||
|
||||
{ this.state.loading }
|
||||
|
||||
<TorrentTable response={this.state.torrentResponse} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default PirateSearch
|
||||
export default PirateSearch
|
||||
|
||||
@@ -1,57 +1,248 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import Interactive from 'react-interactive';
|
||||
|
||||
import sidebarCSS from '../styles/adminSidebar.jsx'
|
||||
|
||||
class SidebarComponent extends Component {
|
||||
|
||||
generateListElements(index, item) {
|
||||
if (index == this.props.listItemSelected)
|
||||
return (
|
||||
<td>{item.name}</td>
|
||||
)
|
||||
else
|
||||
return (
|
||||
<td><Link to={{ pathname: '/admin/'+String(index)}}>{item.name}</Link></td>
|
||||
)
|
||||
}
|
||||
constructor(props){
|
||||
super(props)
|
||||
// Constructor with states holding the search query and the element of reponse.
|
||||
this.state = {
|
||||
filterValue: '',
|
||||
filterQuery: '',
|
||||
requestItemsToBeDisplayed: [],
|
||||
listItemSelected: '',
|
||||
height: '0',
|
||||
}
|
||||
|
||||
displayRequestedElementsInfo() {
|
||||
if (this.props.requested_objects) {
|
||||
let requestedElement = this.props.requested_objects.map((item, index) => {
|
||||
return (
|
||||
<tr key={index}>
|
||||
{ this.generateListElements(index, item) }
|
||||
<td>{item.status}</td>
|
||||
<td>{item.requested_date}</td>
|
||||
</tr>
|
||||
)
|
||||
})
|
||||
this.updateWindowDimensions = this.updateWindowDimensions.bind(this);
|
||||
}
|
||||
|
||||
return (
|
||||
<table key='requestedTable'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th><b>Name</b></th>
|
||||
<th><b>Status</b></th>
|
||||
<th><b>Date</b></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{requestedElement}
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
}
|
||||
}
|
||||
// Where we wait for api response to be delivered from parent through props
|
||||
componentWillReceiveProps(props) {
|
||||
this.state.listItemSelected = props.listItemSelected;
|
||||
this.displayRequestedElementsInfo(props.requested_objects);
|
||||
}
|
||||
|
||||
render() {
|
||||
console.log('sidebar: ', this.props.requested_objects)
|
||||
return (
|
||||
<div>
|
||||
<h1>Hello from the sidebar: </h1>
|
||||
<span>{ this.displayRequestedElementsInfo() }</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
componentDidMount() {
|
||||
this.updateWindowDimensions();
|
||||
window.addEventListener('resize', this.updateWindowDimensions);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.updateWindowDimensions);
|
||||
}
|
||||
|
||||
updateWindowDimensions() {
|
||||
this.setState({ height: window.innerHeight });
|
||||
}
|
||||
|
||||
// Inputs a date and returns a text string that matches how long it was since
|
||||
convertDateToDaysSince(date) {
|
||||
var oneDay = 24*60*60*1000;
|
||||
var firstDate = new Date(date);
|
||||
var secondDate = new Date();
|
||||
|
||||
var diffDays = Math.round(Math.abs((firstDate.getTime() - secondDate.getTime()) / oneDay));
|
||||
|
||||
switch (diffDays) {
|
||||
case 0:
|
||||
return 'Today';
|
||||
case 1:
|
||||
return '1 day ago'
|
||||
default:
|
||||
return diffDays + ' days ago'
|
||||
}
|
||||
}
|
||||
|
||||
// Called from our dropdown, receives a filter string and checks it with status field
|
||||
// of our request objects.
|
||||
filterItems(filterValue) {
|
||||
let filteredRequestElements = this.props.requested_objects.map((item, index) => {
|
||||
if (item.status === filterValue || filterValue === 'all')
|
||||
return this.generateListElements(index, item);
|
||||
})
|
||||
|
||||
this.setState({
|
||||
requestItemsToBeDisplayed: filteredRequestElements,
|
||||
filterValue: filterValue,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// Updates the internal state of the query filter and updates the list to only
|
||||
// display names matching the query. This is real-time filtering.
|
||||
updateFilterQuery(event) {
|
||||
const query = event.target.value;
|
||||
|
||||
let filteredByQuery = this.props.requested_objects.map((item, index) => {
|
||||
if (item.name.toLowerCase().indexOf(query.toLowerCase()) != -1)
|
||||
return this.generateListElements(index, item);
|
||||
})
|
||||
|
||||
console.log(filteredByQuery)
|
||||
this.setState({
|
||||
requestItemsToBeDisplayed: filteredByQuery,
|
||||
filterQuery: query,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
generateFilterSearch() {
|
||||
return (
|
||||
<div style={sidebarCSS.searchSidebar}>
|
||||
<div style={sidebarCSS.searchInner}>
|
||||
<input
|
||||
type="text"
|
||||
id="search"
|
||||
style={sidebarCSS.searchTextField}
|
||||
placeholder="Search requested items"
|
||||
onChange={event => this.updateFilterQuery(event)}
|
||||
value={this.state.filterQuery}/>
|
||||
<span>
|
||||
<svg id="icon-search" style={sidebarCSS.searchIcon} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15">
|
||||
<g id="search">
|
||||
<circle style={sidebarCSS.searchSVGIcon} cx="6.055" cy="5.805" r="5.305"></circle>
|
||||
<path style={sidebarCSS.searchSVGIcon} d="M9.847 9.727l4.166 4.773"></path>
|
||||
</g>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
generateNav() {
|
||||
let filterValue = this.state.filterValue;
|
||||
|
||||
return (
|
||||
<nav style={sidebarCSS.sidebar_navbar_underline}>
|
||||
<ul style={sidebarCSS.ulFilterSelectors}>
|
||||
<li>
|
||||
<span style={sidebarCSS.aFilterSelectors} onClick = { event => this.filterItems('all') }>All</span>
|
||||
{ (filterValue === 'all' || filterValue === '') && <span style={sidebarCSS.spanFilterSelectors}></span> }
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<span style={sidebarCSS.aFilterSelectors} onClick = { event => this.filterItems('requested') }>Requested</span>
|
||||
{ filterValue === 'requested' && <span style={sidebarCSS.spanFilterSelectors}></span> }
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<span style={sidebarCSS.aFilterSelectors} onClick = { event => this.filterItems('downloading') }>Downloading</span>
|
||||
{ filterValue === 'downloading' && <span style={sidebarCSS.spanFilterSelectors}></span> }
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<span style={sidebarCSS.aFilterSelectors} onClick = { event => this.filterItems('downloaded') }>Downloaded</span>
|
||||
{ filterValue === 'downloaded' && <span style={sidebarCSS.spanFilterSelectors}></span> }
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
generateBody(cards) {
|
||||
let style = sidebarCSS.ulCard;
|
||||
style.maxHeight = this.state.height - 160;
|
||||
|
||||
return (
|
||||
<ul style={style}>
|
||||
{ cards }
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
generateListElements(index, item) {
|
||||
let statusBar;
|
||||
|
||||
switch (item.status) {
|
||||
case 'requested':
|
||||
// Yellow
|
||||
statusBar = { background: 'linear-gradient(to right, rgb(63, 195, 243) 0, rgb(63, 195, 243) 4px, #fff 4px, #fff 100%) no-repeat' }
|
||||
break;
|
||||
case 'downloading':
|
||||
// Blue
|
||||
statusBar = { background: 'linear-gradient(to right, rgb(255, 225, 77) 0, rgb(255, 225, 77) 4px, #fff 4px, #fff 100%) no-repeat' }
|
||||
break;
|
||||
case 'downloaded':
|
||||
// Green
|
||||
statusBar = { background: 'linear-gradient(to right, #39aa56 0, #39aa56 4px, #fff 4px, #fff 100%) no-repeat' }
|
||||
break;
|
||||
default:
|
||||
statusBar = { background: 'linear-gradient(to right, grey 0, grey 4px, #fff 4px, #fff 100%) no-repeat' }
|
||||
}
|
||||
|
||||
statusBar.listStyleType = 'none';
|
||||
|
||||
return (
|
||||
<Link style={sidebarCSS.link} to={{ pathname: '/admin/'+String(index)}} key={index}>
|
||||
<li style={statusBar}>
|
||||
<Interactive
|
||||
as='div'
|
||||
style={ (index != this.state.listItemSelected) ? sidebarCSS.card : sidebarCSS.cardSelected }
|
||||
hover={sidebarCSS.cardSelected}
|
||||
focus={sidebarCSS.cardSelected}
|
||||
active={sidebarCSS.cardSelected}>
|
||||
|
||||
<h2 style={sidebarCSS.titleCard}>
|
||||
<span>{ item.name }</span>
|
||||
</h2>
|
||||
|
||||
<p style={sidebarCSS.pCard}>
|
||||
<span>Requested:
|
||||
<time>
|
||||
{ this.convertDateToDaysSince(item.requested_date) }
|
||||
</time>
|
||||
</span>
|
||||
</p>
|
||||
</Interactive>
|
||||
</li>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
// This is our main loader that gets called when we receive api response through props from parent
|
||||
displayRequestedElementsInfo(requested_objects) {
|
||||
let requestedElement = requested_objects.map((item, index) => {
|
||||
if (['requested', 'downloading', 'downloaded'].indexOf(this.state.filterValue) != -1) {
|
||||
if (item.status === this.state.filterValue){
|
||||
return this.generateListElements(index, item);
|
||||
}
|
||||
}
|
||||
else if (this.state.filterQuery !== '') {
|
||||
if (item.name.toLowerCase().indexOf(this.state.filterQuery.toLowerCase()) != -1)
|
||||
return this.generateListElements(index, item);
|
||||
}
|
||||
else
|
||||
return this.generateListElements(index, item);
|
||||
})
|
||||
|
||||
this.setState({
|
||||
requestItemsToBeDisplayed: this.generateBody(requestedElement)
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
// if (typeof InstallTrigger !== 'undefined')
|
||||
// bodyCSS.width = '-moz-min-content';
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 style={sidebarCSS.header}>Requested items</h1>
|
||||
{ this.generateFilterSearch() }
|
||||
{ this.generateNav() }
|
||||
|
||||
<div key='requestedTable' style={sidebarCSS.body}>
|
||||
{ this.state.requestItemsToBeDisplayed }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SidebarComponent;
|
||||
52
client/app/components/buttons/InfoButton.jsx
Normal file
52
client/app/components/buttons/InfoButton.jsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import React, { Component } from 'react';
|
||||
import Interactive from 'react-interactive';
|
||||
|
||||
import buttonsCSS from '../styles/buttons.jsx';
|
||||
|
||||
|
||||
class InfoButton extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
if (props) {
|
||||
this.state = {
|
||||
id: props.id,
|
||||
type: props.type,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(props) {
|
||||
this.setState({
|
||||
id: props.id,
|
||||
type: props.type,
|
||||
})
|
||||
}
|
||||
|
||||
getTMDBLink() {
|
||||
const id = this.state.id;
|
||||
const type = this.state.type;
|
||||
|
||||
if (type === 'movie')
|
||||
return 'https://www.themoviedb.org/movie/' + id
|
||||
else if (type === 'show')
|
||||
return 'https://www.themoviedb.org/tv/' + id
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<a href={this.getTMDBLink()}>
|
||||
<Interactive
|
||||
as='button'
|
||||
hover={buttonsCSS.info_hover}
|
||||
focus={buttonsCSS.info_hover}
|
||||
style={buttonsCSS.info}>
|
||||
|
||||
<span>More info</span>
|
||||
</Interactive>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default InfoButton;
|
||||
34
client/app/components/images/loading.jsx
Normal file
34
client/app/components/images/loading.jsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
|
||||
function Loading() {
|
||||
return (
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<svg version="1.1"
|
||||
style={{height: '75px'}}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 80 80">
|
||||
<path
|
||||
fill="#e9a131"
|
||||
d="M40,72C22.4,72,8,57.6,8,40C8,22.4,
|
||||
22.4,8,40,8c17.6,0,32,14.4,32,32c0,1.1-0.9,2-2,2
|
||||
s-2-0.9-2-2c0-15.4-12.6-28-28-28S12,24.6,12,40s12.6,
|
||||
28,28,28c1.1,0,2,0.9,2,2S41.1,72,40,72z">
|
||||
|
||||
<animateTransform
|
||||
attributeType="xml"
|
||||
attributeName="transform"
|
||||
type="rotate"
|
||||
from="0 40 40"
|
||||
to="360 40 40"
|
||||
dur="1.0s"
|
||||
repeatCount="indefinite"/>
|
||||
</path>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default Loading;
|
||||
16
client/app/components/styles/adminComponent.jsx
Normal file
16
client/app/components/styles/adminComponent.jsx
Normal file
@@ -0,0 +1,16 @@
|
||||
export default {
|
||||
sidebar: {
|
||||
float: 'left',
|
||||
width: '18%',
|
||||
minWidth: '250px',
|
||||
fontFamily: '"Open Sans", sans-serif',
|
||||
fontSize: '14px',
|
||||
borderRight: '2px solid #f2f2f2',
|
||||
},
|
||||
selectedObjectPanel: {
|
||||
width: '80%',
|
||||
float: 'right',
|
||||
fontFamily: '"Open Sans", sans-serif',
|
||||
marginTop: '1em',
|
||||
}
|
||||
}
|
||||
58
client/app/components/styles/adminRequestInfo.jsx
Normal file
58
client/app/components/styles/adminRequestInfo.jsx
Normal file
@@ -0,0 +1,58 @@
|
||||
export default {
|
||||
wrapper: {
|
||||
width: '100%',
|
||||
},
|
||||
stick: {
|
||||
marginBottom: '1em',
|
||||
},
|
||||
|
||||
title: {
|
||||
fontSize: '2em',
|
||||
},
|
||||
image: {
|
||||
width: '105px',
|
||||
borderRadius: '4px',
|
||||
},
|
||||
|
||||
info: {
|
||||
paddingTop: '1em',
|
||||
paddingBottom: '0.5em',
|
||||
marginRight: '2em',
|
||||
backgroundColor: 'white',
|
||||
border: '1px solid #d0d0d0',
|
||||
borderRadius: '2px',
|
||||
display: 'flex',
|
||||
},
|
||||
|
||||
type_icon: {
|
||||
marginLeft: '-1.9em',
|
||||
marginRight: '0.7em',
|
||||
},
|
||||
type_text: {
|
||||
verticalAlign: 'super',
|
||||
},
|
||||
|
||||
|
||||
info_poster: {
|
||||
marginLeft: '2em',
|
||||
flex: '0 1 10%'
|
||||
},
|
||||
|
||||
info_request: {
|
||||
flex: '0 1 auto'
|
||||
},
|
||||
info_request_header: {
|
||||
margin: '0',
|
||||
marginBottom: '0.5em',
|
||||
},
|
||||
|
||||
info_movie: {
|
||||
maxWidth: '70%',
|
||||
marginLeft: '1em',
|
||||
flex: '0 1 auto',
|
||||
},
|
||||
info_movie_header: {
|
||||
margin: '0',
|
||||
marginBottom: '0.5em',
|
||||
}
|
||||
}
|
||||
153
client/app/components/styles/adminSidebar.jsx
Normal file
153
client/app/components/styles/adminSidebar.jsx
Normal file
@@ -0,0 +1,153 @@
|
||||
export default {
|
||||
header: {
|
||||
textAlign: 'center',
|
||||
},
|
||||
body: {
|
||||
backgroundColor: 'white',
|
||||
},
|
||||
|
||||
parentElement: {
|
||||
display: 'inline-block',
|
||||
width: '100%',
|
||||
border: '1px solid grey',
|
||||
borderRadius: '2px',
|
||||
padding: '4px',
|
||||
margin: '4px',
|
||||
marginLeft: '4px',
|
||||
backgroundColor: 'white',
|
||||
},
|
||||
|
||||
parentElement_hover: {
|
||||
backgroundColor: '#f8f8f8',
|
||||
pointer: 'hand',
|
||||
},
|
||||
|
||||
parentElement_active: {
|
||||
textDecoration: 'none',
|
||||
},
|
||||
|
||||
parentElement_selected: {
|
||||
display: 'inline-block',
|
||||
width: '100%',
|
||||
border: '1px solid grey',
|
||||
borderRadius: '2px',
|
||||
padding: '4px',
|
||||
margin: '4px 0px 4px 4px',
|
||||
marginLeft: '10px',
|
||||
backgroundColor: 'white',
|
||||
},
|
||||
|
||||
title: {
|
||||
maxWidth: '65%',
|
||||
display: 'inline-flex',
|
||||
},
|
||||
|
||||
link: {
|
||||
color: 'black',
|
||||
textDecoration: 'none',
|
||||
},
|
||||
|
||||
rightContainer: {
|
||||
float: 'right',
|
||||
},
|
||||
|
||||
|
||||
|
||||
searchSidebar: {
|
||||
height: '4em',
|
||||
},
|
||||
searchInner: {
|
||||
top: '0',
|
||||
right: '0',
|
||||
left: '0',
|
||||
bottom: '0',
|
||||
margin: 'auto',
|
||||
width: '90%',
|
||||
minWidth: '280px',
|
||||
height: '30px',
|
||||
border: '1px solid #d0d0d0',
|
||||
borderRadius: '4px',
|
||||
overflow: 'hidden'
|
||||
},
|
||||
searchTextField: {
|
||||
display: 'inline-block',
|
||||
width: '90%',
|
||||
padding: '.3em',
|
||||
verticalAlign: 'middle',
|
||||
border: 'none',
|
||||
background: '#fff',
|
||||
fontSize: '14px',
|
||||
marginTop: '-7px',
|
||||
},
|
||||
searchIcon: {
|
||||
width: '15px',
|
||||
height: '16px',
|
||||
marginRight: '4px',
|
||||
marginTop: '7px',
|
||||
},
|
||||
searchSVGIcon: {
|
||||
fill: 'none',
|
||||
stroke: '#9d9d9d',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
strokeMiterlimit: '10',
|
||||
},
|
||||
|
||||
|
||||
ulFilterSelectors: {
|
||||
borderBottom: '2px solid #f1f1f1',
|
||||
display: 'flex',
|
||||
padding: '0',
|
||||
margin: '0',
|
||||
listStyle: 'none',
|
||||
justifyContent: 'space-evenly',
|
||||
},
|
||||
aFilterSelectors: {
|
||||
color: '#3eaaaf',
|
||||
fontSize: '16px',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
spanFilterSelectors: {
|
||||
content: '""',
|
||||
bottom: '-2px',
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
height: '2px',
|
||||
backgroundColor: '#3eaaaa',
|
||||
},
|
||||
|
||||
|
||||
ulCard: {
|
||||
margin: '1em 0 0 0',
|
||||
padding: '0',
|
||||
listStyle: 'none',
|
||||
borderBottom: '.46rem solid #f1f1f',
|
||||
backgroundColor: '#f1f1f1',
|
||||
overflow: 'scroll',
|
||||
},
|
||||
|
||||
|
||||
card: {
|
||||
padding: '.1em .5em .8em 1.5em',
|
||||
marginBottom: '.26rem',
|
||||
height: 'auto',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
cardSelected: {
|
||||
padding: '.1em .5em .8em 1.5em',
|
||||
marginBottom: '.26rem',
|
||||
height: 'auto',
|
||||
cursor: 'pointer',
|
||||
|
||||
backgroundColor: '#f9f9f9',
|
||||
},
|
||||
titleCard: {
|
||||
fontSize: '15px',
|
||||
fontWeight: '400',
|
||||
whiteSpace: 'no-wrap',
|
||||
textDecoration: 'none',
|
||||
},
|
||||
pCard: {
|
||||
margin: '0',
|
||||
},
|
||||
}
|
||||
48
client/app/components/styles/adminTorrentTable.jsx
Normal file
48
client/app/components/styles/adminTorrentTable.jsx
Normal file
@@ -0,0 +1,48 @@
|
||||
export default {
|
||||
table: {
|
||||
width: '80%',
|
||||
marginRight: 'auto',
|
||||
marginLeft: 'auto',
|
||||
},
|
||||
|
||||
searchSidebar: {
|
||||
height: '4em',
|
||||
marginTop: '1em',
|
||||
},
|
||||
searchInner: {
|
||||
top: '0',
|
||||
right: '0',
|
||||
left: '0',
|
||||
bottom: '0',
|
||||
margin: 'auto',
|
||||
width: '50%',
|
||||
minWidth: '280px',
|
||||
height: '30px',
|
||||
border: '1px solid #d0d0d0',
|
||||
borderRadius: '4px',
|
||||
overflow: 'hidden'
|
||||
},
|
||||
searchTextField: {
|
||||
display: 'inline-block',
|
||||
width: '95%',
|
||||
padding: '.3em',
|
||||
verticalAlign: 'middle',
|
||||
border: 'none',
|
||||
background: '#fff',
|
||||
fontSize: '14px',
|
||||
marginTop: '-7px',
|
||||
},
|
||||
searchIcon: {
|
||||
width: '15px',
|
||||
height: '16px',
|
||||
marginRight: '4px',
|
||||
marginTop: '7px',
|
||||
},
|
||||
searchSVGIcon: {
|
||||
fill: 'none',
|
||||
stroke: '#9d9d9d',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
strokeMiterlimit: '10',
|
||||
},
|
||||
}
|
||||
80
client/app/components/styles/buttons.jsx
Normal file
80
client/app/components/styles/buttons.jsx
Normal file
@@ -0,0 +1,80 @@
|
||||
|
||||
export default {
|
||||
|
||||
submit: {
|
||||
color: '#e9a131',
|
||||
marginRight: '10px',
|
||||
backgroundColor: 'white',
|
||||
border: '#e9a131 2px solid',
|
||||
borderColor: '#e9a131',
|
||||
borderRadius: '4px',
|
||||
textAlign: 'center',
|
||||
padding: '10px',
|
||||
minWidth: '100px',
|
||||
float: 'left',
|
||||
fontSize: '13px',
|
||||
fontWeight: '800',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
|
||||
submit_hover: {
|
||||
backgroundColor: '#e9a131',
|
||||
color: 'white',
|
||||
},
|
||||
|
||||
info: {
|
||||
color: '#00d17c',
|
||||
marginRight: '10px',
|
||||
backgroundColor: 'white',
|
||||
border: '#00d17c 2px solid',
|
||||
borderRadius: '4px',
|
||||
textAlign: 'center',
|
||||
padding: '10px',
|
||||
minWidth: '100px',
|
||||
float: 'left',
|
||||
fontSize: '13px',
|
||||
fontWeight: '800',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
|
||||
info_hover: {
|
||||
backgroundColor: '#00d17c',
|
||||
color: 'white',
|
||||
},
|
||||
|
||||
edit: {
|
||||
color: '#4a95da',
|
||||
marginRight: '10px',
|
||||
backgroundColor: 'white',
|
||||
border: '#4a95da 2px solid',
|
||||
borderRadius: '4px',
|
||||
textAlign: 'center',
|
||||
padding: '10px',
|
||||
minWidth: '100px',
|
||||
float: 'left',
|
||||
fontSize: '13px',
|
||||
fontWeight: '800',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
|
||||
edit_small: {
|
||||
color: '#4a95da',
|
||||
marginRight: '10px',
|
||||
backgroundColor: 'white',
|
||||
border: '#4a95da 2px solid',
|
||||
borderRadius: '4px',
|
||||
textAlign: 'center',
|
||||
padding: '4px',
|
||||
minWidth: '50px',
|
||||
float: 'left',
|
||||
fontSize: '13px',
|
||||
fontWeight: '800',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
|
||||
edit_hover: {
|
||||
backgroundColor: '#4a95da',
|
||||
color: 'white',
|
||||
},
|
||||
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
|
||||
export default {
|
||||
resultItem: {
|
||||
maxWidth: '95%',
|
||||
margin: '0 auto',
|
||||
minHeight: '230px'
|
||||
},
|
||||
|
||||
movie_content: {
|
||||
marginLeft: '15px'
|
||||
},
|
||||
|
||||
resultTitleLarge: {
|
||||
color: 'black',
|
||||
fontSize: '2em',
|
||||
},
|
||||
|
||||
resultTitleSmall: {
|
||||
color: 'black',
|
||||
fontSize: '22px',
|
||||
},
|
||||
|
||||
yearRatingLarge: {
|
||||
fontSize: '0.8em'
|
||||
},
|
||||
|
||||
resultPoster: {
|
||||
float: 'left',
|
||||
zIndex: '3',
|
||||
position: 'relative',
|
||||
marginRight: '30px'
|
||||
},
|
||||
|
||||
background: {
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
yearRatingSmall: {
|
||||
marginTop: '5px',
|
||||
fontSize: '0.8em'
|
||||
},
|
||||
|
||||
resultPosterImg: {
|
||||
border: '2px none',
|
||||
borderRadius: '2px',
|
||||
width: '150px'
|
||||
},
|
||||
|
||||
cornerRibbon: {
|
||||
position: 'absolute',
|
||||
width: '450px',
|
||||
},
|
||||
|
||||
summary: {
|
||||
fontSize: '15px',
|
||||
},
|
||||
|
||||
buttons: {
|
||||
paddingTop: '20px'
|
||||
},
|
||||
|
||||
requestButton: {
|
||||
color: '#e9a131',
|
||||
marginRight: '10px',
|
||||
background: 'white',
|
||||
border: '#e9a131 2px solid',
|
||||
borderRadius: '4px',
|
||||
textAlign: 'center',
|
||||
padding: '10px',
|
||||
minWidth: '100px',
|
||||
float: 'left',
|
||||
fontSize: '13px',
|
||||
fontWeight: '800',
|
||||
cursor: 'pointer'
|
||||
},
|
||||
|
||||
tmdbButton: {
|
||||
color: '#00d17c',
|
||||
marginRight: '10px',
|
||||
background: 'white',
|
||||
border: '#00d17c 2px solid',
|
||||
borderRadius: '4px',
|
||||
textAlign: 'center',
|
||||
padding: '10px',
|
||||
minWidth: '100px',
|
||||
float: 'left',
|
||||
fontSize: '13px',
|
||||
fontWeight: '800',
|
||||
cursor: 'pointer'
|
||||
},
|
||||
|
||||
row: {
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
itemDivider: {
|
||||
width: '90%',
|
||||
borderBottom: '1px solid grey',
|
||||
margin: '2rem auto'
|
||||
}
|
||||
}
|
||||
62
client/app/components/styles/searchObject.jsx
Normal file
62
client/app/components/styles/searchObject.jsx
Normal file
@@ -0,0 +1,62 @@
|
||||
|
||||
export default {
|
||||
container: {
|
||||
maxWidth: '95%',
|
||||
margin: '0 auto',
|
||||
minHeight: '230px'
|
||||
},
|
||||
|
||||
title_large: {
|
||||
color: 'black',
|
||||
fontSize: '2em',
|
||||
},
|
||||
|
||||
title_small: {
|
||||
color: 'black',
|
||||
fontSize: '22px',
|
||||
},
|
||||
|
||||
stats_large: {
|
||||
fontSize: '0.8em'
|
||||
},
|
||||
|
||||
stats_small: {
|
||||
marginTop: '5px',
|
||||
fontSize: '0.8em'
|
||||
},
|
||||
|
||||
posterContainer: {
|
||||
float: 'left',
|
||||
zIndex: '3',
|
||||
position: 'relative',
|
||||
marginRight: '30px'
|
||||
},
|
||||
|
||||
posterImage: {
|
||||
border: '2px none',
|
||||
borderRadius: '2px',
|
||||
width: '150px'
|
||||
},
|
||||
|
||||
backgroundImage: {
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
buttons: {
|
||||
paddingTop: '20px',
|
||||
},
|
||||
|
||||
summary: {
|
||||
fontSize: '15px',
|
||||
},
|
||||
|
||||
dividerRow: {
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
itemDivider: {
|
||||
width: '90%',
|
||||
borderBottom: '1px solid grey',
|
||||
margin: '2rem auto'
|
||||
}
|
||||
}
|
||||
@@ -10,18 +10,19 @@ export default {
|
||||
|
||||
backgroundLargeHeader: {
|
||||
width: '100%',
|
||||
minHeight: '400px',
|
||||
backgroundColor: '#011c23',
|
||||
minHeight: '180px',
|
||||
backgroundColor: 'rgb(1, 28, 35)',
|
||||
// backgroundImage: 'radial-gradient(circle, #004c67 0, #005771 120%)',
|
||||
zIndex: 1,
|
||||
marginBottom: '-100px'
|
||||
marginBottom: '80px'
|
||||
},
|
||||
|
||||
backgroundSmallHeader: {
|
||||
backgroundSmallHeader: {
|
||||
width: '100%',
|
||||
minHeight: '300px',
|
||||
minHeight: '120px',
|
||||
backgroundColor: '#011c23',
|
||||
zIndex: 1,
|
||||
marginBottom: '-100px'
|
||||
marginBottom: '40px'
|
||||
},
|
||||
|
||||
requestWrapper: {
|
||||
@@ -31,7 +32,7 @@ export default {
|
||||
backgroundColor: 'white',
|
||||
position: 'relative',
|
||||
zIndex: '10',
|
||||
boxShadow: '0 4px 2px black'
|
||||
boxShadow: '0 1px 2px grey',
|
||||
},
|
||||
|
||||
pageTitle: {
|
||||
@@ -53,39 +54,35 @@ export default {
|
||||
fontSize: '2em',
|
||||
marginTop: '3vh',
|
||||
marginBottom: '3vh'
|
||||
},
|
||||
|
||||
box: {
|
||||
height: '50px',
|
||||
},
|
||||
|
||||
searchLargeContainer: {
|
||||
margin: '0 25%',
|
||||
height: '52px',
|
||||
width: '77%',
|
||||
paddingLeft: '23%',
|
||||
backgroundColor: 'white',
|
||||
},
|
||||
|
||||
searchSmallContainer: {
|
||||
margin: '0 10%',
|
||||
},
|
||||
|
||||
searchIcon: {
|
||||
position: 'absolute',
|
||||
fontSize: '1.2em',
|
||||
marginTop: '12px',
|
||||
marginLeft: '-13px',
|
||||
color: '#4f5b66'
|
||||
fontSize: '1.6em',
|
||||
marginTop: '7px',
|
||||
color: '#4f5b66',
|
||||
display: 'block',
|
||||
},
|
||||
|
||||
searchLargeBar: {
|
||||
width: '100%',
|
||||
width: '50%',
|
||||
height: '50px',
|
||||
background: '#ffffff',
|
||||
border: 'none',
|
||||
fontSize: '10pt',
|
||||
fontSize: '12pt',
|
||||
float: 'left',
|
||||
color: '#63717f',
|
||||
paddingLeft: '45px',
|
||||
marginLeft: '-25px',
|
||||
borderRadius: '5px',
|
||||
paddingLeft: '40px',
|
||||
},
|
||||
|
||||
searchSmallBar: {
|
||||
@@ -93,14 +90,58 @@ export default {
|
||||
height: '50px',
|
||||
background: '#ffffff',
|
||||
border: 'none',
|
||||
fontSize: '13pt',
|
||||
fontSize: '11pt',
|
||||
float: 'left',
|
||||
color: '#63717f',
|
||||
paddingLeft: '45px',
|
||||
paddingLeft: '65px',
|
||||
marginLeft: '-25px',
|
||||
borderRadius: '5px',
|
||||
},
|
||||
|
||||
|
||||
// Dropdown for selecting tmdb lists
|
||||
controls: {
|
||||
textAlign: 'left',
|
||||
paddingTop: '8px',
|
||||
width: '33.3333%',
|
||||
marginLeft: '0',
|
||||
marginRight: '0',
|
||||
},
|
||||
|
||||
withData: {
|
||||
boxSizing: 'border-box',
|
||||
marginBottom: '0',
|
||||
display: 'block',
|
||||
padding: '0',
|
||||
verticalAlign: 'baseline',
|
||||
font: 'inherit',
|
||||
textAlign: 'left',
|
||||
boxSizing: 'border-box',
|
||||
},
|
||||
|
||||
sortOptions: {
|
||||
border: '1px solid #000',
|
||||
maxWidth: '100%',
|
||||
overflow: 'hidden',
|
||||
lineHeight: 'normal',
|
||||
textAlign: 'left',
|
||||
padding: '4px 12px',
|
||||
paddingRight: '2rem',
|
||||
backgroundImage: 'url("data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxOCAxOCI+CiAgPHRpdGxlPmFycm93LWRvd24tbWljcm88L3RpdGxlPgogIDxwb2x5bGluZSBwb2ludHM9IjE0IDQuNjcgOSAxMy4zMyA0IDQuNjciIHN0eWxlPSJmaWxsOiBub25lO3N0cm9rZTogIzAwMDtzdHJva2UtbWl0ZXJsaW1pdDogMTA7c3Ryb2tlLXdpZHRoOiAycHgiLz4KPC9zdmc+Cg==")',
|
||||
backgroundSize: '18px 18px',
|
||||
backgroundPosition: 'right 8px center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
width: 'auto',
|
||||
display: 'inline-block',
|
||||
outline: 'none',
|
||||
boxSizing: 'border-box',
|
||||
fontSize: '15px',
|
||||
WebkitAppearance: 'none',
|
||||
MozAppearance: 'none',
|
||||
appearance: 'none',
|
||||
},
|
||||
|
||||
|
||||
searchFilterActive: {
|
||||
color: '#00d17c',
|
||||
fontSize: '1em',
|
||||
@@ -115,7 +156,6 @@ export default {
|
||||
cursor: 'pointer'
|
||||
},
|
||||
|
||||
|
||||
filter: {
|
||||
color: 'white',
|
||||
paddingLeft: '40px',
|
||||
@@ -123,9 +163,9 @@ export default {
|
||||
},
|
||||
|
||||
resultLargeHeader: {
|
||||
paddingLeft: '30px',
|
||||
color: 'black',
|
||||
fontSize: '1.6em',
|
||||
width: '20%',
|
||||
},
|
||||
|
||||
resultSmallHeader: {
|
||||
@@ -133,70 +173,4 @@ export default {
|
||||
color: 'black',
|
||||
fontSize: '1.4em',
|
||||
},
|
||||
|
||||
row: {
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
itemDivider: {
|
||||
width: '90%',
|
||||
borderBottom: '1px solid grey',
|
||||
margin: '1rem auto'
|
||||
},
|
||||
|
||||
|
||||
pageNavigationBar: {
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
|
||||
pageNavigationButton: {
|
||||
margin: '0 auto',
|
||||
},
|
||||
|
||||
hvrUnderlineFromCenter: {
|
||||
color: 'white',
|
||||
fontSize: '1em',
|
||||
paddingTop: '12px',
|
||||
marginBottom: '12px',
|
||||
marginLeft: '10px',
|
||||
cursor: 'pointer',
|
||||
display: 'inline-block',
|
||||
verticalAlign: 'middle',
|
||||
WebkitTransform: 'perspective(1px) translateZ(0)',
|
||||
transform: 'perspective(1px) translateZ(0)',
|
||||
boxShadow: '0 0 1px transparent',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
':before': {
|
||||
content: "",
|
||||
position: 'absolute',
|
||||
zIndex: '-1',
|
||||
left: '50%',
|
||||
right: '50%',
|
||||
bottom: '0',
|
||||
background: '#00d17c',
|
||||
height: '2px',
|
||||
WebkitTransitionProperty: 'left, right',
|
||||
transitionProperty: 'left, right',
|
||||
WebkitTransitionDuration: '0.3s',
|
||||
transitionDuration: '0.3s',
|
||||
WebkitTransitionTimingFunction: 'ease-out',
|
||||
transitionTimingFunction: 'ease-out'
|
||||
},
|
||||
':hover:before': {
|
||||
left: 0,
|
||||
right: 0
|
||||
},
|
||||
'focus:before': {
|
||||
left: 0,
|
||||
right: 0
|
||||
},
|
||||
'active:before': {
|
||||
left: 0,
|
||||
right: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8">
|
||||
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300" rel="stylesheet">
|
||||
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.min.css" rel="stylesheet">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0 maximum-scale=1.0, user-scalable=0">
|
||||
<title>seasoned Shows</title>
|
||||
</head>
|
||||
<body style='margin: 0'>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"start": "webpack-dev-server --open --config webpack.dev.js",
|
||||
"build": "webpack --config webpack.prod.js",
|
||||
"build": "NODE_ENV=production webpack --config webpack.prod.js",
|
||||
"build_dev": "webpack --config webpack.dev.js"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -19,6 +19,7 @@
|
||||
"react-burger-menu": "^2.1.6",
|
||||
"react-dom": "^15.5.4",
|
||||
"react-infinite-scroller": "^1.0.15",
|
||||
"react-interactive": "^0.8.1",
|
||||
"react-notify-toast": "^0.3.2",
|
||||
"react-redux": "^5.0.6",
|
||||
"react-responsive": "^1.3.4",
|
||||
|
||||
@@ -21,8 +21,7 @@ module.exports = {
|
||||
],
|
||||
module: {
|
||||
loaders: [
|
||||
{ test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/ },
|
||||
{ test: /\.jsx$/, loader: 'babel-loader', exclude: /node_modules/ },
|
||||
{ test: /\.(js|jsx)$/, loader: 'babel-loader', exclude: /node_modules/ },
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
24
seasoned_api/conf/development.json
Normal file
24
seasoned_api/conf/development.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"database": {
|
||||
"host": "../shows.db"
|
||||
},
|
||||
"webserver": {
|
||||
"port": 31459
|
||||
},
|
||||
"tmdb": {
|
||||
"apiKey": ""
|
||||
},
|
||||
"raven": {
|
||||
"DSN": ""
|
||||
},
|
||||
"mail": {
|
||||
"host": "",
|
||||
"user": "",
|
||||
"password": "",
|
||||
"user_pi": "",
|
||||
"password_pi": ""
|
||||
},
|
||||
"authentication": {
|
||||
"secret": "secret"
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@
|
||||
"main": "webserver/server.js",
|
||||
"scripts": {
|
||||
"start": "cross-env SEASONED_CONFIG=conf/development.json NODE_PATH=. node src/webserver/server.js",
|
||||
"test": "cross-env SEASONED_CONFIG=conf/development.json NODE_PATH=. mocha --recursive test/system",
|
||||
"coverage": "cross-env PLANFLIX_CONFIG=conf/test.json NODE_PATH=. istanbul cover -x script/autogenerate-documentation.js --include-all-sources --dir test/.coverage node_modules/mocha/bin/_mocha --recursive test/**/* -- --report lcovonly && cat test/.coverage/lcov.info | coveralls && rm -rf test/.coverage",
|
||||
"lint": "./node_modules/.bin/eslint src/webserver/"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -10,7 +12,7 @@
|
||||
"body-parser": "~1.0.1",
|
||||
"cross-env": "^3.1.3",
|
||||
"express": "~4.11.0",
|
||||
"jsonwebtoken": "^8.0.1",
|
||||
"jsonwebtoken": "^8.0.1",
|
||||
"mongoose": "^3.6.13",
|
||||
"moviedb": "^0.2.10",
|
||||
"node-cache": "^4.1.1",
|
||||
@@ -19,12 +21,17 @@
|
||||
"raven": "^2.2.1",
|
||||
"request": "^2.81.0",
|
||||
"request-promise": "^4.2",
|
||||
"sqlite": "^2.2.1",
|
||||
"sqlite3": "^2.5.0"
|
||||
"sqlite": "^2.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^4.9.0",
|
||||
"eslint-config-airbnb-base": "^12.1.0",
|
||||
"eslint-plugin-import": "^2.8.0"
|
||||
"eslint-plugin-import": "^2.8.0",
|
||||
"eslint": "^4.9.0",
|
||||
"istanbul": "^0.4.5",
|
||||
"mocha": "^3.1.0",
|
||||
"supertest": "^2.0.1",
|
||||
"supertest-as-promised": "^4.0.1"
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,11 @@ class Config {
|
||||
|
||||
const field = new Field(this.fields[section][option])
|
||||
|
||||
if (field.value === '') {
|
||||
const envField = process.env[[section.toUpperCase(), option.toUpperCase()].join('_')]
|
||||
if (envField !== undefined && envField.length !== 0) { return envField }
|
||||
}
|
||||
|
||||
if (field.value === undefined) {
|
||||
throw new Error(`${section} => ${option} is empty.`);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ const EnvironmentVariables = require('./environmentVariables.js');
|
||||
class Field {
|
||||
|
||||
constructor(rawValue, environmentVariables) {
|
||||
this.rawValue, rawValue;
|
||||
this.rawValue = rawValue;
|
||||
this.filters = new Filters(rawValue);
|
||||
this.valueWithoutFilters = this.filters.removeFiltersFromValue();
|
||||
this.environmentVariables = new EnvironmentVariables(environmentVariables);
|
||||
|
||||
@@ -10,7 +10,7 @@ class Filters {
|
||||
}
|
||||
|
||||
isEmpty() {
|
||||
return !this.hasValidType() || this.filters.length === 0;
|
||||
return !this.hasValidType() || this.value.length === 0;
|
||||
}
|
||||
|
||||
has(filter) {
|
||||
|
||||
@@ -10,6 +10,6 @@ const database = new SqliteDatabase(configuration.get('database', 'host'));
|
||||
*/
|
||||
Promise.resolve()
|
||||
.then(() => database.connect())
|
||||
// .then(() => database.setUp());
|
||||
.then(() => database.setUp());
|
||||
|
||||
module.exports = database;
|
||||
|
||||
57
seasoned_api/src/database/schemas/setup.sql
Normal file
57
seasoned_api/src/database/schemas/setup.sql
Normal file
@@ -0,0 +1,57 @@
|
||||
CREATE TABLE IF NOT EXISTS user (
|
||||
user_name varchar(127) UNIQUE,
|
||||
password varchar(127),
|
||||
email varchar(127) UNIQUE,
|
||||
primary key (user_name)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS cache (
|
||||
key varchar(255),
|
||||
value blob,
|
||||
time_to_live INTEGER DEFAULT 60,
|
||||
created_at DATE DEFAULT (datetime('now','localtime')),
|
||||
primary key(key)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS search_history (
|
||||
id integer,
|
||||
user_name varchar(127),
|
||||
search_query varchar(255),
|
||||
primary key (id),
|
||||
foreign key(user_name) REFERENCES user(user_name) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS requests(
|
||||
id TEXT,
|
||||
name TEXT,
|
||||
year NUMBER,
|
||||
poster_path TEXT DEFAULT NULL,
|
||||
background_path TEXT DEFAULT NULL,
|
||||
requested_by TEXT,
|
||||
ip TEXT,
|
||||
requested_date DATE DEFAULT CURRENT_DATE NOT NULL,
|
||||
status CHAR(25) DEFAULT 'requested' NOT NULL,
|
||||
user_agent CHAR(255) DEFAULT NULL,
|
||||
type CHAR(50) DEFAULT 'movie'
|
||||
);
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS stray_eps(
|
||||
id TEXT UNIQUE,
|
||||
parent TEXT,
|
||||
path TEXT,
|
||||
name TEXT,
|
||||
season NUMBER,
|
||||
episode NUMBER,
|
||||
video_files TEXT,
|
||||
subtitles TEXT,
|
||||
trash TEXT,
|
||||
verified BOOLEAN DEFAULT 0,
|
||||
primary key(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS shows(
|
||||
show_names TEXT,
|
||||
date_added DATE,
|
||||
date_modified DATE DEFUALT CURRENT_DATE NOT NULL
|
||||
);
|
||||
3
seasoned_api/src/database/schemas/teardown.sql
Normal file
3
seasoned_api/src/database/schemas/teardown.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
DROP TABLE IF EXISTS user;
|
||||
DROP TABLE IF EXISTS search_history;
|
||||
DROP TABLE IF EXISTS requests;
|
||||
@@ -4,30 +4,89 @@ const sqlite = require('sqlite');
|
||||
|
||||
class SqliteDatabase {
|
||||
|
||||
constructor(host) {
|
||||
this.host = host;
|
||||
this.connection = sqlite;
|
||||
constructor(host) {
|
||||
this.host = host;
|
||||
this.connection = sqlite;
|
||||
this.schemaDirectory = path.join(__dirname, 'schemas');
|
||||
}
|
||||
|
||||
// this.schemaDirectory = path.join(__dirname, 'schemas');
|
||||
}
|
||||
/**
|
||||
* Connect to the database.
|
||||
* @returns {Promise} succeeds if connection was established
|
||||
*/
|
||||
connect() {
|
||||
return Promise.resolve()
|
||||
.then(() => sqlite.open(this.host))
|
||||
.then(() => sqlite.exec('pragma foreign_keys = on;'));
|
||||
}
|
||||
|
||||
connect() {
|
||||
return Promise.resolve()
|
||||
.then(() => sqlite.open(this.host))
|
||||
.then(() => sqlite.exec('pragma foreign_keys = on;'));
|
||||
}
|
||||
/**
|
||||
* Run a SQL query against the database.
|
||||
* @param {String} sql SQL query
|
||||
* @param {Array} parameters in the SQL query
|
||||
* @returns {Promise}
|
||||
*/
|
||||
run(sql, parameters) {
|
||||
return this.connection.run(sql, parameters);
|
||||
}
|
||||
|
||||
all(sql, parameters) {
|
||||
return this.connection.all(sql, parameters);
|
||||
}
|
||||
/**
|
||||
* Run a SQL query against the database and retrieve all the rows.
|
||||
* @param {String} sql SQL query
|
||||
* @param {Array} parameters in the SQL query
|
||||
* @returns {Promise}
|
||||
*/
|
||||
all(sql, parameters) {
|
||||
return this.connection.all(sql, parameters);
|
||||
}
|
||||
|
||||
get(sql, parameters) {
|
||||
return this.connection.get(sql, parameters);
|
||||
}
|
||||
/**
|
||||
* Run a SQL query against the database and retrieve one row.
|
||||
* @param {String} sql SQL query
|
||||
* @param {Array} parameters in the SQL query
|
||||
* @returns {Promise}
|
||||
*/
|
||||
get(sql, parameters) {
|
||||
return this.connection.get(sql, parameters);
|
||||
}
|
||||
|
||||
run(sql, parameters) {
|
||||
return this.connection.run(sql, parameters);
|
||||
}
|
||||
/**
|
||||
* Run a SQL query against the database and retrieve the status.
|
||||
* @param {String} sql SQL query
|
||||
* @param {Array} parameters in the SQL query
|
||||
* @returns {Promise}
|
||||
*/
|
||||
execute(sql) {
|
||||
return this.connection.exec(sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup the database by running setup.sql file in schemas/.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
setUp() {
|
||||
const setupSchema = this.readSqlFile('setup.sql');
|
||||
return this.execute(setupSchema);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tears down the database by running tearDown.sql file in schemas/.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
tearDown() {
|
||||
const tearDownSchema = this.readSqlFile('tearDown.sql');
|
||||
return this.execute(tearDownSchema);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file contents of a SQL file in schemas/.
|
||||
* @returns {String}
|
||||
*/
|
||||
readSqlFile(filename) {
|
||||
const schemaPath = path.join(this.schemaDirectory, filename);
|
||||
const schema = fs.readFileSync(schemaPath).toString('utf-8');
|
||||
return schema;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SqliteDatabase;
|
||||
|
||||
@@ -6,12 +6,12 @@ var PythonShell = require('python-shell');
|
||||
async function find(searchterm, callback) {
|
||||
|
||||
var options = {
|
||||
pythonPath: '/usr/bin/python3',
|
||||
// pythonPath: '/Library/Frameworks/Python.framework/Versions/3.6/bin/python3',
|
||||
args: [searchterm]
|
||||
// pythonPath: '/usr/bin/python3',
|
||||
pythonPath: '/Library/Frameworks/Python.framework/Versions/3.6/bin/python3',
|
||||
args: [searchterm, '-s', 'jackett', '--print']
|
||||
}
|
||||
|
||||
PythonShell.run('../app/pirateSearch.py', options, callback);
|
||||
PythonShell.run('../app/torrent_search/torrentSearch/search.py', options, callback);
|
||||
// PythonShell does not support return
|
||||
};
|
||||
|
||||
@@ -29,6 +29,7 @@ async function callPythonAddMagnet(magnet, callback) {
|
||||
async function SearchPiratebay(query) {
|
||||
return await new Promise((resolve) => {
|
||||
return find(query, function(err, results) {
|
||||
console.log('err', err, '. result', results);
|
||||
resolve(JSON.parse(results, null, '\t'));
|
||||
})
|
||||
})
|
||||
|
||||
@@ -22,7 +22,7 @@ class RequestRepository {
|
||||
constructor(cache, database) {
|
||||
this.database = database || establishedDatabase;
|
||||
this.queries = {
|
||||
'insertRequest': "INSERT INTO requests VALUES (?, ?, ?, ?, ?, ?, CURRENT_DATE, 'requested', ?, ?)",
|
||||
'insertRequest': "INSERT INTO requests VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_DATE, 'requested', ?, ?)",
|
||||
'fetchRequstedItems': "SELECT * FROM requests",
|
||||
'updateRequestedById': "UPDATE requests SET status = ? WHERE id is ? AND type is ?",
|
||||
}
|
||||
@@ -117,7 +117,7 @@ class RequestRepository {
|
||||
user = 'NULL';
|
||||
console.log(user)
|
||||
// Add request to database
|
||||
this.database.run(this.queries.insertRequest, [movie.id, movie.title, movie.year, movie.poster, user, ip, user_agent, movie.type])
|
||||
this.database.run(this.queries.insertRequest, [movie.id, movie.title, movie.year, movie.poster, movie.background, user, ip, user_agent, movie.type])
|
||||
|
||||
|
||||
// create reusable transporter object using the default SMTP transport
|
||||
|
||||
89
seasoned_api/test/fixtures/interstellar-query-success-response.json
vendored
Normal file
89
seasoned_api/test/fixtures/interstellar-query-success-response.json
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"number_of_items_on_page": 5,
|
||||
"page": 1,
|
||||
"results": [
|
||||
{
|
||||
"background": "/xu9zaAevzQ5nnrsXN6JcahLnG4i.jpg",
|
||||
"genre": [
|
||||
12,
|
||||
18,
|
||||
878
|
||||
],
|
||||
"id": 157336,
|
||||
"matchedInPlex": false,
|
||||
"popularity": 50.262329,
|
||||
"poster": "/nBNZadXqJSdt05SHLqgT0HuC5Gm.jpg",
|
||||
"rating": 8.1,
|
||||
"summary": "Interstellar chronicles the adventures of a group of explorers who make use of a newly discovered wormhole to surpass the limitations on human space travel and conquer the vast distances involved in an interstellar voyage.",
|
||||
"title": "Interstellar",
|
||||
"type": "movie",
|
||||
"vote_count": 12095,
|
||||
"year": 2014
|
||||
},
|
||||
{
|
||||
"background": "/bT5jpIZE50MI0COE8pOeq0kMpQo.jpg",
|
||||
"genre": [
|
||||
99
|
||||
],
|
||||
"id": 301959,
|
||||
"matchedInPlex": false,
|
||||
"popularity": 6.174326,
|
||||
"poster": "/xZwUIPqBHyJ2QIfMPANOZ1mAld6.jpg",
|
||||
"rating": 7.9,
|
||||
"summary": "Behind the scenes of Christopher Nolan's sci-fi drama, which stars Matthew McConaughey and Anne Hathaway",
|
||||
"title": "Interstellar: Nolan's Odyssey",
|
||||
"type": "movie",
|
||||
"vote_count": 102,
|
||||
"year": 2014
|
||||
},
|
||||
{
|
||||
"background": "/yTnHa6lgIv8rNneSNBDkBe8MnZe.jpg",
|
||||
"genre": [
|
||||
878
|
||||
],
|
||||
"id": 398188,
|
||||
"matchedInPlex": false,
|
||||
"popularity": 3.847981,
|
||||
"poster": "/cjvTebuqD8wmhchHE286ltVcbX6.jpg",
|
||||
"rating": 4.7,
|
||||
"summary": "For Millennia the Aliien force has watched and waited, a brooding menace that has now at last decided to take over the Earth. Communications systems worldwide are sent into chaos by a strange atmospheric interference and this has turned into a global phenomenon. A massive spaceship headed towards Earth and smaller spaceships began to cover entire cities around the world. Suddenly, the wonder turns into horror as the spaceships destroy the cities with energy weapons. When the world counterattacks, the alien ships are invincible to military weapons. The survivors have to use their wits to kill the aliens, or die.",
|
||||
"title": "Interstellar Wars",
|
||||
"type": "movie",
|
||||
"vote_count": 5,
|
||||
"year": 2016
|
||||
},
|
||||
{
|
||||
"background": "/mgb6tVEieDYLpQt666ACzGz5cyE.jpg",
|
||||
"genre": [
|
||||
35
|
||||
],
|
||||
"id": 287954,
|
||||
"matchedInPlex": false,
|
||||
"popularity": 2.778622,
|
||||
"poster": "/buoq7zYO4J3ttkEAqEMWelPDC0G.jpg",
|
||||
"rating": 7,
|
||||
"summary": "An undeniably beautiful alien is sent to Earth to study the complex mating rituals of human beings, which leads to the young interstellar traveler experiencing the passion that surrounds the centuries-old ritual of the species.",
|
||||
"title": "Lolita from Interstellar Space",
|
||||
"type": "movie",
|
||||
"vote_count": 1,
|
||||
"year": 2014
|
||||
},
|
||||
{
|
||||
"background": null,
|
||||
"genre": [
|
||||
99
|
||||
],
|
||||
"id": 336592,
|
||||
"matchedInPlex": false,
|
||||
"popularity": 2.147155,
|
||||
"poster": "/6KBD7YSBjCfgBgHwpsQo3G3GGdx.jpg",
|
||||
"rating": 7.8,
|
||||
"summary": "The science of Christopher Nolan's sci-fi, Interstellar.",
|
||||
"title": "The Science of Interstellar",
|
||||
"type": "movie",
|
||||
"vote_count": 6,
|
||||
"year": 2014
|
||||
}
|
||||
],
|
||||
"total_pages": 1
|
||||
}
|
||||
375
seasoned_api/test/fixtures/popular-movies-success-response.json
vendored
Normal file
375
seasoned_api/test/fixtures/popular-movies-success-response.json
vendored
Normal file
@@ -0,0 +1,375 @@
|
||||
{
|
||||
"page": 1,
|
||||
"results": [
|
||||
{
|
||||
"background": "/tcheoA2nPATCm2vvXw2hVQoaEFD.jpg",
|
||||
"genre": [
|
||||
18,
|
||||
14,
|
||||
27,
|
||||
53
|
||||
],
|
||||
"id": 346364,
|
||||
"matchedInPlex": false,
|
||||
"popularity": 847.49142,
|
||||
"poster": "/9E2y5Q7WlCVNEhP5GiVTjhEhx1o.jpg",
|
||||
"rating": 7.2,
|
||||
"summary": "In a small town in Maine, seven children known as The Losers Club come face to face with life problems, bullies and a monster that takes the shape of a clown called Pennywise.",
|
||||
"title": "It",
|
||||
"type": "movie",
|
||||
"vote_count": 4647,
|
||||
"year": 2017
|
||||
},
|
||||
{
|
||||
"background": "/askg3SMvhqEl4OL52YuvdtY40Yb.jpg",
|
||||
"genre": [
|
||||
12,
|
||||
16,
|
||||
10751
|
||||
],
|
||||
"id": 354912,
|
||||
"matchedInPlex": false,
|
||||
"popularity": 545.354595,
|
||||
"poster": "/eKi8dIrr8voobbaGzDpe8w0PVbC.jpg",
|
||||
"rating": 7.5,
|
||||
"summary": "Despite his family’s baffling generations-old ban on music, Miguel dreams of becoming an accomplished musician like his idol, Ernesto de la Cruz. Desperate to prove his talent, Miguel finds himself in the stunning and colorful Land of the Dead following a mysterious chain of events. Along the way, he meets charming trickster Hector, and together, they set off on an extraordinary journey to unlock the real story behind Miguel's family history.",
|
||||
"title": "Coco",
|
||||
"type": "movie",
|
||||
"vote_count": 532,
|
||||
"year": 2017
|
||||
},
|
||||
{
|
||||
"background": "/5Iw7zQTHVRBOYpA0V6z0yypOPZh.jpg",
|
||||
"genre": [
|
||||
28,
|
||||
12,
|
||||
14,
|
||||
878
|
||||
],
|
||||
"id": 181808,
|
||||
"matchedInPlex": false,
|
||||
"popularity": 510.708216,
|
||||
"poster": "/xGWVjewoXnJhvxKW619cMzppJDQ.jpg",
|
||||
"rating": 7.5,
|
||||
"summary": "Rey develops her newly discovered abilities with the guidance of Luke Skywalker, who is unsettled by the strength of her powers. Meanwhile, the Resistance prepares to do battle with the First Order.",
|
||||
"title": "Star Wars: The Last Jedi",
|
||||
"type": "movie",
|
||||
"vote_count": 1054,
|
||||
"year": 2017
|
||||
},
|
||||
{
|
||||
"background": "/o5T8rZxoWSBMYwjsUFUqTt6uMQB.jpg",
|
||||
"genre": [
|
||||
28,
|
||||
12,
|
||||
14,
|
||||
878
|
||||
],
|
||||
"id": 141052,
|
||||
"matchedInPlex": false,
|
||||
"popularity": 423.487801,
|
||||
"poster": "/9rtrRGeRnL0JKtu9IMBWsmlmmZz.jpg",
|
||||
"rating": 6.6,
|
||||
"summary": "Fueled by his restored faith in humanity and inspired by Superman's selfless act, Bruce Wayne and Diana Prince assemble a team of metahumans consisting of Barry Allen, Arthur Curry, and Victor Stone to face the catastrophic threat of Steppenwolf and the Parademons who are on the hunt for three Mother Boxes on Earth.",
|
||||
"title": "Justice League",
|
||||
"type": "movie",
|
||||
"vote_count": 1805,
|
||||
"year": 2017
|
||||
},
|
||||
{
|
||||
"background": "/52lVqTDhIeNTjT7EiJuovXgw6iE.jpg",
|
||||
"genre": [
|
||||
12,
|
||||
14,
|
||||
10751
|
||||
],
|
||||
"id": 8844,
|
||||
"matchedInPlex": false,
|
||||
"popularity": 372.129434,
|
||||
"poster": "/8wBKXZNod4frLZjAKSDuAcQ2dEU.jpg",
|
||||
"rating": 6.9,
|
||||
"summary": "When siblings Judy and Peter discover an enchanted board game that opens the door to a magical world, they unwittingly invite Alan -- an adult who's been trapped inside the game for 26 years -- into their living room. Alan's only hope for freedom is to finish the game, which proves risky as all three find themselves running from giant rhinoceroses, evil monkeys and other terrifying creatures.",
|
||||
"title": "Jumanji",
|
||||
"type": "movie",
|
||||
"vote_count": 2907,
|
||||
"year": 1995
|
||||
},
|
||||
{
|
||||
"background": "/qLmdjn2fv0FV2Mh4NBzMArdA0Uu.jpg",
|
||||
"genre": [
|
||||
10751,
|
||||
16,
|
||||
12,
|
||||
35
|
||||
],
|
||||
"id": 211672,
|
||||
"matchedInPlex": false,
|
||||
"popularity": 345.173187,
|
||||
"poster": "/q0R4crx2SehcEEQEkYObktdeFy.jpg",
|
||||
"rating": 6.4,
|
||||
"summary": "Minions Stuart, Kevin and Bob are recruited by Scarlet Overkill, a super-villain who, alongside her inventor husband Herb, hatches a plot to take over the world.",
|
||||
"title": "Minions",
|
||||
"type": "movie",
|
||||
"vote_count": 5237,
|
||||
"year": 2015
|
||||
},
|
||||
{
|
||||
"background": "/6aUWe0GSl69wMTSWWexsorMIvwU.jpg",
|
||||
"genre": [
|
||||
10751,
|
||||
14,
|
||||
10749
|
||||
],
|
||||
"id": 321612,
|
||||
"matchedInPlex": false,
|
||||
"popularity": 297.124109,
|
||||
"poster": "/tWqifoYuwLETmmasnGHO7xBjEtt.jpg",
|
||||
"rating": 6.8,
|
||||
"summary": "A live-action adaptation of Disney's version of the classic tale of a cursed prince and a beautiful young woman who helps him break the spell.",
|
||||
"title": "Beauty and the Beast",
|
||||
"type": "movie",
|
||||
"vote_count": 6318,
|
||||
"year": 2017
|
||||
},
|
||||
{
|
||||
"background": "/lMDyuHzBhx3zNAv48vYzmgcJCCD.jpg",
|
||||
"genre": [
|
||||
18,
|
||||
35
|
||||
],
|
||||
"id": 419680,
|
||||
"matchedInPlex": false,
|
||||
"popularity": 278.025258,
|
||||
"poster": "/rF2IoKL0IFmumEXQFUuB8LajTYP.jpg",
|
||||
"rating": 5.7,
|
||||
"summary": "Brad and Dusty must deal with their intrusive fathers during the holidays.",
|
||||
"title": "Daddy's Home 2",
|
||||
"type": "movie",
|
||||
"vote_count": 408,
|
||||
"year": 2017
|
||||
},
|
||||
{
|
||||
"background": "/tvKcA4OFUiZkNeTJmmTkNqKHMMg.jpg",
|
||||
"genre": [
|
||||
80,
|
||||
18,
|
||||
9648
|
||||
],
|
||||
"id": 392044,
|
||||
"matchedInPlex": false,
|
||||
"popularity": 259.276687,
|
||||
"poster": "/iBlfxlw8qwtUS0R8YjIU7JtM6LM.jpg",
|
||||
"rating": 6.8,
|
||||
"summary": "Genius Belgian detective Hercule Poirot investigates the murder of an American tycoon aboard the Orient Express train.",
|
||||
"title": "Murder on the Orient Express",
|
||||
"type": "movie",
|
||||
"vote_count": 878,
|
||||
"year": 2017
|
||||
},
|
||||
{
|
||||
"background": "/ulMscezy9YX0bhknvJbZoUgQxO5.jpg",
|
||||
"genre": [
|
||||
18,
|
||||
878,
|
||||
10752
|
||||
],
|
||||
"id": 281338,
|
||||
"matchedInPlex": false,
|
||||
"popularity": 252.304349,
|
||||
"poster": "/3vYhLLxrTtZLysXtIWktmd57Snv.jpg",
|
||||
"rating": 6.7,
|
||||
"summary": "Caesar and his apes are forced into a deadly conflict with an army of humans led by a ruthless Colonel. After the apes suffer unimaginable losses, Caesar wrestles with his darker instincts and begins his own mythic quest to avenge his kind. As the journey finally brings them face to face, Caesar and the Colonel are pitted against each other in an epic battle that will determine the fate of both their species and the future of the planet.",
|
||||
"title": "War for the Planet of the Apes",
|
||||
"type": "movie",
|
||||
"vote_count": 2692,
|
||||
"year": 2017
|
||||
},
|
||||
{
|
||||
"background": "/rz3TAyd5kmiJmozp3GUbYeB5Kep.jpg",
|
||||
"genre": [
|
||||
28,
|
||||
12,
|
||||
35,
|
||||
10751
|
||||
],
|
||||
"id": 353486,
|
||||
"matchedInPlex": false,
|
||||
"popularity": 250.35028,
|
||||
"poster": "/bXrZ5iHBEjH7WMidbUDQ0U2xbmr.jpg",
|
||||
"rating": 5.6,
|
||||
"summary": "The tables are turned as four teenagers are sucked into Jumanji's world - pitted against rhinos, black mambas and an endless variety of jungle traps and puzzles. To survive, they'll play as characters from the game.",
|
||||
"title": "Jumanji: Welcome to the Jungle",
|
||||
"type": "movie",
|
||||
"vote_count": 128,
|
||||
"year": 2017
|
||||
},
|
||||
{
|
||||
"background": "/iJ5dkwIHQnq8dfmwSLh7dpETNhi.jpg",
|
||||
"genre": [
|
||||
35,
|
||||
16,
|
||||
12
|
||||
],
|
||||
"id": 355547,
|
||||
"matchedInPlex": false,
|
||||
"popularity": 250.28269,
|
||||
"poster": "/zms2RpkqjAtCsbjndTG9gAGWvnx.jpg",
|
||||
"rating": 4.6,
|
||||
"summary": "A small but brave donkey and his animal friends become the unsung heroes of the greatest story ever told, the first Christmas.",
|
||||
"title": "The Star",
|
||||
"type": "movie",
|
||||
"vote_count": 78,
|
||||
"year": 2017
|
||||
},
|
||||
{
|
||||
"background": "/2SEgJ0mHJ7TSdVDbkGU061tR33K.jpg",
|
||||
"genre": [
|
||||
18,
|
||||
53,
|
||||
28,
|
||||
878
|
||||
],
|
||||
"id": 347882,
|
||||
"matchedInPlex": false,
|
||||
"popularity": 210.896389,
|
||||
"poster": "/wridRvGxDqGldhzAIh3IcZhHT5F.jpg",
|
||||
"rating": 5.4,
|
||||
"summary": "A young street magician is left to take care of his little sister after his mother's passing and turns to drug dealing in the Los Angeles party scene to keep a roof over their heads. When he gets into trouble with his supplier, his sister is kidnapped and he is forced to rely on both his sleight of hand and brilliant mind to save her.",
|
||||
"title": "Sleight",
|
||||
"type": "movie",
|
||||
"vote_count": 156,
|
||||
"year": 2017
|
||||
},
|
||||
{
|
||||
"background": "/5wNUJs23rT5rTBacNyf5h83AynM.jpg",
|
||||
"genre": [
|
||||
28,
|
||||
12,
|
||||
35,
|
||||
14,
|
||||
878
|
||||
],
|
||||
"id": 284053,
|
||||
"matchedInPlex": false,
|
||||
"popularity": 210.575092,
|
||||
"poster": "/oSLd5GYGsiGgzDPKTwQh7wamO8t.jpg",
|
||||
"rating": 7.5,
|
||||
"summary": "Thor is imprisoned on the other side of the universe and finds himself in a race against time to get back to Asgard to stop Ragnarok, the prophecy of destruction to his homeworld and the end of Asgardian civilization, at the hands of an all-powerful new threat, the ruthless Hela.",
|
||||
"title": "Thor: Ragnarok",
|
||||
"type": "movie",
|
||||
"vote_count": 2598,
|
||||
"year": 2017
|
||||
},
|
||||
{
|
||||
"background": "/uExPmkOHJySrbJyJDJylHDqaT58.jpg",
|
||||
"genre": [
|
||||
28,
|
||||
12,
|
||||
35
|
||||
],
|
||||
"id": 343668,
|
||||
"matchedInPlex": false,
|
||||
"popularity": 190.179283,
|
||||
"poster": "/34xBL6BXNYFqtHO9zhcgoakS4aP.jpg",
|
||||
"rating": 7.1,
|
||||
"summary": "When an attack on the Kingsman headquarters takes place and a new villain rises, Eggsy and Merlin are forced to work together with the American agency known as the Statesman to save the world.",
|
||||
"title": "Kingsman: The Golden Circle",
|
||||
"type": "movie",
|
||||
"vote_count": 1714,
|
||||
"year": 2017
|
||||
},
|
||||
{
|
||||
"background": "/bAI7aPHQcvSZXvt7L11kMJdS0Gm.jpg",
|
||||
"genre": [
|
||||
18,
|
||||
35,
|
||||
36
|
||||
],
|
||||
"id": 371638,
|
||||
"matchedInPlex": false,
|
||||
"popularity": 187.757689,
|
||||
"poster": "/uCH6FOFsDW6pfvbbmIIswuvuNtM.jpg",
|
||||
"rating": 7.2,
|
||||
"summary": "An aspiring actor in Hollywood meets an enigmatic stranger by the name of Tommy Wiseau, the meeting leads the actor down a path nobody could have predicted; creating the worst movie ever made.",
|
||||
"title": "The Disaster Artist",
|
||||
"type": "movie",
|
||||
"vote_count": 87,
|
||||
"year": 2017
|
||||
},
|
||||
{
|
||||
"background": "/2BXd0t9JdVqCp9sKf6kzMkr7QjB.jpg",
|
||||
"genre": [
|
||||
12,
|
||||
10751,
|
||||
16,
|
||||
28,
|
||||
35
|
||||
],
|
||||
"id": 177572,
|
||||
"matchedInPlex": false,
|
||||
"popularity": 180.209866,
|
||||
"poster": "/9gLu47Zw5ertuFTZaxXOvNfy78T.jpg",
|
||||
"rating": 7.7,
|
||||
"summary": "The special bond that develops between plus-sized inflatable robot Baymax, and prodigy Hiro Hamada, who team up with a group of friends to form a band of high-tech heroes.",
|
||||
"title": "Big Hero 6",
|
||||
"type": "movie",
|
||||
"vote_count": 6872,
|
||||
"year": 2014
|
||||
},
|
||||
{
|
||||
"background": "/6iUNJZymJBMXXriQyFZfLAKnjO6.jpg",
|
||||
"genre": [
|
||||
28,
|
||||
12,
|
||||
14
|
||||
],
|
||||
"id": 297762,
|
||||
"matchedInPlex": false,
|
||||
"popularity": 176.828995,
|
||||
"poster": "/imekS7f1OuHyUP2LAiTEM0zBzUz.jpg",
|
||||
"rating": 7.2,
|
||||
"summary": "An Amazon princess comes to the world of Man to become the greatest of the female superheroes.",
|
||||
"title": "Wonder Woman",
|
||||
"type": "movie",
|
||||
"vote_count": 6535,
|
||||
"year": 2017
|
||||
},
|
||||
{
|
||||
"background": "/umC04Cozevu8nn3JTDJ1pc7PVTn.jpg",
|
||||
"genre": [
|
||||
28,
|
||||
53
|
||||
],
|
||||
"id": 245891,
|
||||
"matchedInPlex": false,
|
||||
"popularity": 171.364116,
|
||||
"poster": "/5vHssUeVe25bMrof1HyaPyWgaP.jpg",
|
||||
"rating": 7,
|
||||
"summary": "Ex-hitman John Wick comes out of retirement to track down the gangsters that took everything from him.",
|
||||
"title": "John Wick",
|
||||
"type": "movie",
|
||||
"vote_count": 6117,
|
||||
"year": 2014
|
||||
},
|
||||
{
|
||||
"background": "/vc8bCGjdVp0UbMNLzHnHSLRbBWQ.jpg",
|
||||
"genre": [
|
||||
28,
|
||||
12,
|
||||
35,
|
||||
878
|
||||
],
|
||||
"id": 315635,
|
||||
"matchedInPlex": false,
|
||||
"popularity": 157.789584,
|
||||
"poster": "/ApYhuwBWzl29Oxe9JJsgL7qILbD.jpg",
|
||||
"rating": 7.3,
|
||||
"summary": "Following the events of Captain America: Civil War, Peter Parker, with the help of his mentor Tony Stark, tries to balance his life as an ordinary high school student in Queens, New York City, with fighting crime as his superhero alter ego Spider-Man as a new threat, the Vulture, emerges.",
|
||||
"title": "Spider-Man: Homecoming",
|
||||
"type": "movie",
|
||||
"vote_count": 5218,
|
||||
"year": 2017
|
||||
}
|
||||
],
|
||||
"total_pages": 992
|
||||
}
|
||||
10
seasoned_api/test/helpers/createCacheEntry.js
Normal file
10
seasoned_api/test/helpers/createCacheEntry.js
Normal file
@@ -0,0 +1,10 @@
|
||||
const Cache = require('src/tmdb/cache');
|
||||
const SqliteDatabase = require('src/database/sqliteDatabase');
|
||||
|
||||
function createCacheEntry(key, value) {
|
||||
const database = new SqliteDatabase(':memory:');
|
||||
const cache = new Cache(database);
|
||||
return cache.set(key, value);
|
||||
}
|
||||
|
||||
module.exports = createCacheEntry;
|
||||
10
seasoned_api/test/helpers/createToken.js
Normal file
10
seasoned_api/test/helpers/createToken.js
Normal file
@@ -0,0 +1,10 @@
|
||||
const User = require('src/user/user');
|
||||
const Token = require('src/user/token');
|
||||
|
||||
function createToken(username, secret) {
|
||||
const user = new User(username);
|
||||
const token = new Token(user);
|
||||
return token.toString(secret);
|
||||
}
|
||||
|
||||
module.exports = createToken;
|
||||
12
seasoned_api/test/helpers/createUser.js
Normal file
12
seasoned_api/test/helpers/createUser.js
Normal file
@@ -0,0 +1,12 @@
|
||||
const User = require('src/user/user');
|
||||
const UserSecurity = require('src/user/userSecurity');
|
||||
const SqliteDatabase = require('src/database/sqliteDatabase');
|
||||
|
||||
function createUser(username, email, password) {
|
||||
const database = new SqliteDatabase(':memory:');
|
||||
const userSecurity = new UserSecurity(database);
|
||||
const user = new User(username, email);
|
||||
return userSecurity.createNewUser(user, password);
|
||||
}
|
||||
|
||||
module.exports = createUser;
|
||||
11
seasoned_api/test/helpers/resetDatabase.js
Normal file
11
seasoned_api/test/helpers/resetDatabase.js
Normal file
@@ -0,0 +1,11 @@
|
||||
const SqliteDatabase = require('src/database/sqliteDatabase');
|
||||
|
||||
function resetDatabase() {
|
||||
const database = new SqliteDatabase(':memory:');
|
||||
return Promise.resolve()
|
||||
.then(() => database.connect())
|
||||
// .then(() => database.tearDown())
|
||||
.then(() => database.setUp());
|
||||
}
|
||||
|
||||
module.exports = resetDatabase;
|
||||
@@ -0,0 +1,16 @@
|
||||
const assert = require('assert');
|
||||
const request = require('supertest-as-promised');
|
||||
const app = require('src/webserver/app');
|
||||
const resetDatabase = require('test/helpers/resetDatabase');
|
||||
|
||||
describe('As a user I want to register', () => {
|
||||
before(() => resetDatabase());
|
||||
|
||||
it('should return 200 and a message indicating success', () =>
|
||||
request(app)
|
||||
.post('/api/v1/user')
|
||||
.send({ username: 'test', email: 'test@gmail.com', password: 'password' })
|
||||
.expect(200)
|
||||
.then(response => assert.equal(response.body.message, 'Welcome to Seasoned!'))
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
/* eslint-disable no-return-assign */
|
||||
const net = require('net');
|
||||
|
||||
xdescribe('As a developer I want the server to start', () => {
|
||||
beforeEach(() =>
|
||||
this.server = require('src/webserver/server'));
|
||||
|
||||
it('should listen on port 31459', (done) => {
|
||||
net.createConnection(31459, done);
|
||||
});
|
||||
|
||||
afterEach(() =>
|
||||
this.server.close());
|
||||
});
|
||||
25
seasoned_api/test/system/asADeveloperIWantToLogin.js
Normal file
25
seasoned_api/test/system/asADeveloperIWantToLogin.js
Normal file
@@ -0,0 +1,25 @@
|
||||
const assert = require('assert');
|
||||
const request = require('supertest-as-promised');
|
||||
const app = require('src/webserver/app');
|
||||
const createUser = require('test/helpers/createUser');
|
||||
const resetDatabase = require('test/helpers/resetDatabase');
|
||||
|
||||
describe('As a user I want to log in', () => {
|
||||
before(() => resetDatabase());
|
||||
before(() => createUser('test_user', 'test@gmail.com', 'password'));
|
||||
|
||||
it('should return 200 with a token if correct credentials are given', () =>
|
||||
request(app)
|
||||
.post('/api/v1/user/login')
|
||||
.send({ username: 'test_user', password: 'password' })
|
||||
.expect(200)
|
||||
.then(response => assert.equal(typeof response.body.token, 'string'))
|
||||
);
|
||||
|
||||
it('should return 401 if incorrect credentials are given', () =>
|
||||
request(app)
|
||||
.post('/api/v1/user/login')
|
||||
.send({ username: 'test_user', password: 'anti-password' })
|
||||
.expect(401)
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
const assert = require('assert');
|
||||
const resetDatabase = require('test/helpers/resetDatabase');
|
||||
const app = require('src/webserver/app');
|
||||
const request = require('supertest-as-promised');
|
||||
|
||||
describe('As a user I want a forbidden error if the token is malformed', () => {
|
||||
before(() => resetDatabase());
|
||||
|
||||
it('should return 401', () =>
|
||||
request(app)
|
||||
.get('/api/v1/plex/requests/all')
|
||||
.set('Authorization', 'maLfOrMed TOKEN')
|
||||
.expect(401)
|
||||
.then(response => assert.equal(response.body.error, 'You must be logged in.'))
|
||||
);
|
||||
});
|
||||
18
seasoned_api/test/system/asAUserIWantToGetPopularMovies.js
Normal file
18
seasoned_api/test/system/asAUserIWantToGetPopularMovies.js
Normal file
@@ -0,0 +1,18 @@
|
||||
const assert = require('assert');
|
||||
const createCacheEntry = require('test/helpers/createCacheEntry');
|
||||
const resetDatabase = require('test/helpers/resetDatabase');
|
||||
const request = require('supertest-as-promised');
|
||||
const app = require('src/webserver/app');
|
||||
const popularMoviesSuccess = require('test/fixtures/popular-movies-success-response.json');
|
||||
|
||||
describe('As a user I want to get popular movies', () => {
|
||||
before(() => resetDatabase());
|
||||
before(() => createCacheEntry('p:movie::1', popularMoviesSuccess));
|
||||
|
||||
it('should return 200 with the information', () =>
|
||||
request(app)
|
||||
.get('/api/v1/tmdb/list/popular')
|
||||
.expect(200)
|
||||
.then(response => assert.equal(response.body.results.length, 20))
|
||||
);
|
||||
});
|
||||
17
seasoned_api/test/system/asAUserIWantToRequestAMovie.js
Normal file
17
seasoned_api/test/system/asAUserIWantToRequestAMovie.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const resetDatabase = require('test/helpers/resetDatabase');
|
||||
const app = require('src/webserver/app');
|
||||
const request = require('supertest-as-promised');
|
||||
const createUser = require('test/helpers/createUser');
|
||||
const createToken = require('test/helpers/createToken');
|
||||
|
||||
describe('As a user I want to request a movie', () => {
|
||||
before(() => resetDatabase());
|
||||
before(() => createUser('test_user', 'test@gmail.com', 'password'));
|
||||
|
||||
it('should return 200 when item is requested', () =>
|
||||
request(app)
|
||||
.post('/api/v1/plex/request/31749')
|
||||
.set('Authorization', createToken('test_user', 'secret'))
|
||||
.expect(200)
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
const createCacheEntry = require('test/helpers/createCacheEntry');
|
||||
const resetDatabase = require('test/helpers/resetDatabase');
|
||||
const request = require('supertest-as-promised');
|
||||
const app = require('src/webserver/app');
|
||||
const interstellarQuerySuccess = require('test/fixtures/interstellar-query-success-response.json');
|
||||
|
||||
describe('As an anonymous user I want to search for a movie', () => {
|
||||
before(() => resetDatabase());
|
||||
before(() => createCacheEntry('s:1:movie:interstellar', interstellarQuerySuccess));
|
||||
|
||||
it('should return 200 with the search results even if user is not logged in', () =>
|
||||
request(app)
|
||||
.get('/api/v1/tmdb/search?query=interstellar&page=1')
|
||||
.expect(200)
|
||||
);
|
||||
});
|
||||
2403
seasoned_api/yarn.lock
Normal file
2403
seasoned_api/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user