Fixed merge conflict regarding version of express

This commit is contained in:
2018-01-09 17:05:02 +01:00
49 changed files with 4540 additions and 576 deletions

3
.gitignore vendored
View File

@@ -1,7 +1,6 @@
.DS_Store
env
shows.db
yarn.lock
*/yarn.lock
*/package-lock.json

14
.travis.yml Normal file
View 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',
}

View File

@@ -1,9 +1,18 @@
# 🌶 seasonedShows
Your customly seasoned movie and show requester, downloader and organizer
[![Build Status](https://travis-ci.org/KevinMidboe/seasonedShows.svg?branch=testing)](https://travis-ci.org/KevinMidboe/seasonedShows)
[![DUB](https://img.shields.io/dub/l/vibe-d.svg)]()
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

Submodule app/torrent_search added at 3deaed48b7

View File

@@ -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>&#x0002B; 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;

View 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>&#x0002B; 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;

View File

@@ -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;

View File

@@ -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>
)
}

View File

@@ -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;

View File

@@ -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

View File

@@ -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>
&nbsp;{ 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;

View 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;

View 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;

View 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',
}
}

View 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',
}
}

View 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',
},
}

View 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',
},
}

View 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',
},
}

View File

@@ -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'
}
}

View 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'
}
}

View File

@@ -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
}
}
}

View File

@@ -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'>

View File

@@ -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",

View File

@@ -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/ },
]
},

View 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"
}
}

View File

@@ -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"
}
}

View File

@@ -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.`);
}

View File

@@ -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);

View File

@@ -10,7 +10,7 @@ class Filters {
}
isEmpty() {
return !this.hasValidType() || this.filters.length === 0;
return !this.hasValidType() || this.value.length === 0;
}
has(filter) {

View File

@@ -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;

View 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
);

View File

@@ -0,0 +1,3 @@
DROP TABLE IF EXISTS user;
DROP TABLE IF EXISTS search_history;
DROP TABLE IF EXISTS requests;

View File

@@ -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;

View File

@@ -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'));
})
})

View File

@@ -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

View 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
}

View 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 familys 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
}

View 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;

View 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;

View 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;

View 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;

View File

@@ -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!'))
);
});

View File

@@ -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());
});

View 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)
);
});

View File

@@ -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.'))
);
});

View 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))
);
});

View 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)
);
});

View File

@@ -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

File diff suppressed because it is too large Load Diff