Make the show store immutable

This commit is contained in:
Grégoire Delattre 2017-05-31 13:55:03 +02:00
parent 33137e0035
commit dd3ab77f2c
6 changed files with 134 additions and 185 deletions

View File

@ -24,18 +24,13 @@ export function getEpisodeDetails(imdbId, season, episode) {
return request( return request(
'EPISODE_GET_DETAILS', 'EPISODE_GET_DETAILS',
configureAxios().post(`/shows/${imdbId}/seasons/${season}/episodes/${episode}`), configureAxios().post(`/shows/${imdbId}/seasons/${season}/episodes/${episode}`),
) null,
} {
export function updateEpisodeDetailsStore(imdbId, season, episode) {
return {
type: 'EPISODE_GET_DETAILS',
payload: {
imdbId, imdbId,
season, season,
episode, episode,
},
} }
)
} }
export function fetchShowDetails(imdbId) { export function fetchShowDetails(imdbId) {

View File

@ -9,21 +9,15 @@ export function refreshSubtitles(type, id, season, episode) {
return request( return request(
'MOVIE_SUBTITLES_UPDATE', 'MOVIE_SUBTITLES_UPDATE',
configureAxios().post(`${resourceURL}/subtitles/refresh`), configureAxios().post(`${resourceURL}/subtitles/refresh`),
[ null,
addAlertOk("Subtitles refreshed"), { imdb_id: id },
],
{
imdb_id: id,
},
) )
case 'episode': case 'episode':
var resourceURL = `/shows/${id}/seasons/${season}/episodes/${episode}` var resourceURL = `/shows/${id}/seasons/${season}/episodes/${episode}`
return request( return request(
'EPISODE_SUBTITLES_UPDATE', 'EPISODE_SUBTITLES_UPDATE',
configureAxios().post(`${resourceURL}/subtitles/refresh`), configureAxios().post(`${resourceURL}/subtitles/refresh`),
[ null,
addAlertOk("Subtitles refreshed"),
],
{ {
imdb_id: id, imdb_id: id,
season: season, season: season,

View File

@ -13,7 +13,7 @@ export default class SubtitlesButton extends React.Component {
return return
} }
// Refresh the subtitles // Refresh the subtitles
this.props.refreshSubtitles(this.props.type, this.props.resourceID, this.props.data.season, this.props.data.episode); this.props.refreshSubtitles(this.props.type, this.props.resourceID, this.props.season, this.props.episode);
} }
render() { render() {
// If there is no URL, the resource is not in polochon, we won't be able to download subtitles // If there is no URL, the resource is not in polochon, we won't be able to download subtitles

View File

@ -1,26 +1,27 @@
import React from 'react' import React from 'react'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { bindActionCreators } from 'redux' import { bindActionCreators } from 'redux'
import { toJS } from 'immutable'
import { addTorrent } from '../../actions/torrents' import { addTorrent } from '../../actions/torrents'
import { refreshSubtitles } from '../../actions/subtitles' import { refreshSubtitles } from '../../actions/subtitles'
import { addShowToWishlist, deleteShowFromWishlist, getEpisodeDetails, import { addShowToWishlist, deleteShowFromWishlist, getEpisodeDetails, updateShowDetails } from '../../actions/shows'
updateEpisodeDetailsStore, updateShowDetails } from '../../actions/shows'
import Loader from '../loader/loader' import Loader from '../loader/loader'
import DownloadButton from '../buttons/download' import DownloadButton from '../buttons/download'
import SubtitlesButton from '../buttons/subtitles' import SubtitlesButton from '../buttons/subtitles'
import { OverlayTrigger, Tooltip } from 'react-bootstrap' import { OverlayTrigger, Tooltip } from 'react-bootstrap'
function mapStateToProps(state) { function mapStateToProps(state) {
return { return {
loading: state.showStore.loading, loading: state.showStore.loading,
show: state.showStore.show, show: state.showStore.get('show'),
}; };
} }
const mapDispatchToProps = (dispatch) => const mapDispatchToProps = (dispatch) =>
bindActionCreators({addTorrent, addShowToWishlist, deleteShowFromWishlist, bindActionCreators({addTorrent, addShowToWishlist, deleteShowFromWishlist,
updateShowDetails, updateEpisodeDetailsStore, getEpisodeDetails, updateShowDetails, getEpisodeDetails,
refreshSubtitles }, dispatch) refreshSubtitles }, dispatch)
class ShowDetails extends React.Component { class ShowDetails extends React.Component {
@ -41,7 +42,6 @@ class ShowDetails extends React.Component {
addTorrent={this.props.addTorrent} addTorrent={this.props.addTorrent}
addToWishlist={this.props.addShowToWishlist} addToWishlist={this.props.addShowToWishlist}
getEpisodeDetails={this.props.getEpisodeDetails} getEpisodeDetails={this.props.getEpisodeDetails}
updateEpisodeDetailsStore={this.props.updateEpisodeDetailsStore}
refreshSubtitles={this.props.refreshSubtitles} refreshSubtitles={this.props.refreshSubtitles}
/> />
</div> </div>
@ -70,21 +70,21 @@ 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} <img src={props.data.get('poster_url')}
className="show-thumbnail thumbnail-selected img-thumbnail img-responsive"/> className="show-thumbnail thumbnail-selected img-thumbnail img-responsive"/>
</div> </div>
); );
} }
function HeaderDetails(props){ function HeaderDetails(props){
const imdbLink = `http://www.imdb.com/title/${props.data.imdb_id}`; const imdbLink = `http://www.imdb.com/title/${props.data.get('imdb_id')}`;
return ( return (
<div className="col-xs-12 col-sm-10"> <div className="col-xs-12 col-sm-10">
<dl className="dl-horizontal"> <dl className="dl-horizontal">
<dt>Title</dt> <dt>Title</dt>
<dd>{props.data.title}</dd> <dd>{props.data.get('title')}</dd>
<dt>Plot</dt> <dt>Plot</dt>
<dd className="plot">{props.data.plot}</dd> <dd className="plot">{props.data.get('plot')}</dd>
<dt>IMDB</dt> <dt>IMDB</dt>
<dd> <dd>
<a type="button" className="btn btn-warning btn-xs" href={imdbLink}> <a type="button" className="btn btn-warning btn-xs" href={imdbLink}>
@ -92,9 +92,9 @@ function HeaderDetails(props){
</a> </a>
</dd> </dd>
<dt>Year</dt> <dt>Year</dt>
<dd>{props.data.year}</dd> <dd>{props.data.get('year')}</dd>
<dt>Rating</dt> <dt>Rating</dt>
<dd>{props.data.rating}</dd> <dd>{props.data.get('rating')}</dd>
</dl> </dl>
<TrackHeader <TrackHeader
data={props.data} data={props.data}
@ -108,19 +108,20 @@ function HeaderDetails(props){
function SeasonsList(props){ function SeasonsList(props){
return ( return (
<div> <div>
{props.data.seasons.length > 0 && props.data.seasons.map(function(season, index) { {props.data.get('seasons').entrySeq().map(function([season, data]) {
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={season.toString()}>
<Season <Season
data={season} data={data}
season={season}
addTorrent={props.addTorrent} addTorrent={props.addTorrent}
addToWishlist={props.addToWishlist} addToWishlist={props.addToWishlist}
getEpisodeDetails={props.getEpisodeDetails} getEpisodeDetails={props.getEpisodeDetails}
updateEpisodeDetailsStore={props.updateEpisodeDetailsStore}
refreshSubtitles={props.refreshSubtitles} refreshSubtitles={props.refreshSubtitles}
/> />
</div> </div>
) );
})} })}
</div> </div>
) )
@ -140,8 +141,8 @@ class Season extends React.Component {
return ( return (
<div className="panel panel-default"> <div className="panel panel-default">
<div className="panel-heading clickable" onClick={(e) => this.handleClick(e)}> <div className="panel-heading clickable" onClick={(e) => this.handleClick(e)}>
Season {this.props.data.season} Season {this.props.season}
<small className="text-primary"> ({this.props.data.episodes.length} episodes)</small> <small className="text-primary"> ({this.props.data.toList().size} episodes)</small>
<span className="pull-right"> <span className="pull-right">
{this.state.colapsed || {this.state.colapsed ||
<i className="fa fa-chevron-down"></i> <i className="fa fa-chevron-down"></i>
@ -154,8 +155,8 @@ class Season extends React.Component {
{this.state.colapsed || {this.state.colapsed ||
<table className="table table-striped table-hover"> <table className="table table-striped table-hover">
<tbody> <tbody>
{this.props.data.episodes.map(function(episode, index) { {this.props.data.toList().map(function(episode) {
let key = `${episode.season}-${episode.episode}`; let key = `${episode.get('season')}-${episode.get('episode')}`;
return ( return (
<Episode <Episode
key={key} key={key}
@ -163,7 +164,6 @@ class Season extends React.Component {
addTorrent={this.props.addTorrent} addTorrent={this.props.addTorrent}
addToWishlist={this.props.addToWishlist} addToWishlist={this.props.addToWishlist}
getEpisodeDetails={this.props.getEpisodeDetails} getEpisodeDetails={this.props.getEpisodeDetails}
updateEpisodeDetailsStore={this.props.updateEpisodeDetailsStore}
refreshSubtitles={this.props.refreshSubtitles} refreshSubtitles={this.props.refreshSubtitles}
/> />
) )
@ -177,6 +177,12 @@ class Season extends React.Component {
} }
function Episode(props) { function Episode(props) {
// TODO: remove this when everything uses immutable
let subtitles;
if (props.data.has('subtitles') && props.data.get('subtitles')) {
subtitles = props.data.get('subtitles').toJS();
}
return ( return (
<tr> <tr>
<th scope="row" className="col-xs-2"> <th scope="row" className="col-xs-2">
@ -184,23 +190,24 @@ function Episode(props) {
data={props.data} data={props.data}
addToWishlist={props.addToWishlist} addToWishlist={props.addToWishlist}
/> />
{props.data.episode} {props.data.get('episode')}
</th> </th>
<td className="col-xs-12"> <td className="col-xs-12">
{props.data.title} {props.data.get('title')}
<span className="pull-right episode-buttons"> <span className="pull-right episode-buttons">
<SubtitlesButton <SubtitlesButton
url={props.data.polochon_url} url={props.data.get('polochon_url')}
subtitles={props.data.subtitles} subtitles={subtitles}
refreshSubtitles={props.refreshSubtitles} refreshSubtitles={props.refreshSubtitles}
resourceID={props.data.show_imdb_id} resourceID={props.data.get('show_imdb_id')}
data={props.data} season={props.data.get('season')}
episode={props.data.get('episode')}
type="episode" type="episode"
xs xs
/> />
{props.data.torrents && props.data.torrents.map(function(torrent, index) { {props.data.get('torrents') && props.data.get('torrents').toList().map(function(torrent) {
let key = `${props.data.season}-${props.data.episode}-${torrent.source}-${torrent.quality}`; let key = `${props.data.get('season')}-${props.data.get('episode')}-${torrent.get('source')}-${torrent.get('quality')}`;
return ( return (
<Torrent <Torrent
data={torrent} data={torrent}
@ -210,14 +217,13 @@ function Episode(props) {
) )
})} })}
<DownloadButton <DownloadButton
url={props.data.polochon_url} url={props.data.get('polochon_url')}
subtitles={props.data.subtitles} subtitles={subtitles}
xs xs
/> />
<GetDetailsButton <GetDetailsButton
data={props.data} data={props.data}
getEpisodeDetails={props.getEpisodeDetails} getEpisodeDetails={props.getEpisodeDetails}
updateEpisodeDetailsStore={props.updateEpisodeDetailsStore}
/> />
</span> </span>
</td> </td>
@ -239,9 +245,9 @@ class Torrent extends React.Component {
<span> <span>
<a type="button" <a type="button"
className="btn btn-primary btn-xs" className="btn btn-primary btn-xs"
onClick={(e) => this.handleClick(e, this.props.data.url)} onClick={(e) => this.handleClick(e, this.props.data.get('url'))}
href={this.props.data.url} > href={this.props.data.url} >
<i className="fa fa-download"></i> {this.props.data.quality} <i className="fa fa-download"></i> {this.props.data.get('quality')}
</a> </a>
</span> </span>
) )
@ -255,28 +261,30 @@ class TrackHeader extends React.Component {
} }
handleClick(e, url) { handleClick(e, url) {
e.preventDefault(); e.preventDefault();
let wishlisted = (this.props.data.tracked_season !== null && this.props.data.tracked_episode !== null); const trackedSeason = this.props.data.get('tracked_season');
const trackedEpisode = this.props.data.get('tracked_episode');
const imdbId = this.props.data.get('imdb_id');
const wishlisted = (trackedSeason !== null && trackedEpisode !== null);
if (wishlisted) { if (wishlisted) {
this.props.deleteFromWishlist(this.props.data.imdb_id); this.props.deleteFromWishlist(imdbId);
} else { } else {
this.props.addToWishlist(this.props.data.imdb_id); this.props.addToWishlist(imdbId);
} }
} }
render() { render() {
let wishlisted = (this.props.data.tracked_season !== null && this.props.data.tracked_episode !== null); const trackedSeason = this.props.data.get('tracked_season');
if (wishlisted) { const trackedEpisode = this.props.data.get('tracked_episode');
const imdbId = this.props.data.get('imdb_id');
const wishlisted = (trackedSeason !== null && trackedEpisode !== null);
let msg; let msg;
if (this.props.data.tracked_season !== 0 && this.props.data.tracked_episode !== 0) { if (wishlisted) {
if (trackedSeason !== 0 && trackedEpisode !== 0) {
msg = ( msg = (
<dd> <dd>Show tracked from <strong>season {trackedSeason} episode {trackedEpisode}</strong></dd>
Show tracked from <strong>season {this.props.data.tracked_season} episode {this.props.data.tracked_episode}</strong>
</dd>
); );
} else { } else {
msg = ( msg = (
<dd> <dd>Whole show tracked</dd>
Whole show tracked
</dd>
); );
} }
return ( return (
@ -313,7 +321,10 @@ class TrackButton extends React.Component {
} }
handleClick(e, url) { handleClick(e, url) {
e.preventDefault(); e.preventDefault();
this.props.addToWishlist(this.props.data.show_imdb_id, this.props.data.season, this.props.data.episode); const imdbId = this.props.data.get('show_imdb_id');
const season = this.props.data.get('season');
const episode = this.props.data.get('episode');
this.props.addToWishlist(imdbId, season, episode);
} }
render() { render() {
const tooltipId = `tooltip-${this.props.data.season}-${this.props.data.episode}`; const tooltipId = `tooltip-${this.props.data.season}-${this.props.data.episode}`;
@ -337,22 +348,23 @@ class GetDetailsButton extends React.Component {
} }
handleClick(e, url) { handleClick(e, url) {
e.preventDefault(); e.preventDefault();
if (this.props.data.fetching) { if (this.props.data.get('fetching')) {
return return
} }
this.props.updateEpisodeDetailsStore(this.props.data.show_imdb_id, this.props.data.season, this.props.data.episode); const imdbId = this.props.data.get('show_imdb_id');
console.log(this.props.data); const season = this.props.data.get('season');
this.props.getEpisodeDetails(this.props.data.show_imdb_id, this.props.data.season, this.props.data.episode); const episode = this.props.data.get('episode');
this.props.getEpisodeDetails(imdbId, season, episode);
} }
render() { render() {
return ( return (
<a type="button" className="btn btn-xs btn-info" onClick={(e) => this.handleClick(e)}> <a type="button" className="btn btn-xs btn-info" onClick={(e) => this.handleClick(e)}>
{this.props.data.fetching || {this.props.data.get('fetching') ||
<span> <span>
<i className="fa fa-refresh"></i> Refresh <i className="fa fa-refresh"></i> Refresh
</span> </span>
} }
{this.props.data.fetching && {this.props.data.get('fetching') &&
<span> <span>
<i className="fa fa-spin fa-refresh"></i> Refreshing <i className="fa fa-spin fa-refresh"></i> Refreshing
</span> </span>

View File

@ -1,139 +1,94 @@
const defaultState = { import { OrderedMap, Map, List, fromJS } from 'immutable'
const defaultState = Map({
loading: false, loading: false,
show: { show: Map({
seasons: [], seasons: OrderedMap(),
}, }),
}; });
export default function showStore(state = defaultState, action) { export default function showStore(state = defaultState, action) {
switch (action.type) { switch (action.type) {
case 'SHOW_FETCH_DETAILS_PENDING': case 'SHOW_FETCH_DETAILS_PENDING':
return Object.assign({}, state, { return state.set('loading', true)
loading: true,
})
case 'SHOW_FETCH_DETAILS_FULFILLED': case 'SHOW_FETCH_DETAILS_FULFILLED':
return Object.assign({}, state, { return sortEpisodes(state, action.payload.response.data);
show: sortEpisodes(action.payload.response.data),
loading: false,
})
case 'SHOW_UPDATE_STORE_WISHLIST': case 'SHOW_UPDATE_STORE_WISHLIST':
return Object.assign({}, state, { let season = action.payload.season;
show: updateShowStoreWishlist(Object.assign({}, state.show), action.payload), let episode = action.payload.episode;
}) if (action.payload.wishlisted && season === null) {
case 'EPISODE_GET_DETAILS': season = 0;
return Object.assign({}, state, { episode = 0;
show: updateEpisode(Object.assign({}, state.show), true, action.payload), }
}) return state.mergeDeep(fromJS({
'show': {
'tracked_season': season,
'tracked_episode': episode,
}
}));
case 'EPISODE_GET_DETAILS_PENDING':
return state.setIn(['show', 'seasons', action.payload.main.season, action.payload.main.episode, 'fetching'], true);
case 'EPISODE_GET_DETAILS_FULFILLED': case 'EPISODE_GET_DETAILS_FULFILLED':
return Object.assign({}, state, { let data = action.payload.response.data;
show: updateEpisode(Object.assign({}, state.show), false, action.payload.response.data), if (!data) { return state }
}) data.fetching = false;
return state.setIn(['show', 'seasons', data.season, data.episode], fromJS(data));
case 'EPISODE_SUBTITLES_UPDATE_PENDING': case 'EPISODE_SUBTITLES_UPDATE_PENDING':
return Object.assign({}, state, { return state.setIn(['show', 'seasons', action.payload.main.season, action.payload.main.episode, 'fetchingSubtitles'], true);
show: updateEpisodeSubtitles(Object.assign({}, state.show), action.payload.main.season, action.payload.main.episode, true),
})
case 'EPISODE_SUBTITLES_UPDATE_FULFILLED': case 'EPISODE_SUBTITLES_UPDATE_FULFILLED':
console.log("payload :", action.payload); let epId = ['show', 'seasons', action.payload.main.season, action.payload.main.episode];
return Object.assign({}, state, { let ep = state.getIn(epId);
show: updateEpisodeSubtitles(Object.assign({}, state.show), action.payload.main.season, action.payload.main.episode, false, action.payload.response.data), ep = ep.set('subtitles', fromJS(action.payload.response.data)).set('fetchingSubtitles', false);
}) return state.setIn(epId, ep);
default: default:
return state return state
} }
} }
function updateEpisode(show, fetching, data = null) { function updateEpisode(state, fetching, data = null) {
// Error handling for PouuleT
if (data === null) { if (data === null) {
for (let seasonIndex of Object.keys(show.seasons)) { return state;
for (let episodeIndex of Object.keys(show.seasons[seasonIndex].episodes)) {
show.seasons[seasonIndex].episodes[episodeIndex].fetching = false;
}
}
return show
} }
let seasonIndex = show.seasons.map((el) => el.season).indexOf(data.season.toString()); if (!state.hasIn(['show', 'season', data.season, data.episode])) {
let episodeIndex = show.seasons[seasonIndex].episodes.map((el) => el.episode).indexOf(data.episode); return show;
if ('imdb_id' in data) {
show.seasons[seasonIndex].episodes[episodeIndex] = data;
} }
show.seasons[seasonIndex].episodes[episodeIndex].fetching = fetching;
return show data.fetching = fetching
return show.updateIn(['show', 'seasons', data.season, data.episode], fromJS(data));
} }
function updateEpisodeSubtitles(show, season, episode, fetching, data = null) { function sortEpisodes(state, show) {
let seasonIndex = show.seasons.map((el) => el.season).indexOf(season.toString());
let episodeIndex = show.seasons[seasonIndex].episodes.map((el) => el.episode).indexOf(episode);
if (data) {
show.seasons[seasonIndex].episodes[episodeIndex].subtitles = data;
}
show.seasons[seasonIndex].episodes[episodeIndex].fetchingSubtitles = fetching;
return show
}
function sortEpisodes(show) {
let episodes = show.episodes; let episodes = show.episodes;
delete show["episodes"]; delete show["episodes"];
let ret = state.set('loading', false);
if (episodes.length == 0) { if (episodes.length == 0) {
return show; return ret;
} }
// Extract the seasons // Set the show data
let seasons = {}; ret = ret.set('show', fromJS(show));
// Set the show episodes
for (let ep of episodes) { for (let ep of episodes) {
ep.fetching = false; ep.fetching = false;
if (!seasons[ep.season]) { ret = ret.setIn(['show', 'seasons', ep.season, ep.episode], fromJS(ep));
seasons[ep.season] = { episodes: [] };
}
seasons[ep.season].episodes.push(ep);
} }
if (seasons.length === 0) { // Sort the episodes
return show; ret = ret.updateIn(['show', 'seasons'], function(seasons) {
} return seasons.map(function(episodes) {
return episodes.sort((a,b) => a.get('episode') - b.get('episode'));
});
});
// Put all the season in an array // Sort the seasons
let sortedSeasons = []; ret = ret.updateIn(['show', 'seasons'], function(seasons) {
for (let season of Object.keys(seasons)) { return seasons.sort(function(a,b) {
let seasonEpisodes = seasons[season].episodes; return a.first().get('season') - b.first().get('season');
// 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 return ret
for (let i=0; i<sortedSeasons.length; i++) {
sortedSeasons.sort((a,b) => (a.season - b.season))
}
show.seasons = sortedSeasons;
return show;
}
// Update the store containing the current detailed show
function updateShowStoreWishlist(show, payload) {
if (show.seasons.length === 0) {
return show;
}
let season = payload.season;
let episode = payload.episode;
if (payload.wishlisted) {
if (season === null) {
season = 0;
}
if (episode === null) {
episode = 0;
}
}
show.tracked_season = season;
show.tracked_episode = episode;
return show
} }

View File

@ -15,13 +15,6 @@ import { fetchShows, fetchShowDetails, getShowExploreOptions } from './actions/s
import store from './store' import store from './store'
function startPollingTorrents() {
return request(
'TORRENTS_FETCH',
configureAxios().get('/torrents')
)
}
// This function returns true if the user is logged in, false otherwise // This function returns true if the user is logged in, false otherwise
function isLoggedIn() { function isLoggedIn() {
const state = store.getState(); const state = store.getState();
@ -198,7 +191,7 @@ export default function getRoutes(App) {
loginCheck(nextState, replace, next, function() { loginCheck(nextState, replace, next, function() {
var state = store.getState(); var state = store.getState();
// Fetch the explore options // Fetch the explore options
if (Object.keys(state.showStore.exploreOptions).length === 0) { if (Object.keys(state.showsStore.exploreOptions).length === 0) {
store.dispatch(getShowExploreOptions()); store.dispatch(getShowExploreOptions());
} }
store.dispatch(fetchShows( store.dispatch(fetchShows(