Merge branch 'subtitles' into 'master'

Add subtitles in the front end

See merge request !72
This commit is contained in:
Grégoire Delattre 2017-05-29 12:49:06 +00:00
commit 12e43b4397
15 changed files with 301 additions and 70 deletions

View File

@ -325,12 +325,23 @@ func RefreshMovieSubtitlesHandler(env *web.Env, w http.ResponseWriter, r *http.R
return env.RenderError(w, err)
}
err = client.UpdateSubtitles(&papi.Movie{ImdbID: id})
movie := &papi.Movie{ImdbID: id}
refreshSubs, err := client.UpdateSubtitles(movie)
if err != nil {
return env.RenderError(w, err)
}
return env.RenderOK(w, "Subtitles refreshed")
subs := []subtitles.Subtitle{}
for _, lang := range refreshSubs {
subtitleURL, _ := client.SubtitleURL(movie, lang)
subs = append(subs, subtitles.Subtitle{
Language: lang,
URL: subtitleURL,
VVTFile: fmt.Sprintf("/movies/%s/subtitles/%s", id, lang),
})
}
return env.RenderJSON(w, subs)
}
// DownloadVVTSubtitle returns a vvt subtitle for the movie

View File

@ -12,6 +12,7 @@ import (
"github.com/odwrtw/polochon/lib"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/backend"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/subtitles"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/users"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web"
)
@ -31,12 +32,7 @@ func (m *Movie) MarshalJSON() ([]byte, error) {
type Alias Movie
var downloadURL string
type Subtitle struct {
Language string `json:"language"`
URL string `json:"url"`
VVTFile string `json:"vvt_file"`
}
var subtitles []Subtitle
var subs []subtitles.Subtitle
// If the episode is present, fill the downloadURL
if m.pMovie != nil {
// Get the DownloadURL
@ -44,7 +40,7 @@ func (m *Movie) MarshalJSON() ([]byte, error) {
// Append the Subtitles
for _, l := range m.pMovie.Subtitles {
subtitleURL, _ := m.client.SubtitleURL(m.pMovie, l)
subtitles = append(subtitles, Subtitle{
subs = append(subs, subtitles.Subtitle{
Language: l,
URL: subtitleURL,
VVTFile: fmt.Sprintf("/movies/%s/subtitles/%s", m.ImdbID, l),
@ -57,12 +53,12 @@ func (m *Movie) MarshalJSON() ([]byte, error) {
*Alias
PolochonURL string `json:"polochon_url"`
PosterURL string `json:"poster_url"`
Subtitles []Subtitle `json:"subtitles"`
Subtitles []subtitles.Subtitle `json:"subtitles"`
}{
Alias: (*Alias)(m),
PolochonURL: downloadURL,
PosterURL: m.PosterURL(),
Subtitles: subtitles,
Subtitles: subs,
}
return json.Marshal(movieToMarshal)

View File

@ -9,6 +9,7 @@ import (
polochon "github.com/odwrtw/polochon/lib"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/backend"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/subtitles"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web"
)
@ -24,12 +25,7 @@ func (e *Episode) MarshalJSON() ([]byte, error) {
type alias Episode
var downloadURL string
type Subtitle struct {
Language string `json:"language"`
URL string `json:"url"`
VVTFile string `json:"vvt_file"`
}
var subtitles []Subtitle
var subs []subtitles.Subtitle
// If the episode is present, fill the downloadURL
if e.pShow != nil {
pEpisode := e.pShow.GetEpisode(e.Season, e.Episode)
@ -45,7 +41,7 @@ func (e *Episode) MarshalJSON() ([]byte, error) {
// Append the Subtitles
for _, l := range pEpisode.Subtitles {
subtitleURL, _ := e.client.SubtitleURL(pEpisode, l)
subtitles = append(subtitles, Subtitle{
subs = append(subs, subtitles.Subtitle{
Language: l,
URL: subtitleURL,
VVTFile: fmt.Sprintf("/shows/%s/seasons/%d/episodes/%d/subtitles/%s", e.ShowImdbID, e.Season, e.Episode, l),
@ -58,11 +54,11 @@ func (e *Episode) MarshalJSON() ([]byte, error) {
episodeToMarshal := &struct {
*alias
PolochonURL string `json:"polochon_url"`
Subtitles []Subtitle `json:"subtitles"`
Subtitles []subtitles.Subtitle `json:"subtitles"`
}{
alias: (*alias)(e),
PolochonURL: downloadURL,
Subtitles: subtitles,
Subtitles: subs,
}
return json.Marshal(episodeToMarshal)

View File

@ -374,16 +374,28 @@ func RefreshEpisodeSubtitlesHandler(env *web.Env, w http.ResponseWriter, r *http
return env.RenderError(w, err)
}
err = client.UpdateSubtitles(&papi.Episode{
e := &papi.Episode{
ShowImdbID: id,
Season: season,
Episode: episode,
})
}
refreshedSubs, err := client.UpdateSubtitles(e)
if err != nil {
return env.RenderError(w, err)
}
return env.RenderOK(w, "Subtitles refreshed")
subs := []subtitles.Subtitle{}
for _, lang := range refreshedSubs {
subtitleURL, _ := client.SubtitleURL(e, lang)
subs = append(subs, subtitles.Subtitle{
Language: lang,
URL: subtitleURL,
VVTFile: fmt.Sprintf("/shows/%s/seasons/%d/episodes/%d/subtitles/%s", e.ShowImdbID, e.Season, e.Episode, lang),
})
}
return env.RenderJSON(w, subs)
}
// DownloadVVTSubtitle returns a vvt subtitle for the movie

View File

@ -0,0 +1,8 @@
package subtitles
// Subtitle represents a Subtitle
type Subtitle struct {
Language string `json:"language"`
URL string `json:"url"`
VVTFile string `json:"vvt_file"`
}

View File

@ -0,0 +1,36 @@
import { configureAxios, request } from '../requests'
import { addAlertOk } from './alerts'
export function refreshSubtitles(type, id, season, episode) {
switch (type) {
case 'movie':
var resourceURL = `/movies/${id}`
return request(
'MOVIE_SUBTITLES_UPDATE',
configureAxios().post(`${resourceURL}/subtitles/refresh`),
[
addAlertOk("Subtitles refreshed"),
],
{
imdb_id: id,
},
)
case 'episode':
var resourceURL = `/shows/${id}/seasons/${season}/episodes/${episode}`
return request(
'EPISODE_SUBTITLES_UPDATE',
configureAxios().post(`${resourceURL}/subtitles/refresh`),
[
addAlertOk("Subtitles refreshed"),
],
{
imdb_id: id,
season: season,
episode: episode,
},
)
default:
console.log("refreshSubtitles - Unknown type " + type)
}
}

View File

@ -0,0 +1,97 @@
import React from 'react'
import { DropdownButton, MenuItem } from 'react-bootstrap'
export default class SubtitlesButton extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick(e, url) {
e.preventDefault();
if (this.props.fetching) {
return
}
// Refresh the subtitles
this.props.refreshSubtitles(this.props.type, this.props.resourceID, this.props.data.season, this.props.data.episode);
}
render() {
// If there is no URL, the resource is not in polochon, we won't be able to download subtitles
if (this.props.url === "") {
return null;
}
// Build the button
const entries = buildMenuItems(this.props.subtitles);
let btnSize = "small";
if (this.props.xs) {
btnSize = "xsmall";
}
return (
<DropdownButton bsStyle="success" bsSize={btnSize} title="Subtitles" id="download-subtitles-button" dropup>
{entries.map(function(e, index) {
switch (e.type) {
case 'action':
return (
<MenuItem key={index} onClick={(event) => this.handleClick(event, e.url)}>
{this.props.fetchingSubtitles ||
<span>
<i className="fa fa-refresh">
</i> Refresh
</span>
}
{this.props.fetchingSubtitles &&
<span>
<i className="fa fa-spin fa-refresh">
</i> Refreshing
</span>
}
</MenuItem>
);
case 'divider':
return (
<MenuItem key={index} divider></MenuItem>
);
case 'entry':
return (
<MenuItem key={index} href={e.url}>
<i className="fa fa-download"></i> {e.lang}
</MenuItem>
);
}
}, this)}
</DropdownButton>
);
}
}
function buildMenuItems(subtitles) {
// Build the array of entries
let entries = [];
// Push the refresh button
entries.push({
type: "action",
value: "Refresh",
});
// If there is no subtitles, stop here
if (!subtitles) {
return entries;
}
// Push the divider
entries.push({ type: "divider" });
// Push the subtitles
for (let sub of subtitles) {
entries.push({
type: "entry",
// Take only the last part of fr_FR
lang: sub.language.split("_")[1],
url: sub.url,
});
}
return entries;
}

View File

@ -2,10 +2,12 @@ import React from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import { addTorrent } from '../../actions/torrents'
import { refreshSubtitles } from '../../actions/subtitles'
import { addMovieToWishlist, deleteMovieFromWishlist,
getMovieDetails, selectMovie } from '../../actions/movies'
import DownloadButton from '../buttons/download'
import SubtitlesButton from '../buttons/subtitles'
import TorrentsButton from './torrents'
import ActionsButton from './actions'
import ListPosters from '../list/posters'
@ -16,7 +18,7 @@ function mapStateToProps(state) {
}
const mapDispatchToProps = (dipatch) =>
bindActionCreators({ selectMovie, getMovieDetails, addTorrent,
addMovieToWishlist, deleteMovieFromWishlist }, dipatch)
addMovieToWishlist, deleteMovieFromWishlist, refreshSubtitles }, dipatch)
function MovieButtons(props) {
const imdb_link = `http://www.imdb.com/title/${props.movie.imdb_id}`;
@ -48,6 +50,15 @@ function MovieButtons(props) {
subtitles={props.movie.subtitles}
/>
<SubtitlesButton
url={props.movie.polochon_url}
subtitles={props.movie.subtitles}
refreshSubtitles={props.refreshSubtitles}
resourceID={props.movie.imdb_id}
data={props.movie}
type="movie"
/>
<a type="button" className="btn btn-warning btn-sm" href={imdb_link}>
<i className="fa fa-external-link"></i> IMDB
</a>
@ -96,6 +107,7 @@ class MovieList extends React.Component {
addToWishlist={this.props.addMovieToWishlist}
deleteFromWishlist={this.props.deleteMovieFromWishlist}
lastFetchUrl={this.props.movieStore.lastFetchUrl}
refreshSubtitles={this.props.refreshSubtitles}
/>
</ListDetails>
}

View File

@ -2,11 +2,13 @@ import React from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import { addTorrent } from '../../actions/torrents'
import { refreshSubtitles } from '../../actions/subtitles'
import { addShowToWishlist, deleteFromWishlist, getEpisodeDetails,
updateEpisodeDetailsStore, updateShowDetails } from '../../actions/shows'
import Loader from '../loader/loader'
import DownloadButton from '../buttons/download'
import SubtitlesButton from '../buttons/subtitles'
import { OverlayTrigger, Tooltip } from 'react-bootstrap'
@ -18,7 +20,8 @@ function mapStateToProps(state) {
}
const mapDispatchToProps = (dispatch) =>
bindActionCreators({addTorrent, addShowToWishlist, deleteFromWishlist,
updateShowDetails, updateEpisodeDetailsStore, getEpisodeDetails }, dispatch)
updateShowDetails, updateEpisodeDetailsStore, getEpisodeDetails,
refreshSubtitles }, dispatch)
class ShowDetails extends React.Component {
render() {
@ -39,6 +42,7 @@ class ShowDetails extends React.Component {
addToWishlist={this.props.addShowToWishlist}
getEpisodeDetails={this.props.getEpisodeDetails}
updateEpisodeDetailsStore={this.props.updateEpisodeDetailsStore}
refreshSubtitles={this.props.refreshSubtitles}
/>
</div>
);
@ -113,6 +117,7 @@ function SeasonsList(props){
addToWishlist={props.addToWishlist}
getEpisodeDetails={props.getEpisodeDetails}
updateEpisodeDetailsStore={props.updateEpisodeDetailsStore}
refreshSubtitles={props.refreshSubtitles}
/>
</div>
)
@ -159,6 +164,7 @@ class Season extends React.Component {
addToWishlist={this.props.addToWishlist}
getEpisodeDetails={this.props.getEpisodeDetails}
updateEpisodeDetailsStore={this.props.updateEpisodeDetailsStore}
refreshSubtitles={this.props.refreshSubtitles}
/>
)
}, this)}
@ -181,8 +187,17 @@ function Episode(props) {
{props.data.episode}
</th>
<td className="col-xs-5">{props.data.title}</td>
<td className="col-xs-6">
<span className="pull-right">
<td className="col-xs-6 list-details-button">
<span className="pull-right episode-buttons btn-toolbar">
<SubtitlesButton
url={props.data.polochon_url}
subtitles={props.data.subtitles}
refreshSubtitles={props.refreshSubtitles}
resourceID={props.data.show_imdb_id}
data={props.data}
type="episode"
xs
/>
{props.data.torrents && props.data.torrents.map(function(torrent, index) {
let key = `${props.data.season}-${props.data.episode}-${torrent.source}-${torrent.quality}`;
return (
@ -196,7 +211,6 @@ function Episode(props) {
<DownloadButton
url={props.data.polochon_url}
subtitles={props.data.subtitles}
customClassName="episode-button"
xs
/>
<GetDetailsButton
@ -221,7 +235,7 @@ class Torrent extends React.Component {
}
render() {
return (
<span className="episode-button">
<span>
<a type="button"
className="btn btn-primary btn-xs"
onClick={(e) => this.handleClick(e, this.props.data.url)}
@ -306,7 +320,7 @@ class TrackButton extends React.Component {
<Tooltip id={tooltipId}>Track show from here</Tooltip>
);
return (
<OverlayTrigger placement="top" overlay={tooltip} className="episode-button">
<OverlayTrigger placement="top" overlay={tooltip}>
<a type="button" className="btn btn-default btn-xs" onClick={(e) => this.handleClick(e)}>
<i className="fa fa-bookmark"></i>
</a>
@ -326,11 +340,11 @@ class GetDetailsButton extends React.Component {
return
}
this.props.updateEpisodeDetailsStore(this.props.data.show_imdb_id, this.props.data.season, this.props.data.episode);
console.log(this.props.data);
this.props.getEpisodeDetails(this.props.data.show_imdb_id, this.props.data.season, this.props.data.episode);
}
render() {
return (
<span className="episode-button">
<a type="button" className="btn btn-xs btn-info" onClick={(e) => this.handleClick(e)}>
{this.props.data.fetching ||
<span>
@ -343,7 +357,6 @@ class GetDetailsButton extends React.Component {
</span>
}
</a>
</span>
);
}
}

View File

@ -18,13 +18,13 @@ export default function movieStore(state = defaultState, action) {
case 'MOVIE_LIST_FETCH_FULFILLED':
let selectedImdbId = "";
// Select the first movie
if (action.payload.data.length > 0) {
if (action.payload.response.data.length > 0) {
// Sort by year
action.payload.data.sort((a,b) => b.year - a.year);
selectedImdbId = action.payload.data[0].imdb_id;
action.payload.response.data.sort((a,b) => b.year - a.year);
selectedImdbId = action.payload.response.data[0].imdb_id;
}
return Object.assign({}, state, {
movies: action.payload.data,
movies: action.payload.response.data,
selectedImdbId: selectedImdbId,
filter: defaultState.filter,
perPage: defaultState.perPage,
@ -36,7 +36,7 @@ export default function movieStore(state = defaultState, action) {
})
case 'MOVIE_GET_DETAILS_FULFILLED':
return Object.assign({}, state, {
movies: updateMovieDetails(state.movies.slice(), action.payload.data.imdb_id, action.payload.data),
movies: updateMovieDetails(state.movies.slice(), action.payload.response.data.imdb_id, action.payload.response.data),
fetchingDetails: false,
})
case 'MOVIE_UPDATE_STORE_WISHLIST':
@ -45,12 +45,21 @@ export default function movieStore(state = defaultState, action) {
})
case 'MOVIE_GET_EXPLORE_OPTIONS_FULFILLED':
return Object.assign({}, state, {
exploreOptions: action.payload.data,
exploreOptions: action.payload.response.data,
})
case 'UPDATE_LAST_MOVIE_FETCH_URL':
return Object.assign({}, state, {
lastFetchUrl: action.payload.url,
})
case 'MOVIE_SUBTITLES_UPDATE_PENDING':
return Object.assign({}, state, {
movies: updateMovieSubtitles(state.movies.slice(), state.selectedImdbId, true),
})
case 'MOVIE_SUBTITLES_UPDATE_FULFILLED':
console.log("payload :", action.payload);
return Object.assign({}, state, {
movies: updateMovieSubtitles(state.movies.slice(), state.selectedImdbId, false, action.payload.response.data),
})
case 'SELECT_MOVIE':
// Don't select the movie if we're fetching another movie's details
if (state.fetchingDetails) {
@ -76,3 +85,12 @@ function updateStoreWishlist(movies, imdbId, wishlisted) {
movies[index].wishlisted = wishlisted;
return movies
}
function updateMovieSubtitles(movies, imdbId, fetching, data = null) {
let index = movies.map((el) => el.imdb_id).indexOf(imdbId);
if (data) {
movies[index].subtitles = data;
}
movies[index].fetchingSubtitles = fetching;
return movies
}

View File

@ -21,11 +21,12 @@ export default function showStore(state = defaultState, action) {
case 'SHOW_LIST_FETCH_FULFILLED':
let selectedImdbId = "";
// Select the first show
if (action.payload.data.length > 0) {
selectedImdbId = action.payload.data[0].imdb_id;
console.log("Hey", action.payload);
if (action.payload.response.data.length > 0) {
selectedImdbId = action.payload.response.data[0].imdb_id;
}
return Object.assign({}, state, {
shows: action.payload.data,
shows: action.payload.response.data,
selectedImdbId: selectedImdbId,
filter: defaultState.filter,
perPage: defaultState.perPage,
@ -37,7 +38,7 @@ export default function showStore(state = defaultState, action) {
})
case 'SHOW_GET_DETAILS_FULFILLED':
return Object.assign({}, state, {
shows: updateShowDetails(state.shows.slice(), action.payload.data),
shows: updateShowDetails(state.shows.slice(), action.payload.response.data),
getDetails: false,
})
case 'SHOW_FETCH_DETAILS_PENDING':
@ -46,7 +47,7 @@ export default function showStore(state = defaultState, action) {
})
case 'SHOW_FETCH_DETAILS_FULFILLED':
return Object.assign({}, state, {
show: sortEpisodes(action.payload.data),
show: sortEpisodes(action.payload.response.data),
loading: false,
})
case 'EPISODE_GET_DETAILS':
@ -55,7 +56,7 @@ export default function showStore(state = defaultState, action) {
})
case 'EPISODE_GET_DETAILS_FULFILLED':
return Object.assign({}, state, {
show: updateEpisode(Object.assign({}, state.show), false, action.payload.data),
show: updateEpisode(Object.assign({}, state.show), false, action.payload.response.data),
})
case 'EXPLORE_SHOWS_PENDING':
return Object.assign({}, state, {
@ -63,12 +64,12 @@ export default function showStore(state = defaultState, action) {
})
case 'EXPLORE_SHOWS_FULFILLED':
return Object.assign({}, state, {
shows: action.payload.data,
shows: action.payload.response.data,
loading: false,
})
case 'SHOW_GET_EXPLORE_OPTIONS_FULFILLED':
return Object.assign({}, state, {
exploreOptions: action.payload.data,
exploreOptions: action.payload.response.data,
})
case 'SHOW_UPDATE_STORE_WISHLIST':
return Object.assign({}, state, {
@ -79,6 +80,15 @@ export default function showStore(state = defaultState, action) {
return Object.assign({}, state, {
lastShowsFetchUrl: action.payload.url,
})
case 'EPISODE_SUBTITLES_UPDATE_PENDING':
return Object.assign({}, state, {
show: updateEpisodeSubtitles(Object.assign({}, state.show), action.payload.main.season, action.payload.main.episode, true),
})
case 'EPISODE_SUBTITLES_UPDATE_FULFILLED':
console.log("payload :", action.payload);
return Object.assign({}, state, {
show: updateEpisodeSubtitles(Object.assign({}, state.show), action.payload.main.season, action.payload.main.episode, false, action.payload.response.data),
})
case 'SELECT_SHOW':
// Don't select the show if we're fetching another show's details
if (state.fetchingDetails) {
@ -113,6 +123,17 @@ function updateEpisode(show, fetching, data = null) {
return show
}
function updateEpisodeSubtitles(show, season, episode, fetching, data = null) {
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;
delete show["episodes"];

View File

@ -12,7 +12,7 @@ export default function torrentStore(state = defaultState, action) {
case 'TORRENTS_FETCH_FULFILLED':
return state.merge(fromJS({
fetching: false,
torrents: action.payload.data,
torrents: action.payload.response.data,
}));
default:
return state

View File

@ -17,18 +17,18 @@ export default function userStore(state = defaultState, action) {
userLoading: true,
})
case 'USER_LOGIN_FULFILLED':
if (action.payload.status === "error") {
if (action.payload.response.status === "error") {
return logoutUser(state)
}
return updateFromToken(state, action.payload.data.token)
return updateFromToken(state, action.payload.response.data.token)
case 'USER_SET_TOKEN':
return updateFromToken(state, action.payload.token)
case 'USER_LOGOUT':
return logoutUser(state)
case 'GET_USER_FULFILLED':
return Object.assign({}, state, {
polochonToken: action.payload.data.token,
polochonUrl: action.payload.data.url,
polochonToken: action.payload.response.data.token,
polochonUrl: action.payload.response.data.url,
})
default:
return state;

View File

@ -16,7 +16,7 @@ export function configureAxios(headers = {}) {
// This function takes en event prefix to dispatch evens during the life of the
// request, it also take a promise (axios request)
export function request(eventPrefix, promise, callbackEvents = null) {
export function request(eventPrefix, promise, callbackEvents = null, mainPayload = null) {
// Events
const pending = `${eventPrefix}_PENDING`;
const fulfilled = `${eventPrefix}_FULFILLED`;
@ -24,6 +24,9 @@ export function request(eventPrefix, promise, callbackEvents = null) {
return function(dispatch) {
dispatch({
type: pending,
payload: {
main: mainPayload,
}
})
promise
.then(response => {
@ -33,12 +36,16 @@ export function request(eventPrefix, promise, callbackEvents = null) {
type: 'ADD_ALERT_ERROR',
payload: {
message: response.data.message,
main: mainPayload,
}
})
}
dispatch({
type: fulfilled,
payload: response.data,
payload: {
response: response.data,
main: mainPayload,
},
})
if (callbackEvents) {
for (let event of callbackEvents) {
@ -57,6 +64,7 @@ export function request(eventPrefix, promise, callbackEvents = null) {
type: 'ADD_ALERT_ERROR',
payload: {
message: error.response.data,
main: mainPayload,
}
})
})

View File

@ -46,8 +46,11 @@ body {
max-height: 300px;
}
.episode-button {
padding-right: 5px;
.episode-buttons {
display: flex;
span {
margin-left: 5px;
}
}
.navbar {