From c98117c4f6263a5808771e63871e3baf1ac7da32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Delattre?= Date: Fri, 16 Dec 2016 22:42:49 +0100 Subject: [PATCH 1/4] Add show list --- src/public/js/actions/actionCreators.js | 55 ++++++++--- src/public/js/app.js | 7 ++ src/public/js/components/list/details.js | 24 +++++ src/public/js/components/list/filter.js | 22 +++++ src/public/js/components/list/poster.js | 16 +++ src/public/js/components/list/posters.js | 45 +++++++++ src/public/js/components/movies/list.js | 121 +++-------------------- src/public/js/components/navbar.js | 3 + src/public/js/components/shows/list.js | 49 +++++++++ src/public/js/reducers/index.js | 2 + src/public/js/reducers/shows.js | 32 ++++++ src/public/less/app.less | 6 +- 12 files changed, 261 insertions(+), 121 deletions(-) create mode 100644 src/public/js/components/list/details.js create mode 100644 src/public/js/components/list/filter.js create mode 100644 src/public/js/components/list/poster.js create mode 100644 src/public/js/components/list/posters.js create mode 100644 src/public/js/components/shows/list.js create mode 100644 src/public/js/reducers/shows.js diff --git a/src/public/js/actions/actionCreators.js b/src/public/js/actions/actionCreators.js index a3d41dc..3603633 100644 --- a/src/public/js/actions/actionCreators.js +++ b/src/public/js/actions/actionCreators.js @@ -1,18 +1,8 @@ import { configureAxios, request } from '../requests' -// Select Movie -export function selectMovie(imdbId) { - return { - type: 'SELECT_MOVIE', - imdbId - } -} - -export function isUserLoggedIn() { - return { - type: 'IS_USER_LOGGED_IN', - } -} +// ====================== +// Errors +// ====================== export function addError(message) { return { @@ -29,12 +19,22 @@ export function dismissError() { } } +// ====================== +// Users +// ====================== + export function userLogout() { return { type: 'USER_LOGOUT', } } +export function isUserLoggedIn() { + return { + type: 'IS_USER_LOGGED_IN', + } +} + export function loginUser(username, password) { return request( 'USER_LOGIN', @@ -69,6 +69,17 @@ export function getUserInfos() { ) } +// ====================== +// Movies +// ====================== + +export function selectMovie(imdbId) { + return { + type: 'SELECT_MOVIE', + imdbId + } +} + export function getMovieDetails(imdbId) { return request( 'MOVIE_GET_DETAILS', @@ -82,3 +93,21 @@ export function fetchMovies(url) { configureAxios().get(url) ) } + +// ====================== +// Shows +// ====================== + +export function fetchShows(url) { + return request( + 'SHOW_LIST_FETCH', + configureAxios().get(url) + ) +} + +export function selectShow(imdbId) { + return { + type: 'SELECT_SHOW', + imdbId + } +} diff --git a/src/public/js/app.js b/src/public/js/app.js index 7a0a242..c82d250 100644 --- a/src/public/js/app.js +++ b/src/public/js/app.js @@ -29,6 +29,7 @@ import store, { history } from './store' import NavBar from './components/navbar' import Error from './components/errors' import MovieList from './components/movies/list' +import ShowList from './components/shows/list' import UserLoginForm from './components/users/login' import UserEdit from './components/users/edit' import UserSignUp from './components/users/signup' @@ -53,6 +54,7 @@ class Main extends React.Component { function mapStateToProps(state) { return { movieStore: state.movieStore, + showStore: state.showStore, userStore: state.userStore, errors: state.errors, } @@ -81,6 +83,10 @@ const MovieListPolochon = (props) => ( ) +const ShowListPopular = (props) => ( + +) + ReactDOM.render(( @@ -91,6 +97,7 @@ ReactDOM.render(( + diff --git a/src/public/js/components/list/details.js b/src/public/js/components/list/details.js new file mode 100644 index 0000000..ef5945a --- /dev/null +++ b/src/public/js/components/list/details.js @@ -0,0 +1,24 @@ +import React from 'react' + +export default function ListDetails(props) { + return ( +
+
+

{props.data.title}

+

{props.data.title}

+ {props.data.runtime && +

+ +  {props.data.runtime} min +

+ } +

+ +  {props.data.rating} ({props.data.votes} counts) +

+

{props.data.plot}

+
+ {props.children} +
+ ); +} diff --git a/src/public/js/components/list/filter.js b/src/public/js/components/list/filter.js new file mode 100644 index 0000000..8396379 --- /dev/null +++ b/src/public/js/components/list/filter.js @@ -0,0 +1,22 @@ +import React from 'react' +import { Control, Form } from 'react-redux-form'; + +export default function ListFilter(props) { + return ( +
+
+
+ + + + + +
+
+ ); +} diff --git a/src/public/js/components/list/poster.js b/src/public/js/components/list/poster.js new file mode 100644 index 0000000..e5f8d57 --- /dev/null +++ b/src/public/js/components/list/poster.js @@ -0,0 +1,16 @@ +import React from 'react' + +export default function ListPoster(props) { + const selected = props.selected ? ' thumbnail-selected' : ''; + const imgClass = 'thumbnail' + selected; + return ( +
+ + + +
+ ); +} diff --git a/src/public/js/components/list/posters.js b/src/public/js/components/list/posters.js new file mode 100644 index 0000000..2514a56 --- /dev/null +++ b/src/public/js/components/list/posters.js @@ -0,0 +1,45 @@ +import React from 'react' +import fuzzy from 'fuzzy'; + +import ListFilter from './filter' +import ListPoster from './poster' + +export default function ListPosters(props) { + let elmts = props.data.slice(); + + // Filter the list of elements + if (props.filter !== "") { + const filtered = fuzzy.filter(props.filter, elmts, { + extract: (el) => el.title + }); + elmts = filtered.map((el) => el.original); + } + + // Limit the number of results + if (elmts.length > props.perPage) { + elmts = elmts.slice(0, props.perPage); + } + + return ( +
+ +
+ {elmts.map(function(el, index) { + const selected = (el.imdb_id === props.selectedImdbId) ? true : false; + return ( + props.onClick(el.imdb_id)} + /> + )} + )} +
+
+ ); +} diff --git a/src/public/js/components/movies/list.js b/src/public/js/components/movies/list.js index 4ec4dd0..c1b4731 100644 --- a/src/public/js/components/movies/list.js +++ b/src/public/js/components/movies/list.js @@ -1,101 +1,7 @@ import React from 'react' -import axios from 'axios' -import { Control, Form } from 'react-redux-form'; -import fuzzy from 'fuzzy'; -function MoviePosters(props) { - let movies = props.movies.slice(); - - // Filter the movies - if (props.filter !== "") { - const filtered = fuzzy.filter(props.filter, movies, { - extract: (el) => el.title - }); - movies = filtered.map((el) => el.original); - } - - // Limit the number of results - if (movies.length > props.perPage) { - movies = movies.slice(0, props.perPage); - } - - return ( -
- -
- {movies.map(function(movie, index) { - const selected = (movie.imdb_id === props.selectedMovieId) ? true : false; - return ( - props.onClick(movie.imdb_id)} - /> - )} - )} -
-
- ); -} - -class MovieListFilter extends React.Component { - render() { - return ( -
-
-
- - - - - -
-
- ); - } -} - -function MoviePoster(props) { - const selected = props.selected ? ' thumbnail-selected' : ''; - const imgClass = 'thumbnail' + selected; - return ( -
- - - -
- ); -} - -function MovieDetails(props) { - return ( -
-
-

{props.movie.title}

-

{props.movie.title}

-

- -  {props.movie.runtime} min -

-

- -  {props.movie.rating} ({props.movie.votes} counts) -

-

{props.movie.plot}

-
- -
- ); -} +import ListPosters from '../list/posters' +import ListDetails from '../list/details' class MovieButtons extends React.Component { constructor(props) { @@ -112,7 +18,7 @@ class MovieButtons extends React.Component { render() { const imdb_link = `http://www.imdb.com/title/${this.props.movie.imdb_id}`; return ( -
+
{this.props.fetching || @@ -159,19 +65,24 @@ export default class MovieList extends React.Component { const selectedMovie = movies[index]; return (
- {selectedMovie && - + + + }
); diff --git a/src/public/js/components/navbar.js b/src/public/js/components/navbar.js index 859ca48..fd25207 100644 --- a/src/public/js/components/navbar.js +++ b/src/public/js/components/navbar.js @@ -27,6 +27,9 @@ export default class NavBar extends React.Component { Polochon movies + + Polochon shows +
{props.children}
diff --git a/src/public/js/components/shows/details.js b/src/public/js/components/shows/details.js new file mode 100644 index 0000000..9f33991 --- /dev/null +++ b/src/public/js/components/shows/details.js @@ -0,0 +1,145 @@ +import React from 'react' + +export default class ShowDetails extends React.Component { + componentWillMount() { + this.props.fetchShowDetails(this.props.params.imdbId); + } + render() { + return ( +
+
+ +
+ ); + } +} + +function Header(props){ + return ( +
+
+
+ + +
+
+
+ ); +} + +function HeaderThumbnail(props){ + return ( +
+ +
+ ); +} + +function HeaderDetails(props){ + const imdbLink = `http://www.imdb.com/title/${props.data.imdb_id}`; + return ( +
+
+
Title
+
{props.data.title}
+
Plot
+
{props.data.plot}
+
IMDB
+
+ + Open in IMDB + +
+
Year
+
{props.data.year}
+
Rating
+
{props.data.rating}
+
+
+ ); +} + +function SeasonsList(props){ + return ( +
+ {props.data.seasons.length > 0 && props.data.seasons.map(function(season, index) { + return ( +
+ +
+ ) + })} +
+ ) +} + +class Season extends React.Component { + constructor(props) { + super(props); + this.handleClick = this.handleClick.bind(this); + this.state = { colapsed: true }; + } + handleClick(e) { + e.preventDefault(); + this.setState({ colapsed: !this.state.colapsed }); + } + render() { + return ( +
+
this.handleClick(e)}> + Season {this.props.data.season} + — ({this.props.data.episodes.length} episodes) + + {this.state.colapsed || + + } + {this.state.colapsed && + + } + +
+ {this.state.colapsed || + + + {this.props.data.episodes.map(function(episode, index) { + let key = `${episode.season}-${episode.episode}`; + return ( + + ) + })} + +
+ } +
+ ) + } +} + +function Episode(props) { + return ( + + {props.data.episode} + {props.data.title} + + + {props.data.torrents && props.data.torrents.map(function(torrent, index) { + let key = `${props.data.season}-${props.data.episode}-${torrent.source}-${torrent.quality}`; + return ( + + ) + })} + + + + ) +} + +function Torrent(props) { + return ( + + + {props.data.quality} + + + ) +} diff --git a/src/public/js/components/shows/list.js b/src/public/js/components/shows/list.js index fb1ef4d..1d83f0c 100644 --- a/src/public/js/components/shows/list.js +++ b/src/public/js/components/shows/list.js @@ -1,15 +1,19 @@ import React from 'react' +import { Link } from 'react-router' import ListDetails from '../list/details' import ListPosters from '../list/posters' function ShowButtons(props) { - const imdb_link = `http://www.imdb.com/title/${props.show.imdb_id}`; + const imdbLink = `http://www.imdb.com/title/${props.show.imdb_id}`; return (
- + IMDB + + Details +
); } diff --git a/src/public/js/reducers/shows.js b/src/public/js/reducers/shows.js index de3f99d..43ed2af 100644 --- a/src/public/js/reducers/shows.js +++ b/src/public/js/reducers/shows.js @@ -3,6 +3,9 @@ const defaultState = { filter: "", perPage: 30, selectedImdbId: "", + show: { + seasons: [], + }, }; export default function showStore(state = defaultState, action) { @@ -17,6 +20,11 @@ export default function showStore(state = defaultState, action) { shows: action.payload.data, selectedImdbId: selectedImdbId, }) + case 'SHOW_FETCH_DETAILS_FULFILLED': + return Object.assign({}, state, { + show: sortEpisodes(action.payload.data), + }) + return state; case 'SELECT_SHOW': // Don't select the show if we're fetching another show's details if (state.fetchingDetails) { @@ -30,3 +38,47 @@ export default function showStore(state = defaultState, action) { return state } } + +function sortEpisodes(show) { + let episodes = show.episodes; + delete show["episodes"]; + + if (episodes.length == 0) { + return show; + } + + // Extract the seasons + let seasons = {}; + for (let ep of episodes) { + if (!seasons[ep.season]) { + seasons[ep.season] = { episodes: [] }; + } + seasons[ep.season].episodes.push(ep); + } + + if (seasons.length === 0) { + return show; + } + + // Put all the season in an array + let sortedSeasons = []; + for (let season of Object.keys(seasons)) { + let seasonEpisodes = seasons[season].episodes; + // Order the episodes in each season + seasonEpisodes.sort((a,b) => (a.episode - b.episode)) + // Add the season in the list + sortedSeasons.push({ + season: season, + episodes: seasonEpisodes, + }) + } + + // Order the seasons + for (let i=0; i (a.season - b.season)) + } + + show.seasons = sortedSeasons; + + return show; +} diff --git a/src/public/less/app.less b/src/public/less/app.less index 638753c..121e568 100644 --- a/src/public/less/app.less +++ b/src/public/less/app.less @@ -12,7 +12,11 @@ body { background-color: @brand-primary; } -.list-plot { +.clickable { + cursor: pointer; +} + +.plot { .text-justify; margin-right: 5%; } @@ -27,6 +31,14 @@ body { padding-bottom: 10px; } +.show-thumbnail { + max-height: 300px; +} + +.episode-button { + padding-right: 5px; +} + .navbar { opacity: 0.95; } From 34305ade6c48561e206b208166a60454032e4129 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Delattre?= Date: Tue, 3 Jan 2017 15:50:25 +0100 Subject: [PATCH 3/4] Fix poster URL for shows --- src/internal/shows/shows.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/internal/shows/shows.go b/src/internal/shows/shows.go index cd5475c..233f732 100644 --- a/src/internal/shows/shows.go +++ b/src/internal/shows/shows.go @@ -248,7 +248,7 @@ func (s *Show) GetDetails(env *web.Env, force bool) error { // GetPosterURL returns the image URL or the default image if the poster is not yet downloaded func (s *Show) GetPosterURL(env *web.Env) string { // Check if the movie image exists - if _, err := os.Stat(s.imgURL(env, "poster")); os.IsNotExist(err) { + if _, err := os.Stat(s.imgFile(env, "poster")); os.IsNotExist(err) { // TODO image in the config ? return "img/noimage.png" } @@ -273,11 +273,16 @@ func (s *Show) downloadImages(env *web.Env) { } // imgFile returns the image location on disk -func (s *Show) imgURL(env *web.Env, imgType string) string { +func (s *Show) imgFile(env *web.Env, imgType string) string { fileURL := fmt.Sprintf("img/shows/%s-%s.jpg", s.ImdbID, imgType) return filepath.Join(env.Config.PublicDir, fileURL) } +// imgURL returns the default image url +func (s *Show) imgURL(env *web.Env, imgType string) string { + return fmt.Sprintf("img/shows/%s-%s.jpg", s.ImdbID, imgType) +} + // GetDetailsAsUser like GetDetails but with User context func (s *Show) GetDetailsAsUser(db *sqlx.DB, user *users.User, log *logrus.Entry) error { var err error From 5faa3225af9dd38a1199b0f5c1ed528772bbbd8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Delattre?= Date: Tue, 3 Jan 2017 15:52:24 +0100 Subject: [PATCH 4/4] Add missing JSON tags for Show --- src/internal/shows/shows.go | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/internal/shows/shows.go b/src/internal/shows/shows.go index 233f732..cecf755 100644 --- a/src/internal/shows/shows.go +++ b/src/internal/shows/shows.go @@ -80,12 +80,12 @@ var ( type Show struct { sqly.BaseModel polochon.Show - Episodes []*Episode - TrackedSeason int - TrackedEpisode int - BannerURL string `json:"banner_url"` - FanartURL string `json:"fanart_url"` - PosterURL string `json:"poster_url"` + Episodes []*Episode `json:"episodes"` + TrackedSeason int `json:"tracked_season"` + TrackedEpisode int `json:"tracked_episode"` + BannerURL string `json:"banner_url"` + FanartURL string `json:"fanart_url"` + PosterURL string `json:"poster_url"` } // ShowDB represents the Show in the DB @@ -100,7 +100,6 @@ type ShowDB struct { FirstAired time.Time `db:"first_aired"` Created time.Time `db:"created_at"` Updated time.Time `db:"updated_at"` - // URL string `json:"-"` } // NewShowDB returns a Show ready to be put in DB from a