Merge branch 'dev/torrents' into 'master'

Torrents

See merge request !32
This commit is contained in:
Lucas 2017-01-19 11:45:23 +00:00
commit 68285f165a
16 changed files with 273 additions and 74 deletions

View File

@ -15,6 +15,7 @@
"jwt-decode": "^2.1.0", "jwt-decode": "^2.1.0",
"react": "^15.3.2", "react": "^15.3.2",
"react-bootstrap": "^0.30.6", "react-bootstrap": "^0.30.6",
"react-bootstrap-sweetalert": "^3.0.0",
"react-dom": "^15.3.2", "react-dom": "^15.3.2",
"react-loading": "^0.0.9", "react-loading": "^0.0.9",
"react-redux": "^4.4.6", "react-redux": "^4.4.6",

View File

@ -87,7 +87,7 @@ func main() {
env.Handle("/shows/refresh", extmedias.RefreshShows).WithRole(users.UserRole).Methods("POST") env.Handle("/shows/refresh", extmedias.RefreshShows).WithRole(users.UserRole).Methods("POST")
env.Handle("/shows/explore", extmedias.ExploreShows).WithRole(users.UserRole).Methods("GET") env.Handle("/shows/explore", extmedias.ExploreShows).WithRole(users.UserRole).Methods("GET")
env.Handle("/shows/search", shows.SearchShow).WithRole(users.UserRole).Methods("POST") env.Handle("/shows/search", shows.SearchShow).WithRole(users.UserRole).Methods("POST")
env.Handle("/download", torrents.DownloadHandler).WithRole(users.UserRole).Methods("POST") env.Handle("/torrents", torrents.DownloadHandler).WithRole(users.UserRole).Methods("POST")
n := negroni.Classic() n := negroni.Classic()
n.Use(authMiddleware) n.Use(authMiddleware)

View File

@ -1,21 +1,30 @@
import { configureAxios, request } from '../requests' import { configureAxios, request } from '../requests'
// ====================== // ======================
// Errors // Alerts
// ====================== // ======================
export function addError(message) { export function addAlertError(message) {
return { return {
type: 'ADD_ERROR', type: 'ADD_ALERT_ERROR',
payload: { payload: {
message, message,
} }
} }
} }
export function dismissError() { export function addAlertOk(message) {
return { return {
type: 'DISMISS_ERROR', type: 'ADD_ALERT_OK',
payload: {
message,
}
}
}
export function dismissAlert() {
return {
type: 'DISMISS_ALERT',
} }
} }
@ -44,14 +53,15 @@ export function loginUser(username, password) {
username: username, username: username,
password: password, password: password,
}, },
) ),
) )
} }
export function updateUser(config) { export function updateUser(config) {
return request( return request(
'USER_UPDATE', 'USER_UPDATE',
configureAxios().post('/users/edit', config) configureAxios().post('/users/edit', config),
"User updated",
) )
} }
@ -132,3 +142,17 @@ export function selectShow(imdbId) {
imdbId imdbId
} }
} }
// ======================
// AddTorrent
// ======================
export function addTorrent(url) {
return request(
'ADD_TORRENT',
configureAxios().post('/torrents', {
url: url,
}),
"Torrent added",
)
}

View File

@ -27,7 +27,7 @@ import store, { history } from './store'
// Components // Components
import NavBar from './components/navbar' import NavBar from './components/navbar'
import Error from './components/errors' import Alert from './components/alerts/alert'
import MovieList from './components/movies/list' import MovieList from './components/movies/list'
import ShowList from './components/shows/list' import ShowList from './components/shows/list'
import ShowDetails from './components/shows/details' import ShowDetails from './components/shows/details'
@ -43,7 +43,7 @@ class Main extends React.Component {
return ( return (
<div> <div>
<NavBar {...this.props}/> <NavBar {...this.props}/>
<Error {...this.props}/> <Alert {...this.props}/>
<div className="container-fluid"> <div className="container-fluid">
{React.cloneElement(this.props.children, this.props)} {React.cloneElement(this.props.children, this.props)}
</div> </div>
@ -57,7 +57,7 @@ function mapStateToProps(state) {
movieStore: state.movieStore, movieStore: state.movieStore,
showStore: state.showStore, showStore: state.showStore,
userStore: state.userStore, userStore: state.userStore,
errors: state.errors, alerts: state.alerts,
} }
} }

View File

@ -0,0 +1,17 @@
import React from 'react'
import SweetAlert from 'react-bootstrap-sweetalert';
export default function Alert(props) {
if (!props.alerts.show) {
return null
}
return (
<SweetAlert
type={props.alerts.type}
onConfirm={props.dismissAlert}
title={props.alerts.message}
/>
)
}

View File

@ -1,19 +0,0 @@
import React from 'react'
export default function Error(props) {
if (!props.errors.message) {
return null
}
return (
<div className="row">
<div className="col-md-6 col-md-offset-3 col-xs-12">
<div className="alert alert-warning">
<button type="button" className="close" onClick={props.dismissError}>
<span>&times;</span>
</button>
<p>{props.errors.message}</p>
</div>
</div>
</div>
)
}

View File

@ -13,11 +13,27 @@ export default function ListPosters(props) {
extract: (el) => el.title extract: (el) => el.title
}); });
elmts = filtered.map((el) => el.original); elmts = filtered.map((el) => el.original);
} } else {
// Get the page number if defined
let page = 1;
let perPage = props.perPage;
if (props.params && props.params.page) {
page = parseInt(props.params.page);
}
// Limit the number of results let from = 0;
if (elmts.length > props.perPage) { let to = perPage - 1;
elmts = elmts.slice(0, props.perPage); if (page > 1) {
from = ((page - 1) * perPage) - 1;
to = from + perPage;
}
// Limit the number of results
if ((from + perPage) > elmts.length) {
elmts = elmts.slice(from);
} else {
elmts = elmts.slice(from, to);
}
} }
return ( return (

View File

@ -1,5 +1,6 @@
import React from 'react' import React from 'react'
import TorrentsButton from './torrents'
import ListPosters from '../list/posters' import ListPosters from '../list/posters'
import ListDetails from '../list/details' import ListDetails from '../list/details'
import Loader from '../loader/loader' import Loader from '../loader/loader'
@ -37,13 +38,14 @@ class MovieButtons extends React.Component {
<i className="fa fa-download"></i> Download <i className="fa fa-download"></i> Download
</a> </a>
} }
{this.props.movie.torrents && this.props.movie.torrents.map(function(torrent, index) {
return ( {this.props.movie.torrents &&
<a key={torrent.url} type="button" className="btn btn-primary btn-sm" href={torrent.url}> <TorrentsButton
<i className="fa fa-download"></i> {torrent.quality} Torrent torrents={this.props.movie.torrents}
</a> addTorrent={this.props.addTorrent}
)} />
)} }
<a type="button" className="btn btn-warning btn-sm" href={imdb_link}> <a type="button" className="btn btn-warning btn-sm" href={imdb_link}>
<i className="fa fa-external-link"></i> IMDB <i className="fa fa-external-link"></i> IMDB
</a> </a>
@ -98,6 +100,7 @@ export default class MovieList extends React.Component {
filter={this.props.movieStore.filter} filter={this.props.movieStore.filter}
perPage={this.props.movieStore.perPage} perPage={this.props.movieStore.perPage}
onClick={this.props.selectMovie} onClick={this.props.selectMovie}
params={this.props.params}
/> />
{selectedMovie && {selectedMovie &&
<ListDetails data={selectedMovie}> <ListDetails data={selectedMovie}>
@ -105,6 +108,7 @@ export default class MovieList extends React.Component {
movie={selectedMovie} movie={selectedMovie}
fetching={this.props.movieStore.fetchingDetails} fetching={this.props.movieStore.fetchingDetails}
getMovieDetails={this.props.getMovieDetails} getMovieDetails={this.props.getMovieDetails}
addTorrent={this.props.addTorrent}
/> />
</ListDetails> </ListDetails>
} }

View File

@ -0,0 +1,81 @@
import React from 'react'
import { DropdownButton, MenuItem } from 'react-bootstrap'
export default class TorrentsButton extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick(e, url) {
e.preventDefault();
this.props.addTorrent(url);
}
render() {
const entries = buildMenuItems(this.props.torrents);
return (
<DropdownButton className="btn btn-default btn-sm" title="Torrents" id="download-torrents-button" dropup>
{entries.map(function(e, index) {
switch (e.type) {
case 'header':
return (
<MenuItem key={index} className="text-warning" header>
{e.value}
</MenuItem>
);
case 'divider':
return (
<MenuItem key={index} divider></MenuItem>
);
case 'entry':
return (
<MenuItem key={index} href={e.url} onClick={(event) => this.handleClick(event, e.url)}>
{e.quality}
</MenuItem>
);
}
}, this)}
</DropdownButton>
);
}
}
function buildMenuItems(torrents) {
// Organise by source
let sources = {}
for (let torrent of torrents) {
if (!sources[torrent.source]) {
sources[torrent.source] = [];
}
sources[torrent.source].push(torrent);
}
// Build the array of entries
let entries = [];
let sourceNames = Object.keys(sources);
let dividerCount = sourceNames.length - 1;
for (let source of sourceNames) {
// Push the title
entries.push({
type: "header",
value: source,
});
// Push the torrents
for (let torrent of sources[source]) {
entries.push({
type: "entry",
quality: torrent.quality,
url: torrent.url,
});
}
// Push the divider
if (dividerCount > 0) {
dividerCount--;
entries.push({ type: "divider" });
}
}
return entries;
}

View File

@ -13,7 +13,10 @@ export default class ShowDetails extends React.Component {
return ( return (
<div className="row" id="container"> <div className="row" id="container">
<Header data={this.props.showStore.show} /> <Header data={this.props.showStore.show} />
<SeasonsList data={this.props.showStore.show} /> <SeasonsList
data={this.props.showStore.show}
addTorrent={this.props.addTorrent}
/>
</div> </div>
); );
} }
@ -35,7 +38,8 @@ function Header(props){
function HeaderThumbnail(props){ function HeaderThumbnail(props){
return ( return (
<div className="col-xs-12 col-sm-2 text-center"> <div className="col-xs-12 col-sm-2 text-center">
<img src={props.data.poster_url} className="show-thumbnail thumbnail-selected img-thumbnail img-responsive"/> <img src={props.data.poster_url}
className="show-thumbnail thumbnail-selected img-thumbnail img-responsive"/>
</div> </div>
); );
} }
@ -70,7 +74,10 @@ function SeasonsList(props){
{props.data.seasons.length > 0 && props.data.seasons.map(function(season, index) { {props.data.seasons.length > 0 && props.data.seasons.map(function(season, index) {
return ( return (
<div className="col-xs-12 col-sm-10 col-sm-offset-1 col-md-10 col-md-offset-1" key={index}> <div className="col-xs-12 col-sm-10 col-sm-offset-1 col-md-10 col-md-offset-1" key={index}>
<Season data={season} /> <Season
data={season}
addTorrent={props.addTorrent}
/>
</div> </div>
) )
})} })}
@ -109,9 +116,13 @@ class Season extends React.Component {
{this.props.data.episodes.map(function(episode, index) { {this.props.data.episodes.map(function(episode, index) {
let key = `${episode.season}-${episode.episode}`; let key = `${episode.season}-${episode.episode}`;
return ( return (
<Episode key={key} data={episode} /> <Episode
key={key}
data={episode}
addTorrent={this.props.addTorrent}
/>
) )
})} }, this)}
</tbody> </tbody>
</table> </table>
} }
@ -130,7 +141,11 @@ function Episode(props) {
{props.data.torrents && props.data.torrents.map(function(torrent, index) { {props.data.torrents && props.data.torrents.map(function(torrent, index) {
let key = `${props.data.season}-${props.data.episode}-${torrent.source}-${torrent.quality}`; let key = `${props.data.season}-${props.data.episode}-${torrent.source}-${torrent.quality}`;
return ( return (
<Torrent data={torrent} key={key} /> <Torrent
data={torrent}
key={key}
addTorrent={props.addTorrent}
/>
) )
})} })}
</span> </span>
@ -139,12 +154,25 @@ function Episode(props) {
) )
} }
function Torrent(props) { class Torrent extends React.Component {
return ( constructor(props) {
<span className="episode-button"> super(props);
<a type="button" className="btn btn-primary btn-xs" href={props.data.url}> this.handleClick = this.handleClick.bind(this);
<i className="fa fa-download"></i> {props.data.quality} }
</a> handleClick(e, url) {
</span> e.preventDefault();
) this.props.addTorrent(url);
}
render() {
return (
<span className="episode-button">
<a type="button"
className="btn btn-primary btn-xs"
onClick={(e) => this.handleClick(e, this.props.data.url)}
href={this.props.data.url} >
<i className="fa fa-download"></i> {this.props.data.quality}
</a>
</span>
)
}
} }

View File

@ -0,0 +1,30 @@
const defaultState = {
show: false,
message: "",
type: "",
};
export default function Alert(state = defaultState, action) {
switch (action.type) {
case 'ADD_ALERT_ERROR':
return Object.assign({}, state, {
message: action.payload.message,
show: true,
type: "error",
})
case 'ADD_ALERT_OK':
return Object.assign({}, state, {
message: action.payload.message,
show: true,
type: "success",
})
case 'DISMISS_ALERT':
return Object.assign({}, state, {
message: "",
show: false,
type: "",
})
default:
return state;
}
}

View File

@ -1,12 +0,0 @@
export default function error(state = {}, action) {
switch (action.type) {
case 'ADD_ERROR':
return Object.assign({}, state, {
message: action.payload.message,
})
case 'DISMISS_ERROR':
return {};
default:
return state;
}
}

View File

@ -4,7 +4,7 @@ import { routerReducer } from 'react-router-redux'
import movieStore from './movies' import movieStore from './movies'
import showStore from './shows' import showStore from './shows'
import userStore from './users' import userStore from './users'
import errors from './errors' import alerts from './alerts'
// Use combine form form react-redux-form, it's a thin wrapper arround the // Use combine form form react-redux-form, it's a thin wrapper arround the
// default combinedReducers provided with React. It allows the forms to be // default combinedReducers provided with React. It allows the forms to be
@ -14,7 +14,7 @@ const rootReducer = combineForms({
movieStore, movieStore,
showStore, showStore,
userStore, userStore,
errors, alerts,
}) })
export default rootReducer; export default rootReducer;

View File

@ -16,7 +16,7 @@ export function configureAxios(headers = {}) {
// This function takes en event prefix to dispatch evens during the life of the // This function takes en event prefix to dispatch evens during the life of the
// request, it also take a promise (axios request) // request, it also take a promise (axios request)
export function request(eventPrefix, promise) { export function request(eventPrefix, promise, successMessage = null) {
// Events // Events
const pending = `${eventPrefix}_PENDING`; const pending = `${eventPrefix}_PENDING`;
const fulfilled = `${eventPrefix}_FULFILLED`; const fulfilled = `${eventPrefix}_FULFILLED`;
@ -30,7 +30,7 @@ export function request(eventPrefix, promise) {
if (response.data.status === 'error') if (response.data.status === 'error')
{ {
dispatch({ dispatch({
type: 'ADD_ERROR', type: 'ADD_ALERT_ERROR',
payload: { payload: {
message: response.data.message, message: response.data.message,
} }
@ -40,6 +40,14 @@ export function request(eventPrefix, promise) {
type: fulfilled, type: fulfilled,
payload: response.data, payload: response.data,
}) })
if (successMessage) {
dispatch({
type: 'ADD_ALERT_OK',
payload: {
message: successMessage,
},
})
}
}) })
.catch(error => { .catch(error => {
// Unauthorized // Unauthorized
@ -49,7 +57,7 @@ export function request(eventPrefix, promise) {
}) })
} }
dispatch({ dispatch({
type: 'ADD_ERROR', type: 'ADD_ALERT_ERROR',
payload: { payload: {
message: error.response.data, message: error.response.data,
} }

View File

@ -23,8 +23,19 @@ body {
.list-details-buttons { .list-details-buttons {
position: fixed; position: fixed;
bottom: 1%; bottom: 0px;
right: 1%; padding-top: 5px;
background-color: @body-bg;
@media (min-width: @screen-xs-max) {
right: 0px;
margin-right: 5px;
}
a.btn,
div.btn-group {
margin-bottom: 5px;
}
} }
.list-filter { .list-filter {
@ -42,3 +53,7 @@ body {
.navbar { .navbar {
opacity: 0.95; opacity: 0.95;
} }
div.sweet-alert > h2 {
color: @body-bg;
}

View File

@ -3077,6 +3077,12 @@ react-bootstrap:
uncontrollable "^4.0.1" uncontrollable "^4.0.1"
warning "^3.0.0" warning "^3.0.0"
react-bootstrap-sweetalert:
version "3.0.0"
resolved "https://registry.yarnpkg.com/react-bootstrap-sweetalert/-/react-bootstrap-sweetalert-3.0.0.tgz#65378a42f37845676acf98e8c43ce4a61f23306f"
dependencies:
object-assign "^4.1.0"
react-dom@^15.3.2: react-dom@^15.3.2:
version "15.3.2" version "15.3.2"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.3.2.tgz#c46b0aa5380d7b838e7a59c4a7beff2ed315531f" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.3.2.tgz#c46b0aa5380d7b838e7a59c4a7beff2ed315531f"