From 372ec1b241dbacdb6dd983d3681157f578b09a33 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Thu, 21 Sep 2017 14:36:41 +0200 Subject: [PATCH 01/18] Rewrote most of how api calls are made when searching for a movie and how the returning data is handled. We now have finer handling of status response from the original api call aswell as if any of the functions hit a error e.g. not hitting the server, than we have our own errors thrown. Also updated the page incrementers to updated the last api call with a higher or lower number. --- client/app/components/SearchRequest.jsx | 243 ++++++++++++++++++------ 1 file changed, 187 insertions(+), 56 deletions(-) diff --git a/client/app/components/SearchRequest.jsx b/client/app/components/SearchRequest.jsx index 7bed5a9..2f5c31b 100644 --- a/client/app/components/SearchRequest.jsx +++ b/client/app/components/SearchRequest.jsx @@ -4,6 +4,7 @@ import MovieObject from './MovieObject.jsx'; // StyleComponents import searchStyle from './styles/searchRequestStyle.jsx'; +import movieStyle from './styles/movieObjectStyle.jsx' import URI from 'urijs'; @@ -13,23 +14,25 @@ class SearchRequest extends React.Component { super(props) // Constructor with states holding the search query and the element of reponse. this.state = { + lastApiCallURI: '', searchQuery: '', responseMovieList: null, movieFilter: true, showFilter: false, discoverType: '', - page: 1 + page: 1, + resultHeader: '' } - this.allowedDiscoverTypes = [ + this.allowedListTypes = [ 'discover', 'popular', 'nowplaying', 'upcoming' ] - this.baseUrl = 'https://apollo.kevinmidboe.com/api/v1/tmdb'; + this.baseUrl = 'https://apollo.kevinmidboe.com/api/v1/tmdb/'; // this.baseUrl = 'http://localhost:31459/api/v1/tmdb/'; this.URLs = { - request: 'https://apollo.kevinmidboe.com/api/v1/plex/request?page='+this.state.page+'&query=', + searchRequest: 'https://apollo.kevinmidboe.com/api/v1/plex/request', // request: 'http://localhost:31459/api/v1/plex/request?page='+this.state.page+'&query=', upcoming: 'https://apollo.kevinmidboe.com/api/v1/tmdb/upcoming', // upcoming: 'http://localhost:31459/api/v1/tmdb/upcoming', @@ -45,26 +48,125 @@ class SearchRequest extends React.Component { this.fetchDiscover('upcoming'); } - // Handles all errors of the response of a fetch call - handleErrors(response) { - if (!response.ok) { - throw Error(response.status); + // Handles all errors of the response of a fetch call + handleErrors(response) { + if (!response.ok) + throw Error(response.status); + return response; } - return response; - } + + handleQueryError(response) { + if (!response.ok) { + if (response.status === 404) { + this.setState({ + responseMovieList:

Nothing found for search query: { this.findQueryInURI(uri) }

+ }) + } + console.log(error); + } + return response; + } + + // Unpacks the query value of a uri + findQueryValueInURI(uri) { + let uriSearchValues = uri.query(true); + let queryValue = uriSearchValues['query'] + + return queryValue; + } + + resetPageNumber() { + this.state.page = 1; + } + + // Test this by calling missing endpoint or 404 query and see what code + // and filter the error message based on the code. + // Calls a uri and returns the response as json + callURI(uri) { + return fetch(uri) + .then(response => { return response }) + .catch(error => { + throw Error('Something went wrong while fetching URI.'); + }); + } + + // Saves the input string as a h1 element in responseMovieList state + fillResponseMovieListWithError(msg) { + this.setState({ + responseMovieList:

{ msg }

+ }) + } + + // 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) { + Promise.resolve() + .then(() => this.callURI(uri)) + .then(response => { + // If we get a error code for the request + if (!response.ok) { + if (response.status === 404) { + let errorMsg = 'Nothing found for the search query: ' + this.findQueryValueInURI(uri); + this.fillResponseMovieListWithError(errorMsg) + } + else { + let errorMsg = 'Error fetching query from server ' + this.response.status; + this.fillResponseMovieListWithError(errorMsg) + } + } + + // Convert to json and update the state of responseMovieList with the results of the api call + // mapped as a movieObject. + response.json() + .then(responseData => { + this.setState({ + responseMovieList: responseData.results.map(searchResultItem => this.createMovieObjects(searchResultItem)), + lastApiCallURI: uri // Save the value of the last sucessfull api call + }) + }) + }) + .catch(() => { + throw Error('Something went wrong when fetching query.') + }) + } + + searchSeasonedRequest() { + // Build uri with the url for searching requests + var uri = new URI(this.URLs.searchRequest); + // Add input of search query and page count to the uri payload + uri = uri.search({ 'query': this.state.searchQuery, 'page': this.state.page }); + + if (this.state.showFilter) + uri = uri.addSearch('type', 'show'); + + + // Send uri to call and fill the response list with movie/show objects + this.callSearchFillMovieList(uri); + } + + + + + fetchDiscover(queryDiscoverType) { - if (this.allowedDiscoverTypes.indexOf(queryDiscoverType) === -1) + if (this.allowedListTypes.indexOf(queryDiscoverType) === -1) throw Error('Invalid discover type: ' + queryDiscoverType); + // Captialize the first letter of and save the discoverQueryType to resultHeader state. + this.state.resultHeader = queryDiscoverType.toLowerCase().replace(/\b[a-z]/g, function(letter) { + return letter.toUpperCase(); + }); + var uri = new URI(this.baseUrl); uri.segment(queryDiscoverType) uri = uri.setSearch('page', this.state.page); if (this.state.showFilter) uri = uri.addSearch('type', 'show'); - console.log(uri) + + this.state.lastApiCallURI = uri; this.setState({ responseMovieList: 'Loading...' @@ -96,11 +198,15 @@ class SearchRequest extends React.Component { fetchQuery() { - let url = this.URLs.request + this.state.searchQuery + let url = this.URLs.request + this.state.searchQuery; if (this.state.showFilter) { url = url + '&type=tv' } + this.state.apiQuery = url; + console.log(this.state.apiQuery.toString()); + this.state.resultHeader = "Results for: " + this.state.searchQuery + ""; + fetch(url) // Check if the response is ok .then(response => this.handleErrors(response)) @@ -135,7 +241,10 @@ class SearchRequest extends React.Component { // For checking if the enter key was pressed in the search field. _handleQueryKeyPress(e) { if (e.key === 'Enter') { - this.fetchQuery(); + // this.fetchQuery(); + // Reset page number for a new search + this.resetPageNumber(); + this.searchSeasonedRequest(); } } @@ -161,21 +270,39 @@ class SearchRequest extends React.Component { } } - pageBackwards() { - if (this.state.page > 1) { - console.log('backwards'); - this.state.page--; - this.getUpcoming(); - } - console.log(this.state.page) - } + pageBackwards() { + if (this.state.page > 1) { + let pageNumber = this.state.page - 1; + let uri = this.state.lastApiCallURI; + + // Augment the page number of the uri with a callback + uri.search(function(data) { + data.page = pageNumber; + }); - pageForwards() { - this.state.page++; - this.getUpcoming(); - console.log('forwards'); - console.log(this.state.page) - } + // Call the api with the new uri + this.callSearchFillMovieList(uri); + // Update state of our page number after the call is done + this.state.page = pageNumber; + } + } + + // TODO need to get total page number and save in a state to not overflow + pageForwards() { + // Wrap this in the check + let pageNumber = this.state.page + 1; + let uri = this.state.lastApiCallURI; + + // Augment the page number of the uri with a callback + uri.search(function(data) { + data.page = pageNumber; + }); + + // Call the api with the new uri + this.callSearchFillMovieList(uri); + // Update state of our page number after the call is done + this.state.page = pageNumber; + } render(){ @@ -186,40 +313,44 @@ class SearchRequest extends React.Component { -
-
- Request new movies or tv shows -
+
+
+ Request new movies or tv shows +
-
-
- +
+
+ - 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.toggleFilter('movies')}} + id="category_active">Movies + {this.toggleFilter('shows')}} + id="category_inactive">TV Shows
+
-
- {this.state.responseMovieList} -
-
- - -
-
+
+ +

{this.state.resultHeader}

+ + {this.state.responseMovieList} +
+ +
+ + +
+
) } From c79d5dbc6e075c4a141eb68e8359342933ced4f4 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Thu, 21 Sep 2017 15:01:51 +0200 Subject: [PATCH 02/18] Also rewrote the funtionality for fetching lists from tmdb. It is now done in a similar fashion as searching requests, but with its own error handling and messaging. --- client/app/components/SearchRequest.jsx | 157 +++++++++++------------- 1 file changed, 72 insertions(+), 85 deletions(-) diff --git a/client/app/components/SearchRequest.jsx b/client/app/components/SearchRequest.jsx index 2f5c31b..c0b78ea 100644 --- a/client/app/components/SearchRequest.jsx +++ b/client/app/components/SearchRequest.jsx @@ -45,7 +45,7 @@ class SearchRequest extends React.Component { componentDidMount(){ var that = this; // this.setState({responseMovieList: null}) - this.fetchDiscover('upcoming'); + this.fetchTmdbList('upcoming'); } // Handles all errors of the response of a fetch call @@ -79,6 +79,12 @@ class SearchRequest extends React.Component { this.state.page = 1; } + writeLoading() { + this.setState({ + responseMovieList: 'Loading...' + }); + } + // Test this by calling missing endpoint or 404 query and see what code // and filter the error message based on the code. // Calls a uri and returns the response as json @@ -97,9 +103,13 @@ 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(); + Promise.resolve() .then(() => this.callURI(uri)) .then(response => { @@ -130,6 +140,40 @@ class SearchRequest extends React.Component { }) } + callListFillMovieList(uri) { + // Write loading animation + // this.writeLoading(); + + Promise.resolve() + .then(() => this.callURI(uri)) + .then(response => { + // If we get a error code for the request + if (!response.ok) { + if (response.status === 404) { + let errorMsg = 'List not found'; + this.fillResponseMovieListWithError(errorMsg) + } + else { + let errorMsg = 'Error fetching list from server ' + this.response.status; + this.fillResponseMovieListWithError(errorMsg) + } + } + + // Convert to json and update the state of responseMovieList with the results of the api call + // mapped as a movieObject. + response.json() + .then(responseData => { + this.setState({ + responseMovieList: responseData.results.map(searchResultItem => this.createMovieObjects(searchResultItem)), + lastApiCallURI: uri // Save the value of the last sucessfull api call + }) + }) + }) + .catch(() => { + throw Error('Something went wrong when fetching query.') + }) + } + searchSeasonedRequest() { // Build uri with the url for searching requests var uri = new URI(this.URLs.searchRequest); @@ -138,98 +182,41 @@ class SearchRequest extends React.Component { if (this.state.showFilter) uri = uri.addSearch('type', 'show'); - // Send uri to call and fill the response list with movie/show objects this.callSearchFillMovieList(uri); } + fetchTmdbList(tmdbListType) { + // Check if it is a whitelisted list, this should be replaced with checking if the return call is 500 + if (this.allowedListTypes.indexOf(tmdbListType) === -1) + throw Error('Invalid discover type: ' + tmdbListType); + // Captialize the first letter of and save the discoverQueryType to resultHeader state. + this.state.resultHeader = tmdbListType.toLowerCase().replace(/\b[a-z]/g, function(letter) { + return letter.toUpperCase(); + }); + // Build uri with the url for searching requests + var uri = new URI(this.baseUrl); + uri.segment(tmdbListType); + // Add input of search query and page count to the uri payload + uri = uri.search({ 'page': this.state.page }); + + if (this.state.showFilter) + uri = uri.addSearch('type', 'show'); - - - - fetchDiscover(queryDiscoverType) { - if (this.allowedListTypes.indexOf(queryDiscoverType) === -1) - throw Error('Invalid discover type: ' + queryDiscoverType); - - // Captialize the first letter of and save the discoverQueryType to resultHeader state. - this.state.resultHeader = queryDiscoverType.toLowerCase().replace(/\b[a-z]/g, function(letter) { - return letter.toUpperCase(); - }); - - var uri = new URI(this.baseUrl); - uri.segment(queryDiscoverType) - uri = uri.setSearch('page', this.state.page); - if (this.state.showFilter) - uri = uri.addSearch('type', 'show'); - - - this.state.lastApiCallURI = uri; - - this.setState({ - responseMovieList: 'Loading...' - }); - - fetch(uri) - // Check if the response is ok - .then(response => this.handleErrors(response)) - .then(response => response.json()) // Convert to json object and pass to next then - .then(data => { // Parse the data of the JSON response - // If it is something here it updates the state variable with the HTML list of all - // movie objects that where returned by the search request - if (data.results.length > 0) { - this.setState({ - responseMovieList: data.results.map(item => this.createMovieObjects(item)) - }) - } - }) - // If the -------- - .catch(error => { - console.log(error) - this.setState({ - responseMovieList:

Not Found

- }) - - console.log('Error submit: ', error.toString()); - }); - } - - - fetchQuery() { - let url = this.URLs.request + this.state.searchQuery; - if (this.state.showFilter) { - url = url + '&type=tv' + // Send uri to call and fill the response list with movie/show objects + this.callListFillMovieList(uri); } - this.state.apiQuery = url; - console.log(this.state.apiQuery.toString()); - this.state.resultHeader = "Results for: " + this.state.searchQuery + ""; - fetch(url) - // Check if the response is ok - .then(response => this.handleErrors(response)) - .then(response => response.json()) // Convert to json object and pass to next then - .then(data => { // Parse the data of the JSON response - // If it is something here it updates the state variable with the HTML list of all - // movie objects that where returned by the search request - if (data.results.length > 0) { - this.setState({ - responseMovieList: data.results.map(item => this.createMovieObjects(item)) - }) - } - }) - // If the -------- - .catch(error => { - console.log(error) - this.setState({ - responseMovieList:

Not Found

- }) - console.log('Error submit: ', error.toString()); - }); - } + + + + + // Updates the internal state of the query search field. updateQueryState(event){ @@ -308,10 +295,10 @@ class SearchRequest extends React.Component { render(){ return(
- - - - + + + +
From 00d000b8f8264599c4e57508ea6275fa29c2c408 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Thu, 21 Sep 2017 15:03:33 +0200 Subject: [PATCH 03/18] Started working in the infinate scroll so it loads the next page when visible on page. This commit was under_development bugs as of now. --- client/app/components/SearchRequest.jsx | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/client/app/components/SearchRequest.jsx b/client/app/components/SearchRequest.jsx index c0b78ea..41edd5c 100644 --- a/client/app/components/SearchRequest.jsx +++ b/client/app/components/SearchRequest.jsx @@ -129,10 +129,24 @@ class SearchRequest extends React.Component { // mapped as a movieObject. response.json() .then(responseData => { - this.setState({ - responseMovieList: responseData.results.map(searchResultItem => this.createMovieObjects(searchResultItem)), - lastApiCallURI: uri // Save the value of the last sucessfull api call - }) + if (this.state.page === 1) { + this.setState({ + responseMovieList: responseData.results.map(searchResultItem => this.createMovieObjects(searchResultItem)), + lastApiCallURI: uri // Save the value of the last sucessfull api call + }) + } else { + let responseMovieObjects = responseData.results.map(searchResultItem => this.createMovieObjects(searchResultItem)); + console.log(responseMovieObjects) + console.log(this.state.responseMovieList) + let growingReponseMovieObjectList = this.state.responseMovieList.concat(responseMovieObjects); + this.setState({ + responseMovieList: growingReponseMovieObjectList, + lastApiCallURI: uri // Save the value of the last sucessfull api call + }) + } + }) + .catch((error) => { + console.log(error) }) }) .catch(() => { @@ -216,8 +230,6 @@ class SearchRequest extends React.Component { - - // Updates the internal state of the query search field. updateQueryState(event){ this.setState({ From b219242787cef6da42755ecc452dcc18b04f7143 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Thu, 21 Sep 2017 15:04:53 +0200 Subject: [PATCH 04/18] Added infinate scroll to lists also. --- client/app/components/SearchRequest.jsx | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/client/app/components/SearchRequest.jsx b/client/app/components/SearchRequest.jsx index 41edd5c..2102cd6 100644 --- a/client/app/components/SearchRequest.jsx +++ b/client/app/components/SearchRequest.jsx @@ -136,8 +136,6 @@ class SearchRequest extends React.Component { }) } else { let responseMovieObjects = responseData.results.map(searchResultItem => this.createMovieObjects(searchResultItem)); - console.log(responseMovieObjects) - console.log(this.state.responseMovieList) let growingReponseMovieObjectList = this.state.responseMovieList.concat(responseMovieObjects); this.setState({ responseMovieList: growingReponseMovieObjectList, @@ -177,10 +175,19 @@ class SearchRequest extends React.Component { // mapped as a movieObject. response.json() .then(responseData => { - this.setState({ - responseMovieList: responseData.results.map(searchResultItem => this.createMovieObjects(searchResultItem)), - lastApiCallURI: uri // Save the value of the last sucessfull api call - }) + if (this.state.page === 1) { + this.setState({ + responseMovieList: responseData.results.map(searchResultItem => this.createMovieObjects(searchResultItem)), + lastApiCallURI: uri // Save the value of the last sucessfull api call + }) + } else { + let responseMovieObjects = responseData.results.map(searchResultItem => this.createMovieObjects(searchResultItem)); + let growingReponseMovieObjectList = this.state.responseMovieList.concat(responseMovieObjects); + this.setState({ + responseMovieList: growingReponseMovieObjectList, + lastApiCallURI: uri // Save the value of the last sucessfull api call + }) + } }) }) .catch(() => { From b2f9d6f5f54e1df51c14cabb7e620bf05e8cbf83 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Tue, 26 Sep 2017 20:34:54 +0200 Subject: [PATCH 05/18] Added rating and background to the class constructor. Added type variable to url when requesting a movie/show and added a notification agent. Now this page is also updated to support mobile formatting, when tilted it shows the background image instead of poster image. --- client/app/components/MovieObject.jsx | 43 ++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 7 deletions(-) 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 -
- -
-
- - - this._handleQueryKeyPress(event)} - onChange={event => this.updateQueryState(event)} - value={this.state.searchQuery}/> - - {this.toggleFilter('movies')}} id="category_active">Movies - {this.toggleFilter('movies')}} + id="category_active">Movies + } + + showToggle() { + if (this.state.showFilter) + return {this.toggleFilter('shows')}} - id="category_inactive">TV Shows -
-
-
+ id="category_active">TV Shows + else + return {this.toggleFilter('shows')}} + id="category_active">TV Shows + } -
- -

{this.state.resultHeader}

- - {this.state.responseMovieList} -
- -
- - -
-
+ + render(){ + const loader =
Loading ...

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

{this.state.resultHeader}

+ + {this.state.responseMovieList} +
+ +
+
+
+
) } From 5341e940c6c012e93de044eb09efbe830b751f3b Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Tue, 26 Sep 2017 20:37:24 +0200 Subject: [PATCH 07/18] Added viewport width to header of main index.html page. --- client/app/index.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 From 1af9368a6c7e5f576b5421be108c5e8f5952e616 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Tue, 26 Sep 2017 20:38:13 +0200 Subject: [PATCH 08/18] Updated stylesheets to reflect changes to request and movieObject pages. --- .../components/styles/movieObjectStyle.jsx | 35 ++++++-- .../components/styles/searchRequestStyle.jsx | 79 +++++++++++++------ 2 files changed, 84 insertions(+), 30 deletions(-) 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..18549b4 100644 --- a/client/app/components/styles/searchRequestStyle.jsx +++ b/client/app/components/styles/searchRequestStyle.jsx @@ -6,7 +6,6 @@ export default { margin: 0, padding: 0, minHeight: '100%', - position: 'relative' }, backgroundHeader: { @@ -14,15 +13,14 @@ export default { minHeight: '400px', backgroundColor: '#011c23', zIndex: 1, - position: 'absolute' + marginBottom: '-100px' }, requestWrapper: { - top: '300px', width: '90%', maxWidth: '1200px', margin: 'auto', - paddingTop: '20px', + // paddingTop: '20px', backgroundColor: 'white', position: 'relative', zIndex: '10', @@ -32,7 +30,8 @@ export default { pageTitle: { display: 'flex', alignItems: 'center', - justifyContent: 'center' + justifyContent: 'center', + textAlign: 'center' }, pageTitleSpan: { @@ -43,31 +42,23 @@ export default { }, box: { - width: '90%', height: '50px', - maxWidth: '1200px', - margin: '0 auto' }, container: { - verticalAlign: 'middle', - whiteSpace: 'nowrap', - position: 'relative', - display: 'flex', - justifyContent: 'center' + margin: '0 25%' }, searchIcon: { position: 'absolute', - marginLeft: '17px', - marginTop: '17px', - zIndex: '1', + fontSize: '1.2em', + marginTop: '12px', + marginLeft: '-13px', color: '#4f5b66' }, searchBar: { - width: '60%', - minWidth: '120px', + width: '100%', height: '50px', background: '#ffffff', border: 'none', @@ -75,19 +66,61 @@ export default { float: 'left', color: '#63717f', paddingLeft: '45px', + marginLeft: '-25px', borderRadius: '5px', - marginRight: '15px' }, - searchFilter: { - color: 'white', + 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%', + }, + + resultHeader: { + paddingLeft: '30px', + paddingTop: '15px', + marginBottom: '40px', + color: 'black', + // color: '#00d17c' + }, + + 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', From 1321671840b57ee14671f20fe3c20b99c2c5bc6e Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Tue, 26 Sep 2017 20:40:11 +0200 Subject: [PATCH 09/18] Added infinite-scroller, notify-toast, urijs. Also burger-menu for later support for selecting discover lists. --- client/package.json | 5 +++++ 1 file changed, 5 insertions(+) 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" From 6f54a612232cd10ca2649554e27cdc55a8a218de Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Tue, 26 Sep 2017 20:41:25 +0200 Subject: [PATCH 10/18] Removed a extra / that was in the posterURL --- seasoned_api/src/plex/mailTemplate.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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() { From d47f2bf7573e486b4d2403bc9c32139e0170d57b Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Tue, 26 Sep 2017 20:42:49 +0200 Subject: [PATCH 11/18] Added support for database operations. Now when requesting a item, it is saved to a database and sends a email from pi.midboe account. --- seasoned_api/src/plex/requestRepository.js | 35 ++++++++++++++++------ 1 file changed, 26 insertions(+), 9 deletions(-) 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(), From daa9a7749eb805fe11270d912853e3058f5972e8 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Tue, 26 Sep 2017 20:43:56 +0200 Subject: [PATCH 12/18] Now also the ip address is passed to the sendRequest function in requestRepository so that the requesters ip address can be logged in the database. --- seasoned_api/src/webserver/controllers/plex/submitRequest.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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!' }); }) From 86c479de15d6aff81428abe788f58c6f20bceb14 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Wed, 27 Sep 2017 00:18:06 +0200 Subject: [PATCH 13/18] Changed so that the output is split in two idependent mediaQuery items at the second most parent. This is a lazy way to controll all elements of when resizing to a smaller screen. --- client/app/components/SearchRequest.jsx | 76 +++++++++++++++++-------- 1 file changed, 53 insertions(+), 23 deletions(-) diff --git a/client/app/components/SearchRequest.jsx b/client/app/components/SearchRequest.jsx index 38b1cfb..b541f8d 100644 --- a/client/app/components/SearchRequest.jsx +++ b/client/app/components/SearchRequest.jsx @@ -9,6 +9,8 @@ import movieStyle from './styles/movieObjectStyle.jsx'; import URI from 'urijs'; import InfiniteScroll from 'react-infinite-scroller'; +var MediaQuery = require('react-responsive'); + // TODO add option for searching multi, movies or tv shows class SearchRequest extends React.Component { constructor(props){ @@ -36,7 +38,7 @@ class SearchRequest extends React.Component { this.URLs = { searchRequest: 'https://apollo.kevinmidboe.com/api/v1/plex/request', - // request: 'http://localhost:31459/api/v1/plex/request?page='+this.state.page+'&query=', + // searchRequest: 'http://localhost:31459/api/v1/plex/request', upcoming: 'https://apollo.kevinmidboe.com/api/v1/tmdb/upcoming', // upcoming: 'http://localhost:31459/api/v1/tmdb/upcoming', sendRequest: 'https://apollo.kevinmidboe.com/api/v1/plex/request?query=' @@ -379,35 +381,63 @@ class SearchRequest extends React.Component { loader={loader} initialLoad={this.state.loadResults}> -
-
-
- Request new content -
+ +
+
+
+ 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.state.resultHeader}

- - {this.state.responseMovieList} +
+ {this.state.resultHeader} +



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



+ + {this.state.responseMovieList} +
-
+ ) } From 6bd1b29d5e7119f1c3adedcf4b0d43085ed0cfab Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Wed, 27 Sep 2017 00:19:19 +0200 Subject: [PATCH 14/18] Changed the font size of the header, space between the search bar and result content. Also changed so that the font size in the search bar is large enough to not zoom on ios because of smaller than standard font size. --- .../components/styles/searchRequestStyle.jsx | 54 +++++++++++++++---- 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/client/app/components/styles/searchRequestStyle.jsx b/client/app/components/styles/searchRequestStyle.jsx index 18549b4..1d2f7fc 100644 --- a/client/app/components/styles/searchRequestStyle.jsx +++ b/client/app/components/styles/searchRequestStyle.jsx @@ -8,7 +8,7 @@ export default { minHeight: '100%', }, - backgroundHeader: { + backgroundLargeHeader: { width: '100%', minHeight: '400px', backgroundColor: '#011c23', @@ -16,6 +16,14 @@ export default { marginBottom: '-100px' }, + backgroundSmallHeader: { + width: '100%', + minHeight: '300px', + backgroundColor: '#011c23', + zIndex: 1, + marginBottom: '-100px' + }, + requestWrapper: { width: '90%', maxWidth: '1200px', @@ -34,19 +42,30 @@ export default { textAlign: 'center' }, - pageTitleSpan: { + pageTitleLargeSpan: { color: 'white', fontSize: '3em', marginTop: '4vh', marginBottom: '6vh' }, + pageTitleSmallSpan: { + color: 'white', + fontSize: '2em', + marginTop: '3vh', + marginBottom: '3vh' + }, + box: { height: '50px', }, - container: { - margin: '0 25%' + searchLargeContainer: { + margin: '0 25%', + }, + + searchSmallContainer: { + margin: '0 10%', }, searchIcon: { @@ -57,7 +76,7 @@ export default { color: '#4f5b66' }, - searchBar: { + searchLargeBar: { width: '100%', height: '50px', background: '#ffffff', @@ -70,6 +89,19 @@ export default { borderRadius: '5px', }, + 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', @@ -91,12 +123,16 @@ export default { width: '60%', }, - resultHeader: { + resultLargeHeader: { paddingLeft: '30px', - paddingTop: '15px', - marginBottom: '40px', color: 'black', - // color: '#00d17c' + fontSize: '2em', + }, + + resultSmallHeader: { + paddingLeft: '30px', + color: 'black', + fontSize: '1.7em', }, row: { From d787fba024beb3cd4aec41065b2b991c97ca5ebf Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Wed, 27 Sep 2017 00:22:22 +0200 Subject: [PATCH 15/18] Changed a smellingerror (reponse -> response). Also now pass the type of the search request when converting to seasoned object. Fixed issue where number_of_items_on_page was not set to the length of the list. --- seasoned_api/src/tmdb/tmdb.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/seasoned_api/src/tmdb/tmdb.js b/seasoned_api/src/tmdb/tmdb.js index 3657679..5e04dd4 100644 --- a/seasoned_api/src/tmdb/tmdb.js +++ b/seasoned_api/src/tmdb/tmdb.js @@ -22,20 +22,20 @@ 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) => { - return convertTmdbToSeasoned(tmdbItem); + return convertTmdbToSeasoned(tmdbItem, type); }); // 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) { From 8014f01766d0b50eba1dd00df66a3ed6867d5ad9 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Wed, 27 Sep 2017 00:28:41 +0200 Subject: [PATCH 16/18] Added stricter handling for selecting what mediaType to convert to when searching tmdb and converting to seasoned object. --- seasoned_api/src/tmdb/convertTmdbToSeasoned.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/seasoned_api/src/tmdb/convertTmdbToSeasoned.js b/seasoned_api/src/tmdb/convertTmdbToSeasoned.js index 7379610..739e2d0 100644 --- a/seasoned_api/src/tmdb/convertTmdbToSeasoned.js +++ b/seasoned_api/src/tmdb/convertTmdbToSeasoned.js @@ -2,7 +2,13 @@ const Movie = require('src/media_classes/movie'); const Show = require('src/media_classes/show'); function convertTmdbToSeasoned(tmdbObject, strictType=undefined) { - const mediaType = strictType || tmdbObject.media_type; + // 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 = 'movie'; // There are many diff types of content, we only want to look at movies and tv shows if (mediaType === 'movie') { From 6d0a91dc93f18fb994f5b69cc7e96635745b511c Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Wed, 27 Sep 2017 00:29:28 +0200 Subject: [PATCH 17/18] Changed the default filter values to be multi until a filter selector is implemented. --- client/app/components/SearchRequest.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/app/components/SearchRequest.jsx b/client/app/components/SearchRequest.jsx index b541f8d..1f488dc 100644 --- a/client/app/components/SearchRequest.jsx +++ b/client/app/components/SearchRequest.jsx @@ -20,7 +20,7 @@ class SearchRequest extends React.Component { lastApiCallURI: '', searchQuery: '', responseMovieList: null, - movieFilter: true, + movieFilter: false, showFilter: false, discoverType: '', page: 1, From 44d8dbfe0c45ceca2b0e0ae3f1701ebeef23cec1 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Wed, 27 Sep 2017 00:30:51 +0200 Subject: [PATCH 18/18] Updated gitignore to include yarn.locks. --- .gitignore | 1 + 1 file changed, 1 insertion(+) 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