diff --git a/.gitignore b/.gitignore index 407f120..16f9983 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ client/dist src/webserver/access.log conf/development.json yarn-error.log +*/yarn.lock diff --git a/client/app/components/MovieObject.jsx b/client/app/components/MovieObject.jsx index 1349f68..bacf327 100644 --- a/client/app/components/MovieObject.jsx +++ b/client/app/components/MovieObject.jsx @@ -1,8 +1,12 @@ import React from 'react'; +import Notifications, {notify} from 'react-notify-toast'; + // StyleComponents import movieStyle from './styles/movieObjectStyle.jsx'; +var MediaQuery = require('react-responsive'); + class MovieObject { constructor(object) { this.id = object.id; @@ -10,7 +14,9 @@ class MovieObject { 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; } @@ -20,10 +26,12 @@ class MovieObject { } requestMovie() { - // fetch('https://apollo.kevinmidboe.com/api/v1/plex/request/' + id, { - fetch('http://localhost:31459/api/v1/plex/request/' + this.id + '?type='+this.type, { + // 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' }); + + notify.show(this.title + ' requested!', 'success', 3000); } getElement() { @@ -31,8 +39,10 @@ class MovieObject { 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/w154' + this.poster; + 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 = - - - + -
-
- Request new movies or tv shows -
+ +
+
+
+ Request new content +
+ +
+
+ -
-
- + this._handleQueryKeyPress(event)} + onChange={event => this.updateQueryState(event)} + value={this.state.searchQuery}/> - this._handleQueryKeyPress(event)} - onChange={event => this.updateQueryState(event)} - value={this.state.searchQuery}/> +
+
+
- {this.toggleFilter('movies')}} - id="category_active">Movies - {this.toggleFilter('shows')}} - id="category_inactive">TV Shows -
-
-
+
+ {this.state.resultHeader} +



+ + {this.state.responseMovieList} +
+
+ -
- {this.state.responseMovieList} -
-
- - -
- + +
+
+
+ Request new content +
+ +
+
+ + + this._handleQueryKeyPress(event)} + onChange={event => this.updateQueryState(event)} + value={this.state.searchQuery}/> + +
+
+
+ +
+ {this.state.resultHeader} +



+ + {this.state.responseMovieList} +
+
+
+
) } diff --git a/client/app/components/styles/movieObjectStyle.jsx b/client/app/components/styles/movieObjectStyle.jsx index a87c726..627d7dd 100644 --- a/client/app/components/styles/movieObjectStyle.jsx +++ b/client/app/components/styles/movieObjectStyle.jsx @@ -6,21 +6,24 @@ export default { minHeight: '230px' }, - resultItem: { - maxWidth: '95%', - margin: '0 auto', - minHeight: '230px' - }, - movie_content: { marginLeft: '15px' }, - resultTitle: { + resultTitleLarge: { color: 'black', fontSize: '2em', }, + resultTitleSmall: { + color: 'black', + fontSize: '22px', + }, + + yearRatingLarge: { + fontSize: '0.8em' + }, + resultPoster: { float: 'left', zIndex: '3', @@ -28,12 +31,30 @@ export default { 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' }, diff --git a/client/app/components/styles/searchRequestStyle.jsx b/client/app/components/styles/searchRequestStyle.jsx index d02eb5e..1d2f7fc 100644 --- a/client/app/components/styles/searchRequestStyle.jsx +++ b/client/app/components/styles/searchRequestStyle.jsx @@ -6,23 +6,29 @@ export default { margin: 0, padding: 0, minHeight: '100%', - position: 'relative' }, - backgroundHeader: { + backgroundLargeHeader: { width: '100%', minHeight: '400px', backgroundColor: '#011c23', zIndex: 1, - position: 'absolute' + marginBottom: '-100px' + }, + + backgroundSmallHeader: { + width: '100%', + minHeight: '300px', + backgroundColor: '#011c23', + zIndex: 1, + marginBottom: '-100px' }, requestWrapper: { - top: '300px', width: '90%', maxWidth: '1200px', margin: 'auto', - paddingTop: '20px', + // paddingTop: '20px', backgroundColor: 'white', position: 'relative', zIndex: '10', @@ -32,42 +38,46 @@ export default { pageTitle: { display: 'flex', alignItems: 'center', - justifyContent: 'center' + justifyContent: 'center', + textAlign: 'center' }, - pageTitleSpan: { + pageTitleLargeSpan: { color: 'white', fontSize: '3em', marginTop: '4vh', marginBottom: '6vh' }, + pageTitleSmallSpan: { + color: 'white', + fontSize: '2em', + marginTop: '3vh', + marginBottom: '3vh' + }, + box: { - width: '90%', height: '50px', - maxWidth: '1200px', - margin: '0 auto' }, - container: { - verticalAlign: 'middle', - whiteSpace: 'nowrap', - position: 'relative', - display: 'flex', - justifyContent: 'center' + searchLargeContainer: { + margin: '0 25%', + }, + + searchSmallContainer: { + margin: '0 10%', }, searchIcon: { position: 'absolute', - marginLeft: '17px', - marginTop: '17px', - zIndex: '1', + fontSize: '1.2em', + marginTop: '12px', + marginLeft: '-13px', color: '#4f5b66' }, - searchBar: { - width: '60%', - minWidth: '120px', + searchLargeBar: { + width: '100%', height: '50px', background: '#ffffff', border: 'none', @@ -75,19 +85,78 @@ export default { float: 'left', color: '#63717f', paddingLeft: '45px', + marginLeft: '-25px', borderRadius: '5px', - marginRight: '15px' }, - searchFilter: { - color: 'white', + searchSmallBar: { + width: '100%', + height: '50px', + background: '#ffffff', + border: 'none', + fontSize: '13pt', + float: 'left', + color: '#63717f', + paddingLeft: '45px', + marginLeft: '-25px', + borderRadius: '5px', + }, + + searchFilterActive: { + color: '#00d17c', fontSize: '1em', - paddingTop: '12px', - marginBottom: '12px', marginLeft: '10px', cursor: 'pointer' }, + searchFilterNotActive: { + color: 'white', + fontSize: '1em', + marginLeft: '10px', + cursor: 'pointer' + }, + + + filter: { + color: 'white', + paddingLeft: '40px', + width: '60%', + }, + + resultLargeHeader: { + paddingLeft: '30px', + color: 'black', + fontSize: '2em', + }, + + resultSmallHeader: { + paddingLeft: '30px', + color: 'black', + fontSize: '1.7em', + }, + + 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', diff --git a/client/app/index.html b/client/app/index.html index 91f8c81..3ab7f3f 100644 --- a/client/app/index.html +++ b/client/app/index.html @@ -3,7 +3,8 @@ - + + seasoned Shows diff --git a/client/package.json b/client/package.json index de0ddca..e6fe23f 100644 --- a/client/package.json +++ b/client/package.json @@ -13,7 +13,12 @@ "html-webpack-plugin": "^2.28.0", "path": "^0.12.7", "react": "^15.6.1", + "react-burger-menu": "^2.1.6", "react-dom": "^15.5.4", + "react-infinite-scroller": "^1.0.15", + "react-notify-toast": "^0.3.2", + "react-responsive": "^1.3.4", + "urijs": "^1.18.12", "webfontloader": "^1.6.28", "webpack": "^3.5.5", "webpack-dev-server": "^2.4.5" diff --git a/seasoned_api/src/plex/mailTemplate.js b/seasoned_api/src/plex/mailTemplate.js index eb4d359..c07e34f 100644 --- a/seasoned_api/src/plex/mailTemplate.js +++ b/seasoned_api/src/plex/mailTemplate.js @@ -2,7 +2,7 @@ class mailTemplate { constructor(mediaItem) { this.mediaItem = mediaItem; - this.posterURL = 'https://image.tmdb.org/t/p/w600/'; + this.posterURL = 'https://image.tmdb.org/t/p/w600'; } toText() { diff --git a/seasoned_api/src/plex/requestRepository.js b/seasoned_api/src/plex/requestRepository.js index 48c744b..0af3f2f 100644 --- a/seasoned_api/src/plex/requestRepository.js +++ b/seasoned_api/src/plex/requestRepository.js @@ -7,6 +7,8 @@ const tmdb = new TMDB(configuration.get('tmdb', 'apiKey')); var Promise = require('bluebird'); var rp = require('request-promise'); +const establishedDatabase = require('src/database/database'); + const MailTemplate = require('src/plex/mailTemplate') var pythonShell = require('python-shell'); @@ -15,6 +17,13 @@ const nodemailer = require('nodemailer'); class RequestRepository { + constructor(database) { + this.database = database || establishedDatabase; + this.queries = { + 'insertRequest': "INSERT INTO requests VALUES (?, ?, ?, ?, ?, ?, CURRENT_DATE)" + } + } + searchRequest(query, page, type) { // TODO get from cache // STRIP METADATA THAT IS NOT ALLOWED @@ -99,23 +108,31 @@ class RequestRepository { * @param {identifier, type} the id of the media object and type of media must be defined * @returns {Promise} If nothing has gone wrong. */ - sendRequest(identifier, type) { + sendRequest(identifier, type, ip) { // TODO add to DB so can have a admin page // TODO try a cache hit on the movie item tmdb.lookup(identifier, type).then(movie => { + // Add request to database + this.database.run(this.queries.insertRequest, [movie.id, movie.title, movie.year, movie.poster, 'NULL', ip]) + + + // + + // create reusable transporter object using the default SMTP transport let transporter = nodemailer.createTransport({ - host: configuration.get('mail', 'host'), - port: 26, - ignoreTLS: true, - tls :{rejectUnauthorized: false}, - secure: false, // secure:true for port 465, secure:false for port 587 + service: 'gmail', auth: { - user: configuration.get('mail', 'user'), - pass: configuration.get('mail', 'password') + user: configuration.get('mail', 'user_pi'), + pass: configuration.get('mail', 'password_pi') } + // host: configuration.get('mail', 'host'), + // port: 26, + // ignoreTLS: true, + // tls :{rejectUnauthorized: false}, + // secure: false, // secure:true for port 465, secure:false for port 587 }); const mailTemplate = new MailTemplate(movie) @@ -123,7 +140,7 @@ class RequestRepository { // setup email data with unicode symbols let mailOptions = { // TODO get the mail adr from global location (easy to add) - from: 'MovieRequester ', // sender address + from: 'MovieRequester ', // sender address to: 'kevin.midboe@gmail.com', // list of receivers subject: 'Download request', // Subject line text: mailTemplate.toText(), diff --git a/seasoned_api/src/tmdb/convertTmdbToSeasoned.js b/seasoned_api/src/tmdb/convertTmdbToSeasoned.js index 79f2d00..1643e8d 100644 --- a/seasoned_api/src/tmdb/convertTmdbToSeasoned.js +++ b/seasoned_api/src/tmdb/convertTmdbToSeasoned.js @@ -2,10 +2,13 @@ const Movie = require('src/media_classes/movie'); const Show = require('src/media_classes/show'); function convertTmdbToSeasoned(tmdbObject, strictType=undefined) { - if (strictType === undefined) + // TODO create a default fallback class to set the when falls to else as both are undefined + if (tmdbObject.media_type !== undefined) var mediaType = tmdbObject.media_type; + else if (strictType !== undefined) + var mediaType = strictType; else - var mediaType = strictType; + var mediaType = 'movie'; // There are many diff types of content, we only want to look at movies and tv shows if (mediaType === 'movie') { diff --git a/seasoned_api/src/tmdb/tmdb.js b/seasoned_api/src/tmdb/tmdb.js index 236c1de..76e16f7 100644 --- a/seasoned_api/src/tmdb/tmdb.js +++ b/seasoned_api/src/tmdb/tmdb.js @@ -22,15 +22,16 @@ class TMDB { return Promise.resolve() .then(() => this.tmdb(type, query)) // Search the tmdb api .catch(() => { throw new Error('Could not search for movies.'); }) // If any error at all when fetching - .then((reponse) => { + .then((response) => { try { // We want to filter because there are movies really low rated that are not interesting to us. - let filteredTmdbItems = reponse.results.filter(function(tmdbResultItem) { + let filteredTmdbItems = response.results.filter(function(tmdbResultItem) { return ((tmdbResultItem.vote_count >= 80 || tmdbResultItem.popularity > 18) && (tmdbResultItem.release_date !== undefined || tmdbResultItem.first_air_date !== undefined)) }) // Here we convert the filtered result from the tmdb api to seaonsed objects let seasonedItems = filteredTmdbItems.map((tmdbItem) => { + if (type === 'movie') return convertTmdbToSeasoned(tmdbItem, 'movie'); else if (type === 'show') @@ -40,7 +41,7 @@ class TMDB { }); // TODO add page number if results are larger than 20 - return { 'results': seasonedItems, 'number_of_items_on_page': seasonedItems, + return { 'results': seasonedItems, 'number_of_items_on_page': seasonedItems.length, 'page': 1, 'total_pages': 1 }; } catch (parseError) { diff --git a/seasoned_api/src/webserver/controllers/plex/submitRequest.js b/seasoned_api/src/webserver/controllers/plex/submitRequest.js index fd6345f..f8ff4c2 100644 --- a/seasoned_api/src/webserver/controllers/plex/submitRequest.js +++ b/seasoned_api/src/webserver/controllers/plex/submitRequest.js @@ -12,8 +12,9 @@ function submitRequestController(req, res) { // This is the id that is the param of the url const id = req.params.mediaId; const type = req.query.type; + var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress; - requestRepository.sendRequest(id, type) + requestRepository.sendRequest(id, type, ip) .then(() => { res.send({ success: true, message: 'Media item sucessfully requested!' }); })