diff --git a/README.md b/README.md index 6af9825..8b34d0b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ -# *Seasoned*: an intelligent organizer for your shows +# 🌶 seasonedShows +Your customly seasoned movie and show requester, downloader and organizer -*Seasoned* 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. +## About +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. ## 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. @@ -8,3 +10,21 @@ The flow of the system will first check for new folders in your tv shows directo Then there is a script for looking for replies on twitter by user_admin, if caanges are needed, it handles the changes specified and updates dtabbase. After approval by user the files are modified and moved to folders in resptected area. If error occours, pasteee link if log is sent to user. + +#### External + + Seasoned: request, discover and manage. + + Stray: Overview of downloaded episodes before they are organized. + + (+) Admin Panel: Overview of all stray episodes/movies. + +#### Api + + All communication between public website to server. + + Plex: All querying to what is localy available in your plex library. + + Stray (seasoned) -> also calls services (moveStray) through api. + + Tmdb: Requesting information from tmdb. + + (+) Admin Panel: Use secure login and session tokens to handle logged in viewer. + +#### Services + + 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. \ No newline at end of file diff --git a/_config.yml b/_config.yml new file mode 100644 index 0000000..c419263 --- /dev/null +++ b/_config.yml @@ -0,0 +1 @@ +theme: jekyll-theme-cayman \ No newline at end of file diff --git a/client/app/components/MovieObject.jsx b/client/app/components/MovieObject.jsx index e2c87aa..1349f68 100644 --- a/client/app/components/MovieObject.jsx +++ b/client/app/components/MovieObject.jsx @@ -1,102 +1,32 @@ import React from 'react'; -import glamorous from 'glamorous'; // StyleComponents -import mediaResultItem from './styledComponents/mediaResultItem.jsx'; +import movieStyle from './styles/movieObjectStyle.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.poster = object.poster; this.matchedInPlex = object.matchedInPlex; - this.overview = object.overview; + this.summary = object.summary; } requestExisting(movie) { console.log('Exists', movie); } - requestMovie(id) { - fetch('https://apollo.kevinmidboe.com/api/v1/plex/request/' + id, { + requestMovie() { + // fetch('https://apollo.kevinmidboe.com/api/v1/plex/request/' + id, { + fetch('http://localhost:31459/api/v1/plex/request/' + this.id + '?type='+this.type, { method: 'POST' }); } getElement() { - // TODO move this to separate files. - var resultItem = { - maxWidth: '95%', - margin: '0 auto', - minHeight: '230px' - } - var movie_content = { - marginLeft: '15px' - } - var resultTitle = { - color: 'black', - fontSize: '2em', - } - - var resultPoster = { - float: 'left', - zIndex: '3', - position: 'relative', - marginRight: '30px' - } - - var resultPosterImg = { - border: '2px none', - borderRadius: '2px', - width: '150px' - } - - var buttons = { - paddingTop: '20px' - } - - var 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' - } - - var 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' - } - - var row = { - width: '100%' - } - - var itemDivider = { - width: '90%', - borderBottom: '1px solid grey', - margin: '2rem auto' - } - // 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' @@ -106,10 +36,10 @@ class MovieObject { var foundInPlex; if (this.matchedInPlex) { foundInPlex = ; + style={movieStyle.requestButton}>Request Anyway; } else { - foundInPlex = ; + foundInPlex = ; } var themoviedbLink = 'https://www.themoviedb.org/movie/' + this.id @@ -117,27 +47,29 @@ class MovieObject { return (
-
-
- +
+
+
- {this.title} ({this.year}) + {this.title} ({this.year})

- {this.overview} + {this.summary}

-
+
{foundInPlex} - + + +
-
-
+
+
) diff --git a/client/app/components/SearchRequest.jsx b/client/app/components/SearchRequest.jsx index 32c3be9..5ee5d20 100644 --- a/client/app/components/SearchRequest.jsx +++ b/client/app/components/SearchRequest.jsx @@ -2,67 +2,128 @@ import React from 'react'; import MovieObject from './MovieObject.jsx'; -// TODO add option for searching multi, movies or tv shows +// StyleComponents +import searchStyle from './styles/searchRequestStyle.jsx'; +import URI from 'urijs'; + +// TODO add option for searching multi, movies or tv shows class SearchRequest extends React.Component { - constructor(props){ + constructor(props){ super(props) // Constructor with states holding the search query and the element of reponse. this.state = { searchQuery: '', responseMovieList: null, movieFilter: true, - tvshowFilter: false + showFilter: false, + discoverType: '', + page: 1 } + this.allowedDiscoverTypes = [ + 'discover', 'popular', 'nowplaying', 'upcoming' + ] + + // this.baseUrl = 'https://apollo.kevinmidboe.com/api/v1/'; + this.baseUrl = 'http://localhost:31459/api/v1/tmdb/'; + this.URLs = { - request: 'https://apollo.kevinmidboe.com/api/v1/plex/request?query=', - sendRequest: 'https://apollo.kevinmidboe.com/api/v1/plex/request?query=' + // request: 'https://apollo.kevinmidboe.com/api/v1/plex/request?page='+this.state.page+'&query=', + 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', + // sendRequest: 'https://apollo.kevinmidboe.com/api/v1/plex/request?query=' + sendRequest: 'http://localhost:31459/api/v1/plex/request?query=' } } componentDidMount(){ - var that = this; - this.setState({responseMovieList: null}) + var that = this; + // this.setState({responseMovieList: null}) + this.fetchDiscover('upcoming'); } // Handles all errors of the response of a fetch call handleErrors(response) { - if (!response.ok) { - throw Error(response.status); - } - return response; + if (!response.ok) { + throw Error(response.status); + } + return response; } + + fetchDiscover(queryDiscoverType) { + if (this.allowedDiscoverTypes.indexOf(queryDiscoverType) === -1) + throw Error('Invalid discover type: ' + queryDiscoverType); + + 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.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.tvshowFilter) { + if (this.state.showFilter) { url = url + '&type=tv' } - 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.length > 0) { - this.setState({ - responseMovieList: data.map(item => this.createMovieObjects(item)) - }) - } - }) - // If the -------- - .catch(error => { - console.log(error) - this.setState({ - responseMovieList:

Not Found

- }) + 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 + console.log(data) + if (data.length > 0) { + this.setState({ + responseMovieList: data.map(item => this.createMovieObjects(item)) + }) + } + }) + // If the -------- + .catch(error => { + console.log(error) + this.setState({ + responseMovieList:

Not Found

+ }) - console.log('Error submit: ', error.toString()); - }); + console.log('Error submit: ', error.toString()); + }); } // Updates the internal state of the query search field. @@ -93,186 +154,77 @@ class SearchRequest extends React.Component { }) console.log(this.state.movieFilter); } - else if (filterType == 'tvshows') { + else if (filterType == 'shows') { this.setState({ - tvshowFilter: !this.state.tvshowFilter + showFilter: !this.state.showFilter }) - console.log(this.state.tvshowFilter); + console.log(this.state.showFilter); } } + pageBackwards() { + if (this.state.page > 1) { + console.log('backwards'); + this.state.page--; + this.getUpcoming(); + } + console.log(this.state.page) + } + + pageForwards() { + this.state.page++; + this.getUpcoming(); + console.log('forwards'); + console.log(this.state.page) + } + render(){ - - var body = { - fontFamily: "'Open Sans', sans-serif", - backgroundColor: '#f7f7f7', - margin: 0, - padding: 0, - minHeight: '100%', - position: 'relative' - } - - var backgroundHeader = { - width: '100%', - minHeight: '400px', - backgroundColor: '#011c23', - zIndex: 1, - position: 'absolute' - } - - - var requestWrapper = { - top: '300px', - width: '90%', - maxWidth: '1200px', - margin: 'auto', - paddingTop: '20px', - backgroundColor: 'white', - position: 'relative', - zIndex: '10', - boxShadow: '0 2px 10px grey' - } - - var pageTitle = { - display: 'flex', - alignItems: 'center', - justifyContent: 'center' - } - - var pageTitleSpan = { - color: 'white', - fontSize: '3em', - marginTop: '4vh', - marginBottom: '6vh' - } - - var box = { - width: '90%', - height: '50px', - maxWidth: '1200px', - margin: '0 auto' - } - - var container = { - verticalAlign: 'middle', - whiteSpace: 'nowrap', - position: 'relative', - display: 'flex', - justifyContent: 'center' - } - - var searchIcon = { - position: 'absolute', - marginLeft: '17px', - marginTop: '17px', - zIndex: '1', - color: '#4f5b66' - } - - var searchBar = { - width: '60%', - minWidth: '120px', - height: '50px', - background: '#ffffff', - border: 'none', - fontSize: '10pt', - float: 'left', - color: '#63717f', - paddingLeft: '45px', - borderRadius: '5px', - marginRight: '15px' - } - - var searchFilter = { - color: 'white', - fontSize: '1em', - paddingTop: '12px', - marginBottom: '12px', - marginLeft: '10px', - cursor: 'pointer' - } - - var 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 - } - } - return( -
-
-
- 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.toggleFilter('movies')}} id="category_active">Movies - {this.toggleFilter('tvshows')}} + onClick={() => {this.toggleFilter('shows')}} id="category_inactive">TV Shows
-
+
{this.state.responseMovieList}
+
+ + +
) } - + } export default SearchRequest; \ No newline at end of file diff --git a/client/app/components/styles/movieObjectStyle.jsx b/client/app/components/styles/movieObjectStyle.jsx new file mode 100644 index 0000000..a87c726 --- /dev/null +++ b/client/app/components/styles/movieObjectStyle.jsx @@ -0,0 +1,80 @@ + +export default { + resultItem: { + maxWidth: '95%', + margin: '0 auto', + minHeight: '230px' + }, + + resultItem: { + maxWidth: '95%', + margin: '0 auto', + minHeight: '230px' + }, + + movie_content: { + marginLeft: '15px' + }, + + resultTitle: { + color: 'black', + fontSize: '2em', + }, + + resultPoster: { + float: 'left', + zIndex: '3', + position: 'relative', + marginRight: '30px' + }, + + resultPosterImg: { + border: '2px none', + borderRadius: '2px', + width: '150px' + }, + + 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' + } +} \ No newline at end of file diff --git a/client/app/components/styles/searchRequestStyle.jsx b/client/app/components/styles/searchRequestStyle.jsx new file mode 100644 index 0000000..d02eb5e --- /dev/null +++ b/client/app/components/styles/searchRequestStyle.jsx @@ -0,0 +1,134 @@ + +export default { + body: { + fontFamily: "'Open Sans', sans-serif", + backgroundColor: '#f7f7f7', + margin: 0, + padding: 0, + minHeight: '100%', + position: 'relative' + }, + + backgroundHeader: { + width: '100%', + minHeight: '400px', + backgroundColor: '#011c23', + zIndex: 1, + position: 'absolute' + }, + + requestWrapper: { + top: '300px', + width: '90%', + maxWidth: '1200px', + margin: 'auto', + paddingTop: '20px', + backgroundColor: 'white', + position: 'relative', + zIndex: '10', + boxShadow: '0 2px 10px grey' + }, + + pageTitle: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center' + }, + + pageTitleSpan: { + color: 'white', + fontSize: '3em', + marginTop: '4vh', + marginBottom: '6vh' + }, + + box: { + width: '90%', + height: '50px', + maxWidth: '1200px', + margin: '0 auto' + }, + + container: { + verticalAlign: 'middle', + whiteSpace: 'nowrap', + position: 'relative', + display: 'flex', + justifyContent: 'center' + }, + + searchIcon: { + position: 'absolute', + marginLeft: '17px', + marginTop: '17px', + zIndex: '1', + color: '#4f5b66' + }, + + searchBar: { + width: '60%', + minWidth: '120px', + height: '50px', + background: '#ffffff', + border: 'none', + fontSize: '10pt', + float: 'left', + color: '#63717f', + paddingLeft: '45px', + borderRadius: '5px', + marginRight: '15px' + }, + + searchFilter: { + color: 'white', + fontSize: '1em', + paddingTop: '12px', + marginBottom: '12px', + marginLeft: '10px', + cursor: 'pointer' + }, + + 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 + } + } +} \ No newline at end of file diff --git a/seasoned_api/package.json b/seasoned_api/package.json index 52fe956..94cd8bd 100644 --- a/seasoned_api/package.json +++ b/seasoned_api/package.json @@ -9,7 +9,7 @@ "cross-env": "^3.1.3", "express": "~4.0.0", "mongoose": "^3.6.13", - "moviedb": "^0.2.7", + "moviedb": "^0.2.10", "node-cache": "^4.1.1", "nodemailer": "^4.0.1", "python-shell": "^0.4.0", diff --git a/seasoned_api/src/media_classes/mediaInfo.js b/seasoned_api/src/media_classes/mediaInfo.js index c3cb52f..3019f25 100644 --- a/seasoned_api/src/media_classes/mediaInfo.js +++ b/seasoned_api/src/media_classes/mediaInfo.js @@ -1,12 +1,14 @@ class MediaInfo { - constructor(device, platform) { - this.device = undefined; - this.platform = undefined; - this.ip = undefined; - this.product = undefined; - this.title = undefined; - this.state = undefined; - + constructor() { + this.duration = undefined; + this.height = undefined; + this.width = undefined; + this.bitrate = undefined; + this.resolution = undefined; + this.framerate = undefined; + this.protocol = undefined; + this.container = undefined; + this.audioCodec = undefined; } } diff --git a/seasoned_api/src/plex/convertPlexToStream.js b/seasoned_api/src/plex/convertPlexToStream.js index 46ddfcd..1e82173 100644 --- a/seasoned_api/src/plex/convertPlexToStream.js +++ b/seasoned_api/src/plex/convertPlexToStream.js @@ -5,11 +5,13 @@ const convertStreamToUser = require('src/plex/stream/convertStreamToUser'); const ConvertStreamToPlayback = require('src/plex/stream/convertStreamToPlayback'); function convertPlexToStream(plexStream) { - const stream = convertPlexToSeasoned(plexStream); - stream.mediaInfo = convertStreamToMediaInfo(plexStream.Media); + const stream = convertPlexToSeasoned(plexStream) + const plexStreamMedia = plexStream.Media[0] + stream.mediaInfo = convertStreamToMediaInfo(plexStreamMedia); stream.player = convertStreamToPlayer(plexStream.Player); + stream.user = convertStreamToUser(plexStream.User); - stream.playback = new ConvertStreamToPlayback(plexStream.Media.Part); + stream.playback = new ConvertStreamToPlayback(plexStreamMedia.Part[0]); return stream; } diff --git a/seasoned_api/src/plex/convertStreamToMediaInfo.js b/seasoned_api/src/plex/convertStreamToMediaInfo.js index cd55477..60d6c1d 100644 --- a/seasoned_api/src/plex/convertStreamToMediaInfo.js +++ b/seasoned_api/src/plex/convertStreamToMediaInfo.js @@ -6,6 +6,7 @@ function convertStreamToMediaInfo(plexStream) { mediaInfo.duration = plexStream.duration; mediaInfo.height = plexStream.height; mediaInfo.width = plexStream.width; + if (plexStream.bitrate) { mediaInfo.bitrate = plexStream.bitrate; } diff --git a/seasoned_api/src/plex/plexRepository.js b/seasoned_api/src/plex/plexRepository.js index 0de89c6..25645a8 100644 --- a/seasoned_api/src/plex/plexRepository.js +++ b/seasoned_api/src/plex/plexRepository.js @@ -51,7 +51,7 @@ class PlexRepository { } }) .catch((err) => { - throw new Error(err); + throw new Error('Error handling plex playing. Error: ' + err); }) } } diff --git a/seasoned_api/src/plex/requestRepository.js b/seasoned_api/src/plex/requestRepository.js index 8aef0ca..e72dc72 100644 --- a/seasoned_api/src/plex/requestRepository.js +++ b/seasoned_api/src/plex/requestRepository.js @@ -67,13 +67,16 @@ class RequestRepository { }); } - sendRequest(identifier) { + /** + * Send request for given media id. + * @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) { // TODO add to DB so can have a admin page // TODO try a cache hit on the movie item - tmdb.lookup(identifier).then(movie => { - console.log(movie.title) - + tmdb.lookup(identifier, type).then(movie => { // create reusable transporter object using the default SMTP transport let transporter = nodemailer.createTransport({ @@ -110,6 +113,7 @@ class RequestRepository { }) + // TODO add better response when done. return Promise.resolve(); } diff --git a/seasoned_api/src/plex/stream/convertStreamToPlayback.js b/seasoned_api/src/plex/stream/convertStreamToPlayback.js index 555a07e..f4fae2d 100644 --- a/seasoned_api/src/plex/stream/convertStreamToPlayback.js +++ b/seasoned_api/src/plex/stream/convertStreamToPlayback.js @@ -4,6 +4,11 @@ class convertStreamToPlayback { this.width = plexStream.width; this.height = plexStream.height; this.decision = plexStream.decision; + this.audioProfile = plexStream.audioProfile; + this.videoProfile = plexStream.videoProfile; + this.duration = plexStream.duration; + this.container = plexStream.container; + } } diff --git a/seasoned_api/src/tmdb/convertTmdbToSeasoned.js b/seasoned_api/src/tmdb/convertTmdbToSeasoned.js index 642b7ea..7379610 100644 --- a/seasoned_api/src/tmdb/convertTmdbToSeasoned.js +++ b/seasoned_api/src/tmdb/convertTmdbToSeasoned.js @@ -1,8 +1,8 @@ const Movie = require('src/media_classes/movie'); const Show = require('src/media_classes/show'); -function convertTmdbToSeasoned(tmdbObject) { - const mediaType = tmdbObject.media_type; +function convertTmdbToSeasoned(tmdbObject, strictType=undefined) { + const mediaType = strictType || tmdbObject.media_type; // There are many diff types of content, we only want to look at movies and tv shows if (mediaType === 'movie') { @@ -16,6 +16,7 @@ function convertTmdbToSeasoned(tmdbObject) { const movie = new Movie(title, year, mediaType); + movie.id = tmdbObject.id; movie.summary = tmdbObject.overview; movie.rating = tmdbObject.vote_average; movie.poster = tmdbObject.poster_path; @@ -27,11 +28,12 @@ function convertTmdbToSeasoned(tmdbObject) { return movie; } - else if (mediaType === 'tv') { + else if (mediaType === 'tv' || mediaType === 'show') { const year = new Date(tmdbObject.first_air_date).getFullYear(); - const show = new Show(tmdbObject.title, year, mediaType); + const show = new Show(tmdbObject.name, year, 'show'); + show.id = tmdbObject.id; show.summary = tmdbObject.overview; show.rating = tmdbObject.vote_average; show.poster = tmdbObject.poster_path; diff --git a/seasoned_api/src/tmdb/tmdb.js b/seasoned_api/src/tmdb/tmdb.js index a536381..c9481b7 100644 --- a/seasoned_api/src/tmdb/tmdb.js +++ b/seasoned_api/src/tmdb/tmdb.js @@ -1,7 +1,10 @@ const moviedb = require('moviedb'); const convertTmdbToSeasoned = require('src/tmdb/convertTmdbToSeasoned'); -var methodTypes = { 'movie': 'searchMovie', 'tv': 'searchTv', 'multi': 'searchMulti', 'movieInfo': 'movieInfo', - 'tvInfo': 'tvInfo' }; +var methodTypes = { 'movie': 'searchMovie', 'show': 'searchTv', 'multi': 'searchMulti', 'movieInfo': 'movieInfo', + 'tvInfo': 'tvInfo', 'upcomingMovies': 'miscUpcomingMovies', 'discoverMovie': 'discoverMovie', + 'discoverShow': 'discoverTv', 'popularMovies': 'miscPopularMovies', 'popularShows': 'miscPopularTvs', + 'nowPlayingMovies': 'miscNowPlayingMovies', 'nowAiringShows': 'tvOnTheAir', 'movieSimilar': 'movieSimilar', + 'showSimilar': 'tvSimilar' }; class TMDB { constructor(apiKey, tmdbLibrary) { @@ -9,7 +12,7 @@ class TMDB { } search(text, page = 1, type = 'multi') { - const query = { query: text, page }; + const query = { 'query': text, 'page': page }; return Promise.resolve() .then(() => this.tmdb(type, query)) .catch(() => { throw new Error('Could not search for movies.'); }) @@ -26,27 +29,235 @@ class TMDB { } + /** + * Retrive list of discover section of movies from TMDB. + * @param {Page, type} the page number to specify in the request for discover, + * and type for movie or show + * @returns {Promise} dict with query results, current page and total_pages + */ + discover(page, type='movie') { + // Sets the tmdb function type to the corresponding type from query + var tmdbType; + if (type === 'movie') { + tmdbType = 'discoverMovie'; + } else if (type === 'show') { + tmdbType = 'discoverShow'; + } else { + // Throw error if invalid type from query + return Promise.resolve() + .then(() => { + throw new Error('Invalid type declaration.') + }) + } + + // Build a query for tmdb with pagenumber + const query = { 'page': page } + return Promise.resolve() + .then(() => this.tmdb(tmdbType, query)) + .catch(() => { throw new Error('Could not fetch discover.'); }) + .then((response) => { + try { + // Return a object that has the results and a variable for page, total_pages + // and seasonedResponse + var seasonedResponse = response.results.map((result) => { + return convertTmdbToSeasoned(result, type); } + ); + return { 'results': seasonedResponse, + 'page': response.page, 'total_pages': response.total_pages }; + } catch (error) { + console.log(error) + throw new Error('Error while parsing discover list.') + } + }); + } + + + /** + * Retrive list of popular section of movies or shows from TMDB. + * @param {Page, type} the page number to specify in the request for popular, + * and type for movie or show + * @returns {Promise} dict with query results, current page and total_pages + */ + // TODO add filter for language + popular(page, type='movie') { + // Sets the tmdb function type to the corresponding type from query + var tmdbType; + if (type === 'movie') { + tmdbType = 'popularMovies'; + } else if (type === 'show') { + tmdbType = 'popularShows'; + } else { + // Throw error if invalid type from query + return Promise.resolve() + .then(() => { + throw new Error('Invalid type declaration.') + }) + } + + // Build a query for tmdb with pagenumber + const query = { 'page': page } + return Promise.resolve() + .then(() => this.tmdb(tmdbType, query)) + .catch(() => { throw new Error('Could not fetch popular.'); }) + .then((response) => { + try { + var seasonedResponse = response.results.map((result) => { + return convertTmdbToSeasoned(result, type); } + ); + // Return a object that has the results and a variable for page, total_pages + // and seasonedResponse + return { 'results': seasonedResponse, + 'page': response.page, 'total_pages': response.total_pages }; + } catch (error) { + console.log(error) + throw new Error('Error while parsing discover list.') + } + }); + } + + + + /** + * Retrive list of now playing/airing section of movies or shows from TMDB. + * @param {Page, type} the page number to specify in the request for now playing/airing, + * and type for movie or show + * @returns {Promise} dict with query results, current page and total_pages + */ + // TODO add filter for language + nowplaying(page, type='movie') { + // Sets the tmdb function type to the corresponding type from query + var tmdbType; + if (type === 'movie') { + tmdbType = 'nowPlayingMovies'; + } else if (type === 'show') { + tmdbType = 'nowAiringShows'; + } else { + // Throw error if invalid type from query + return Promise.resolve() + .then(() => { + throw new Error('Invalid type declaration.') + }) + } + + // Build a query for tmdb with pagenumber + const query = { 'page': page } + return Promise.resolve() + .then(() => this.tmdb(tmdbType, query)) + .catch(() => { throw new Error('Could not fetch popular.'); }) + .then((response) => { + try { + var seasonedResponse = response.results.map((result) => { + return convertTmdbToSeasoned(result, type); } + ); + // Return a object that has the results and a variable for page, total_pages + // and seasonedResponse + return { 'results': seasonedResponse, + 'page': response.page, 'total_pages': response.total_pages }; + } catch (error) { + console.log(error) + throw new Error('Error while parsing discover list.') + } + }); + } + + /** + * Retrive list of upcmoing movies from TMDB. + * @param {Page} the page number to specify in the request for upcoming movies + * @returns {Promise} dict with query results, current page and total_pages + */ + // TODO add filter for language + upcoming(page) { + const query = { 'page': page } + return Promise.resolve() + .then(() => this.tmdb('upcomingMovies', query)) + .catch(() => { throw new Error('Could not fetch upcoming movies.'); }) + .then((response) => { + try { + var seasonedResponse = response.results.map((result) => { + return convertTmdbToSeasoned(result, 'movie'); } + ); + // Return a object that has the results and a variable for page, total_pages + // and seasonedResponse + return { 'results': seasonedResponse, + 'page': response.page, 'total_pages': response.total_pages }; + } catch (parseError) { + throw new Error('Error while parsing upcoming movies list.') + } + }); + } + + + /** + * Retrive list of upcmoing movies from TMDB. + * @param {Page} the page number to specify in the request for upcoming movies + * @returns {Promise} dict with query results, current page and total_pages + */ + // TODO add filter for language + similar(identifier, type) { + var tmdbType; + if (type === 'movie') { + tmdbType = 'movieSimilar'; + } else if (type === 'show') { + tmdbType = 'showSimilar'; + } else { + // Throw error if invalid type from query + return Promise.resolve() + .then(() => { + throw new Error('Invalid type declaration.') + }) + } + + const query = { id: identifier } + return Promise.resolve() + .then(() => this.tmdb(tmdbType, query)) + .catch(() => { throw new Error('Could not fetch upcoming movies.'); }) + .then((response) => { + try { + var seasonedResponse = response.results.map((result) => { + return convertTmdbToSeasoned(result, type); } + ); + // Return a object that has the results and a variable for page, total_pages + // and seasonedResponse + return { 'results': seasonedResponse, + 'page': response.page, 'total_pages': response.total_pages }; + } catch (parseError) { + throw new Error('Error while parsing silimar media list.') + } + }); + } + + /** * Retrieve a specific movie by id from TMDB. * @param {Number} identifier of the movie you want to retrieve * @returns {Promise} succeeds if movie was found */ - lookup(identifier, type = 'movie') { - if (type === 'movie') { type = 'movieInfo'} - else if (type === 'tv') { type = 'tvInfo'} + lookup(identifier, queryType = 'movie') { + var type; + if (queryType === 'movie') { type = 'movieInfo'} + else if (queryType === 'show') { type = 'tvInfo'} + else { + return Promise.resolve() + .then(() => { + throw new Error('Invalid type declaration.') + }) + } const query = { id: identifier }; return Promise.resolve() - .then(() => this.tmdb(type, query)) - .catch(() => { throw new Error('Could not find a movie with that id.'); }) - .then((response) => { - try { - return convertTmdbToSeasoned(response); - } catch (parseError) { - throw new Error('Could not parse movie.'); - } - }); + .then(() => this.tmdb(type, query)) + .catch(() => { throw new Error('Could not find a movie with that id.'); }) + .then((response) => { + try { + var car = convertTmdbToSeasoned(response, queryType); + console.log(car); + return car; + } catch (parseError) { + throw new Error('Could not parse movie.'); + } + }); } + // TODO ADD CACHE LOOKUP tmdb(method, argument) { return new Promise((resolve, reject) => { const callback = (error, reponse) => { @@ -58,6 +269,7 @@ class TMDB { if (!argument) { this.tmdbLibrary[methodTypes[method]](callback); + // this.tmdbLibrary['miscUpcomingMovies'] } else { this.tmdbLibrary[methodTypes[method]](argument, callback); } diff --git a/seasoned_api/src/webserver/app.js b/seasoned_api/src/webserver/app.js index 63dde87..9bd47fe 100644 --- a/seasoned_api/src/webserver/app.js +++ b/seasoned_api/src/webserver/app.js @@ -15,7 +15,8 @@ var allowedOrigins = ['https://kevinmidboe.com', 'http://localhost:8080'] router.use(function(req, res, next) { - console.log('Something is happening.'); + // TODO add logging of all incoming + console.log('Request: ', req.originalUrl); var origin = req.headers.origin; if (allowedOrigins.indexOf(origin) > -1) { res.setHeader('Access-Control-Allow-Origin', origin); @@ -40,6 +41,12 @@ router.post('/v1/plex/request/:mediaId', require('./controllers/plex/submitReque router.get('/v1/plex/hook', require('./controllers/plex/hookDump.js')); router.get('/v1/tmdb/search', require('./controllers/tmdb/searchMedia.js')); +router.get('/v1/tmdb/discover', require('./controllers/tmdb/discoverMedia.js')); +router.get('/v1/tmdb/popular', require('./controllers/tmdb/popularMedia.js')); +router.get('/v1/tmdb/nowplaying', require('./controllers/tmdb/nowPlayingMedia.js')); +router.get('/v1/tmdb/upcoming', require('./controllers/tmdb/getUpcoming.js')); + +router.get('/v1/tmdb/similar/:mediaId', require('./controllers/tmdb/searchSimilar.js')); router.get('/v1/tmdb/:mediaId', require('./controllers/tmdb/readMedia.js')); router.post('/v1/git/dump', require('./controllers/git/dumpHook.js')); diff --git a/seasoned_api/src/webserver/controllers/plex/submitRequest.js b/seasoned_api/src/webserver/controllers/plex/submitRequest.js index 80e1355..fd6345f 100644 --- a/seasoned_api/src/webserver/controllers/plex/submitRequest.js +++ b/seasoned_api/src/webserver/controllers/plex/submitRequest.js @@ -11,8 +11,9 @@ const requestRepository = new RequestRepository(); 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; - requestRepository.sendRequest(id) + requestRepository.sendRequest(id, type) .then(() => { res.send({ success: true, message: 'Media item sucessfully requested!' }); }) diff --git a/seasoned_api/src/webserver/controllers/tmdb/discoverMedia.js b/seasoned_api/src/webserver/controllers/tmdb/discoverMedia.js new file mode 100644 index 0000000..3669035 --- /dev/null +++ b/seasoned_api/src/webserver/controllers/tmdb/discoverMedia.js @@ -0,0 +1,21 @@ +const configuration = require('src/config/configuration').getInstance(); +const TMDB = require('src/tmdb/tmdb'); +const tmdb = new TMDB(configuration.get('tmdb', 'apiKey')); + +/** + * Controller: Retrieve a list of movies or shows in discover section in TMDB + * @param {Request} req http request variable + * @param {Response} res + * @returns {Callback} + */ +function discoverMediaController(req, res) { + const { page, type } = req.query; + tmdb.discover(page, type) + .then((results) => { + res.send(results); + }).catch((error) => { + res.status(404).send({ success: false, error: error.message }); + }); +} + +module.exports = discoverMediaController; diff --git a/seasoned_api/src/webserver/controllers/tmdb/getUpcoming.js b/seasoned_api/src/webserver/controllers/tmdb/getUpcoming.js new file mode 100644 index 0000000..9c9e049 --- /dev/null +++ b/seasoned_api/src/webserver/controllers/tmdb/getUpcoming.js @@ -0,0 +1,21 @@ +const configuration = require('src/config/configuration').getInstance(); +const TMDB = require('src/tmdb/tmdb'); +const tmdb = new TMDB(configuration.get('tmdb', 'apiKey')); + +/** + * Controller: Retrieve upcoming movies + * @param {Request} req http request variable + * @param {Response} res + * @returns {Callback} + */ +function getUpcomingController(req, res) { + const { page } = req.query; + tmdb.upcoming(page) + .then((results) => { + res.send(results); + }).catch((error) => { + res.status(404).send({ success: false, error: error.message }); + }); +} + +module.exports = getUpcomingController; diff --git a/seasoned_api/src/webserver/controllers/tmdb/nowPlayingMedia.js b/seasoned_api/src/webserver/controllers/tmdb/nowPlayingMedia.js new file mode 100644 index 0000000..5e67db8 --- /dev/null +++ b/seasoned_api/src/webserver/controllers/tmdb/nowPlayingMedia.js @@ -0,0 +1,21 @@ +const configuration = require('src/config/configuration').getInstance(); +const TMDB = require('src/tmdb/tmdb'); +const tmdb = new TMDB(configuration.get('tmdb', 'apiKey')); + +/** + * Controller: Retrieve nowplaying movies / now airing shows + * @param {Request} req http request variable + * @param {Response} res + * @returns {Callback} + */ +function nowPlayingMediaController(req, res) { + const { page, type } = req.query; + tmdb.nowplaying(page, type) + .then((results) => { + res.send(results); + }).catch((error) => { + res.status(404).send({ success: false, error: error.message }); + }); +} + +module.exports = nowPlayingMediaController; diff --git a/seasoned_api/src/webserver/controllers/tmdb/popularMedia.js b/seasoned_api/src/webserver/controllers/tmdb/popularMedia.js new file mode 100644 index 0000000..963d553 --- /dev/null +++ b/seasoned_api/src/webserver/controllers/tmdb/popularMedia.js @@ -0,0 +1,21 @@ +const configuration = require('src/config/configuration').getInstance(); +const TMDB = require('src/tmdb/tmdb'); +const tmdb = new TMDB(configuration.get('tmdb', 'apiKey')); + +/** + * Controller: Retrieve information for a movie + * @param {Request} req http request variable + * @param {Response} res + * @returns {Callback} + */ +function popularMediaController(req, res) { + const { page, type } = req.query; + tmdb.popular(page, type) + .then((results) => { + res.send(results); + }).catch((error) => { + res.status(404).send({ success: false, error: error.message }); + }); +} + +module.exports = popularMediaController; diff --git a/seasoned_api/src/webserver/controllers/tmdb/searchMedia.js b/seasoned_api/src/webserver/controllers/tmdb/searchMedia.js index f09b859..90f5cda 100644 --- a/seasoned_api/src/webserver/controllers/tmdb/searchMedia.js +++ b/seasoned_api/src/webserver/controllers/tmdb/searchMedia.js @@ -14,7 +14,7 @@ function searchMoviesController(req, res) { Promise.resolve() .then(() => tmdb.search(query, page, type)) .then((movies) => { - if (movies.length > 0) { + if (movies !== undefined || movies.length > 0) { res.send(movies); } else { res.status(404).send({ success: false, error: 'Search query did not return any results.'}) diff --git a/seasoned_api/src/webserver/controllers/tmdb/searchSimilar.js b/seasoned_api/src/webserver/controllers/tmdb/searchSimilar.js new file mode 100644 index 0000000..bc2383f --- /dev/null +++ b/seasoned_api/src/webserver/controllers/tmdb/searchSimilar.js @@ -0,0 +1,22 @@ +const configuration = require('src/config/configuration').getInstance(); +const TMDB = require('src/tmdb/tmdb'); +const tmdb = new TMDB(configuration.get('tmdb', 'apiKey')); + +/** + * Controller: Retrieve similar movies or shows + * @param {Request} req http request variable + * @param {Response} res + * @returns {Callback} + */ +function similarMediaController(req, res) { + const mediaId = req.params.mediaId; + const { type } = req.query; + tmdb.similar(mediaId, type) + .then((results) => { + res.send(results); + }).catch((error) => { + res.status(404).send({ success: false, error: error.message }); + }); +} + +module.exports = similarMediaController;