diff --git a/package.json b/package.json index 5543d11..cbcf306 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,6 @@ "react-dom": "^15.3.2", "react-infinite-scroller": "^1.0.4", "react-loading": "^0.0.9", - "react-player": "^0.14.1", "react-redux": "^4.4.6", "react-redux-form": "^1.2.4", "react-router": "^3.0.0", @@ -27,7 +26,8 @@ "react-router-redux": "^4.0.7", "redux": "^3.6.0", "redux-logger": "^2.7.4", - "redux-thunk": "^2.1.0" + "redux-thunk": "^2.1.0", + "universal-cookie": "^2.0.7" }, "devDependencies": { "axios": "^0.15.2", diff --git a/src/internal/auth/auth.go b/src/internal/auth/auth.go index 0597981..b072cb1 100644 --- a/src/internal/auth/auth.go +++ b/src/internal/auth/auth.go @@ -80,14 +80,26 @@ func (a *Authorizer) Login(rw http.ResponseWriter, req *http.Request, username, // CurrentUser returns the logged in username from session and verifies the token func (a *Authorizer) CurrentUser(rw http.ResponseWriter, req *http.Request) (User, error) { + var tokenStr string h := req.Header.Get("Authorization") - // No user logged - if h == "" { - return nil, nil + if h != "" { + // Get the token from the header + tokenStr = strings.Replace(h, "Bearer ", "", -1) } - // Get the token from the header - tokenStr := strings.Replace(h, "Bearer ", "", -1) + // If the token string is still empty, check in the cookies + if tokenStr == "" { + tokenCookie, err := req.Cookie("token") + if err != nil || tokenCookie == nil { + return nil, nil + } + tokenStr = tokenCookie.Value + } + + // No user logged + if tokenStr == "" { + return nil, nil + } // Keyfunc to decode the token var keyfunc jwt.Keyfunc = func(token *jwt.Token) (interface{}, error) { diff --git a/src/internal/movies/handlers.go b/src/internal/movies/handlers.go index fda3117..49cc7b1 100644 --- a/src/internal/movies/handlers.go +++ b/src/internal/movies/handlers.go @@ -14,6 +14,7 @@ import ( "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/auth" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/backend" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/config" + "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" ) @@ -331,3 +332,30 @@ func RefreshMovieSubtitlesHandler(env *web.Env, w http.ResponseWriter, r *http.R return env.RenderOK(w, "Subtitles refreshed") } + +// DownloadVVTSubtitle returns a vvt subtitle for the movie +func DownloadVVTSubtitle(env *web.Env, w http.ResponseWriter, r *http.Request) error { + vars := mux.Vars(r) + id := vars["id"] + lang := vars["lang"] + + // Get the user + v := auth.GetCurrentUser(r, env.Log) + user, ok := v.(*users.User) + if !ok { + return fmt.Errorf("invalid user type") + } + + // Create a new papi client + client, err := user.NewPapiClient() + if err != nil { + return env.RenderError(w, err) + } + + url, err := client.SubtitleURL(&papi.Movie{ImdbID: id}, lang) + if err != nil { + return env.RenderError(w, err) + } + + return subtitles.ConvertSubtitle(url, w) +} diff --git a/src/internal/movies/movies.go b/src/internal/movies/movies.go index e4147c4..9b07363 100644 --- a/src/internal/movies/movies.go +++ b/src/internal/movies/movies.go @@ -34,6 +34,7 @@ func (m *Movie) MarshalJSON() ([]byte, error) { type Subtitle struct { Language string `json:"language"` URL string `json:"url"` + VVTFile string `json:"vvt_file"` } var subtitles []Subtitle // If the episode is present, fill the downloadURL @@ -46,6 +47,7 @@ func (m *Movie) MarshalJSON() ([]byte, error) { subtitles = append(subtitles, Subtitle{ Language: l, URL: subtitleURL, + VVTFile: fmt.Sprintf("/movies/%s/subtitles/%s", m.ImdbID, l), }) } } diff --git a/src/internal/shows/episodes.go b/src/internal/shows/episodes.go index b3535e7..269e59f 100644 --- a/src/internal/shows/episodes.go +++ b/src/internal/shows/episodes.go @@ -2,6 +2,7 @@ package shows import ( "encoding/json" + "fmt" "github.com/Sirupsen/logrus" "github.com/odwrtw/papi" @@ -26,6 +27,7 @@ func (e *Episode) MarshalJSON() ([]byte, error) { type Subtitle struct { Language string `json:"language"` URL string `json:"url"` + VVTFile string `json:"vvt_file"` } var subtitles []Subtitle // If the episode is present, fill the downloadURL @@ -46,6 +48,7 @@ func (e *Episode) MarshalJSON() ([]byte, error) { subtitles = append(subtitles, Subtitle{ Language: l, URL: subtitleURL, + VVTFile: fmt.Sprintf("/shows/%s/seasons/%d/episodes/%d/subtitles/%s", e.ShowImdbID, e.Season, e.Episode, l), }) } } diff --git a/src/internal/shows/handlers.go b/src/internal/shows/handlers.go index 3d3a011..5a3b9fe 100644 --- a/src/internal/shows/handlers.go +++ b/src/internal/shows/handlers.go @@ -9,6 +9,7 @@ import ( "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/auth" "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" @@ -384,3 +385,36 @@ func RefreshEpisodeSubtitlesHandler(env *web.Env, w http.ResponseWriter, r *http return env.RenderOK(w, "Subtitles refreshed") } + +// DownloadVVTSubtitle returns a vvt subtitle for the movie +func DownloadVVTSubtitle(env *web.Env, w http.ResponseWriter, r *http.Request) error { + vars := mux.Vars(r) + id := vars["id"] + lang := vars["lang"] + season, _ := strconv.Atoi(vars["season"]) + episode, _ := strconv.Atoi(vars["episode"]) + + // Get the user + v := auth.GetCurrentUser(r, env.Log) + user, ok := v.(*users.User) + if !ok { + return fmt.Errorf("invalid user type") + } + + // Create a new papi client + client, err := user.NewPapiClient() + if err != nil { + return env.RenderError(w, err) + } + + url, err := client.SubtitleURL(&papi.Episode{ + ShowImdbID: id, + Season: season, + Episode: episode, + }, lang) + if err != nil { + return env.RenderError(w, err) + } + + return subtitles.ConvertSubtitle(url, w) +} diff --git a/src/internal/subtitles/handler.go b/src/internal/subtitles/handler.go new file mode 100644 index 0000000..6cb79b9 --- /dev/null +++ b/src/internal/subtitles/handler.go @@ -0,0 +1,43 @@ +package subtitles + +import ( + "fmt" + "net/http" + + "github.com/gregdel/srt2vtt" +) + +// ConvertSubtitle downloads and converts a subtitle from srt to vvt +func ConvertSubtitle(url string, w http.ResponseWriter) error { + // Client with compression disabled + c := &http.Client{ + Transport: &http.Transport{DisableCompression: true}, + } + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return err + } + + resp, err := c.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("Invalid subitle response code: %d", resp.StatusCode) + } + + reader, err := srt2vtt.NewReader(resp.Body) + if err != nil { + return err + } + + _, err = reader.WriteTo(w) + if err != nil { + return err + } + + return nil +} diff --git a/src/public/js/app.js b/src/public/js/app.js index 3324d12..366c3b8 100644 --- a/src/public/js/app.js +++ b/src/public/js/app.js @@ -11,9 +11,11 @@ import 'file-loader?name=[name].png!../img/apple-touch-icon.png' import 'file-loader?name=[name].png!../img/favicon-16x16.png' import 'file-loader?name=[name].png!../img/favicon-32x32.png' import 'file-loader?name=[name].png!../img/favicon.ico' -import 'file-loader?name=[name].png!../img/manifest.json' import 'file-loader?name=[name].png!../img/safari-pinned-tab.svg' +// Import manifest +import 'file-loader?name=[name].json!../manifest.json' + // Styles import '../less/app.less' @@ -79,15 +81,15 @@ export function startPollingTorrents() { ) } -var pollingTorrentsId; -const loginCheck = function(nextState, replace, next, f) { +// This function returns true if the user is logged in, false otherwise +function isLoggedIn() { const state = store.getState(); const isLogged = state.userStore.isLogged; let token = localStorage.getItem('token'); // Let's check if the user has a token, if he does let's assume he's logged // in. If that's not the case he will be logged out on the fisrt query - if (token !== "") { + if (token && token !== "") { store.dispatch({ type: 'USER_SET_TOKEN', payload: { @@ -96,10 +98,23 @@ const loginCheck = function(nextState, replace, next, f) { }); } - if (!isLogged && token === "") { + if (isLogged || (token && token !== "")) { + return true + } + + return false +} + +var pollingTorrentsId; +const loginCheck = function(nextState, replace, next, f = null) { + const loggedIn = isLoggedIn(); + if (!loggedIn) { replace('/users/login'); } else { - f(); + if (f) { + f(); + } + // Poll torrents once logged if (!pollingTorrentsId) { // Fetch the torrents every 10s @@ -112,18 +127,42 @@ const loginCheck = function(nextState, replace, next, f) { next(); } +const defaultRoute = '/movies/explore/yts/seeds'; const routes = { path: '/', component: App, - indexRoute: {onEnter: ({params}, replace) => replace('/movies/explore/yts/seeds')}, + indexRoute: {onEnter: ({params}, replace) => replace(defaultRoute)}, childRoutes: [ - { path: '/users/login' , component: UserLoginForm }, - { path: '/users/signup' , component: UserSignUp }, - { path: '/users/edit' , component: UserEdit }, - { path: '/users/signup' , component: UserSignUp }, + { + path: '/users/signup', + component: UserSignUp + }, + { + path: '/users/login', + component: UserLoginForm, + onEnter: function(nextState, replace, next) { + if (isLoggedIn()) { + // User is already logged in, redirect him to the default route + replace(defaultRoute); + } + next(); + }, + }, + { + path: '/users/edit', + component: UserEdit, + onEnter: function(nextState, replace, next) { + loginCheck(nextState, replace, next); + }, + }, { path: '/users/logout', onEnter: function(nextState, replace, next) { + // Stop polling + if (pollingTorrentsId !== null) { + clearInterval(pollingTorrentsId); + pollingTorrentsId = null; + } store.dispatch(actionCreators.userLogout()); replace('/users/login'); next(); diff --git a/src/public/js/components/buttons/download.js b/src/public/js/components/buttons/download.js index 1089be6..8c18ece 100644 --- a/src/public/js/components/buttons/download.js +++ b/src/public/js/components/buttons/download.js @@ -1,5 +1,4 @@ import React from 'react' -import ReactPlayer from 'react-player' import { Button, Dropdown, MenuItem, Modal } from 'react-bootstrap' @@ -56,12 +55,46 @@ export default class DownloadButton extends React.Component { -
- -
+
); } } + +class Player extends React.Component { + constructor(props) { + super(props); + var subtitles = []; + if (props.subtitles && props.subtitles.length) { + subtitles = props.subtitles; + } + this.state = { + subtitles: subtitles, + }; + } + render() { + return ( +
+ +
+ ); + } +} diff --git a/src/public/js/components/movies/list.js b/src/public/js/components/movies/list.js index 402db75..6014cb2 100644 --- a/src/public/js/components/movies/list.js +++ b/src/public/js/components/movies/list.js @@ -32,7 +32,10 @@ function MovieButtons(props) { /> } - + IMDB diff --git a/src/public/js/components/shows/details.js b/src/public/js/components/shows/details.js index 220ce43..81f0332 100644 --- a/src/public/js/components/shows/details.js +++ b/src/public/js/components/shows/details.js @@ -182,6 +182,7 @@ function Episode(props) { })} diff --git a/src/public/js/reducers/users.js b/src/public/js/reducers/users.js index 894982e..d039fb2 100644 --- a/src/public/js/reducers/users.js +++ b/src/public/js/reducers/users.js @@ -1,4 +1,5 @@ import jwtDecode from 'jwt-decode' +import Cookies from 'universal-cookie' const defaultState = { userLoading: false, @@ -36,12 +37,19 @@ export default function userStore(state = defaultState, action) { function logoutUser(state) { localStorage.removeItem('token'); + const cookies = new Cookies(); + cookies.remove('token'); + return Object.assign({}, state, defaultState) } function updateFromToken(state, token) { const decodedToken = jwtDecode(token); localStorage.setItem('token', token); + + const cookies = new Cookies(); + cookies.set('token', token); + return Object.assign({}, state, { userLoading: false, isLogged: true, diff --git a/src/public/img/manifest.json b/src/public/manifest.json similarity index 99% rename from src/public/img/manifest.json rename to src/public/manifest.json index 675bbce..61d95af 100644 --- a/src/public/img/manifest.json +++ b/src/public/manifest.json @@ -15,4 +15,4 @@ "theme_color": "#ffffff", "background_color": "#ffffff", "display": "standalone" -} \ No newline at end of file +} diff --git a/src/routes.go b/src/routes.go index 1569f09..ab1fab3 100644 --- a/src/routes.go +++ b/src/routes.go @@ -23,6 +23,7 @@ func setupRoutes(env *web.Env) { env.Handle("/movies/search/{search}", movies.SearchMovie).WithRole(users.UserRole).Methods("GET") env.Handle("/movies/{id:tt[0-9]+}", movies.PolochonDeleteHandler).WithRole(users.UserRole).Methods("DELETE") env.Handle("/movies/{id:tt[0-9]+}/refresh", movies.RefreshMovieHandler).WithRole(users.UserRole).Methods("POST") + env.Handle("/movies/{id:tt[0-9]+}/subtitles/{lang}", movies.DownloadVVTSubtitle).WithRole(users.UserRole).Methods("GET") env.Handle("/movies/{id:tt[0-9]+}/subtitles/refresh", movies.RefreshMovieSubtitlesHandler).WithRole(users.UserRole).Methods("POST") env.Handle("/movies/refresh", extmedias.RefreshMoviesHandler).WithRole(users.AdminRole).Methods("POST") @@ -35,6 +36,7 @@ func setupRoutes(env *web.Env) { env.Handle("/shows/{id:tt[0-9]+}/refresh", shows.RefreshShowHandler).WithRole(users.UserRole).Methods("POST") env.Handle("/shows/{id:tt[0-9]+}/seasons/{season:[0-9]+}/episodes/{episode:[0-9]+}", shows.RefreshEpisodeHandler).WithRole(users.UserRole).Methods("POST") env.Handle("/shows/{id:tt[0-9]+}/seasons/{season:[0-9]+}/episodes/{episode:[0-9]+}/subtitles/refresh", shows.RefreshEpisodeSubtitlesHandler).WithRole(users.UserRole).Methods("POST") + env.Handle("/shows/{id:tt[0-9]+}/seasons/{season:[0-9]+}/episodes/{episode:[0-9]+}/subtitles/{lang}", shows.DownloadVVTSubtitle).WithRole(users.UserRole).Methods("GET") env.Handle("/shows/refresh", extmedias.RefreshShowsHandler).WithRole(users.AdminRole).Methods("POST") // Wishlist routes for shows diff --git a/yarn.lock b/yarn.lock index 2f85230..4574179 100644 --- a/yarn.lock +++ b/yarn.lock @@ -991,6 +991,10 @@ convert-source-map@^1.1.0: version "1.3.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.3.0.tgz#e9f3e9c6e2728efc2676696a70eb382f73106a67" +cookie@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" + core-js@^1.0.0: version "1.2.7" resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" @@ -1322,10 +1326,6 @@ fbjs@^0.8.4: promise "^7.1.1" ua-parser-js "^0.7.9" -fetch-jsonp@^1.0.2: - version "1.0.5" - resolved "https://registry.yarnpkg.com/fetch-jsonp/-/fetch-jsonp-1.0.5.tgz#fd1720a6876f557237013ec70ee969dd140486e4" - file-loader: version "0.9.0" resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-0.9.0.tgz#1d2daddd424ce6d1b07cfe3f79731bed3617ab42" @@ -1956,6 +1956,10 @@ is-my-json-valid@^2.12.4: jsonpointer "^4.0.0" xtend "^4.0.0" +is-node@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-node/-/is-node-1.0.2.tgz#d7d002745ef7debbb7477e988956ab0a4fccb653" + is-number@^2.0.2, is-number@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" @@ -2178,10 +2182,6 @@ load-json-file@^1.0.0: pinkie-promise "^2.0.0" strip-bom "^2.0.0" -load-script@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/load-script/-/load-script-1.0.0.tgz#0491939e0bee5643ee494a7e3da3d2bac70c6ca4" - loader-utils@0.2.x, loader-utils@^0.2.11, loader-utils@^0.2.5, loader-utils@^0.2.7, loader-utils@~0.2.2, loader-utils@~0.2.5: version "0.2.16" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.16.tgz#f08632066ed8282835dff88dfb52704765adee6d" @@ -3024,7 +3024,7 @@ qs@~6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.0.tgz#f403b264f23bc01228c74131b407f18d5ea5d442" -query-string@^4.1.0, query-string@^4.2.2, query-string@^4.2.3: +query-string@^4.1.0, query-string@^4.2.2: version "4.3.1" resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.1.tgz#54baada6713eafc92be75c47a731f2ebd09cd11d" dependencies: @@ -3096,14 +3096,6 @@ react-overlays@^0.6.10: react-prop-types "^0.4.0" warning "^3.0.0" -react-player: - version "0.14.1" - resolved "https://registry.yarnpkg.com/react-player/-/react-player-0.14.1.tgz#72c45ec13bb8445cda22d0482f26aa1a3af629f0" - dependencies: - fetch-jsonp "^1.0.2" - load-script "^1.0.0" - query-string "^4.2.3" - react-prop-types@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/react-prop-types/-/react-prop-types-0.4.0.tgz#f99b0bfb4006929c9af2051e7c1414a5c75b93d0" @@ -3714,6 +3706,14 @@ unique-stream@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unique-stream/-/unique-stream-1.0.0.tgz#d59a4a75427447d9aa6c91e70263f8d26a4b104b" +universal-cookie@^2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/universal-cookie/-/universal-cookie-2.0.7.tgz#3f42c25574196aba1ca5bbf754b2b6ba28329828" + dependencies: + cookie "^0.3.1" + is-node "^1.0.2" + object-assign "^4.1.0" + url-loader: version "0.5.7" resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-0.5.7.tgz#67e8779759f8000da74994906680c943a9b0925d"