From 538c5224f8b3b165d0e2093fd0a870b811ff9730 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Delattre?= Date: Tue, 15 Aug 2017 12:46:07 +0200 Subject: [PATCH 1/7] Add thepiratebay as torrenter --- config.yml.exemple | 7 +++++++ src/modules.go | 1 + 2 files changed, 8 insertions(+) diff --git a/config.yml.exemple b/config.yml.exemple index 5fe090a..b6a2f7a 100644 --- a/config.yml.exemple +++ b/config.yml.exemple @@ -17,6 +17,7 @@ movie: - trakttv torrenters: - yts + - thepiratebay searchers: - yts explorers: @@ -24,6 +25,7 @@ movie: - trakttv show: torrenters: + - thepiratebay - eztv detailers: - tvdb @@ -34,6 +36,11 @@ show: - trakttv - eztv modules_params: + - name: thepiratebay + show_users: + - EtHD + movie_users: + - YIFY - name: trakttv client_id: my_trakttv_client_id - name: tmdb diff --git a/src/modules.go b/src/modules.go index ea0ae5a..caff444 100644 --- a/src/modules.go +++ b/src/modules.go @@ -13,6 +13,7 @@ import ( _ "github.com/odwrtw/polochon/modules/pam" _ "github.com/odwrtw/polochon/modules/pushover" _ "github.com/odwrtw/polochon/modules/tmdb" + _ "github.com/odwrtw/polochon/modules/tpb" _ "github.com/odwrtw/polochon/modules/trakttv" _ "github.com/odwrtw/polochon/modules/transmission" _ "github.com/odwrtw/polochon/modules/tvdb" From 0dda37de50473546dd5599fd493653c721c1226f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Delattre?= Date: Mon, 14 Aug 2017 14:46:48 +0200 Subject: [PATCH 2/7] Add torrent search in the backend --- src/internal/backend/torrenter.go | 5 +++ src/internal/torrents/handlers.go | 55 +++++++++++++++++++++++++++++++ src/routes.go | 1 + 3 files changed, 61 insertions(+) diff --git a/src/internal/backend/torrenter.go b/src/internal/backend/torrenter.go index 327882a..97f27f3 100644 --- a/src/internal/backend/torrenter.go +++ b/src/internal/backend/torrenter.go @@ -20,6 +20,11 @@ func (b *Backend) GetTorrents(media interface{}, log *logrus.Entry) error { } } +// SearchTorrents implements the polochon Torrenter interface +func (b *Backend) SearchTorrents(s string) ([]*polochon.Torrent, error) { + return nil, nil +} + // GetMovieTorrents fetch Torrents for movies func (b *Backend) GetMovieTorrents(pmovie *polochon.Movie, log *logrus.Entry) error { movieTorrents, err := GetMovieTorrents(b.Database, pmovie.ImdbID) diff --git a/src/internal/torrents/handlers.go b/src/internal/torrents/handlers.go index 8569fce..ca124a8 100644 --- a/src/internal/torrents/handlers.go +++ b/src/internal/torrents/handlers.go @@ -4,9 +4,12 @@ import ( "encoding/json" "errors" "net/http" + "sort" "strconv" "github.com/gorilla/mux" + "github.com/odwrtw/polochon/lib" + "github.com/sirupsen/logrus" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/auth" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/users" @@ -94,3 +97,55 @@ func RemoveHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error { return env.RenderOK(w, "Torrent removed") } + +// SearchHandler search for torrents +func SearchHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error { + log := env.Log.WithFields(logrus.Fields{ + "function": "torrents.SearchHandler", + }) + + vars := mux.Vars(r) + searchType := vars["type"] + searchStr := vars["search"] + + // Get the appropriate torrenters + var torrenters []polochon.Torrenter + switch searchType { + case "movies": + torrenters = env.Config.MovieTorrenters + case "shows": + torrenters = env.Config.ShowTorrenters + default: + return env.RenderError(w, errors.New("invalid search type")) + } + + log.Debugf("searching for %s torrents for the query %q", searchType, searchStr) + + // Search for torrents + results := []*polochon.Torrent{} + for _, torrenter := range torrenters { + torrents, err := torrenter.SearchTorrents(searchStr) + if err != nil { + log.Warn(err) + continue + } + + if torrents == nil { + continue + } + + results = append(results, torrents...) + } + + // Sort by seeds + sort.Sort(BySeed(results)) + + return env.RenderJSON(w, results) +} + +// BySeed is an helper to sort torrents by seeders +type BySeed []*polochon.Torrent + +func (t BySeed) Len() int { return len(t) } +func (t BySeed) Swap(i, j int) { t[i], t[j] = t[j], t[i] } +func (t BySeed) Less(i, j int) bool { return t[i].Seeders > t[j].Seeders } diff --git a/src/routes.go b/src/routes.go index 0c56a58..b2af6c0 100644 --- a/src/routes.go +++ b/src/routes.go @@ -54,6 +54,7 @@ func setupRoutes(env *web.Env) { env.Handle("/torrents", torrents.DownloadHandler).WithRole(users.UserRole).Methods("POST") env.Handle("/torrents", torrents.ListHandler).WithRole(users.UserRole).Methods("GET") env.Handle("/torrents/{id:[0-9]+}", torrents.RemoveHandler).WithRole(users.UserRole).Methods("DELETE") + env.Handle("/torrents/search/{type}/{search}", torrents.SearchHandler).WithRole(users.UserRole).Methods("GET") // Route to refresh all movies and shows env.Handle("/refresh", extmedias.RefreshHandler).WithRole(users.AdminRole).Methods("POST") From 5d3fc58176b25c87e212010432ebc167be77e31c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Delattre?= Date: Tue, 15 Aug 2017 13:04:49 +0200 Subject: [PATCH 3/7] Add torrent search in the UI --- src/public/js/actions/torrents.js | 7 + src/public/js/components/navbar.js | 37 ++-- src/public/js/components/torrents/search.js | 212 ++++++++++++++++++++ src/public/js/reducers/torrents.js | 9 +- src/public/js/routes.js | 21 +- src/public/less/app.less | 22 ++ 6 files changed, 292 insertions(+), 16 deletions(-) create mode 100644 src/public/js/components/torrents/search.js diff --git a/src/public/js/actions/torrents.js b/src/public/js/actions/torrents.js index c554d8d..f6c7f0b 100644 --- a/src/public/js/actions/torrents.js +++ b/src/public/js/actions/torrents.js @@ -30,3 +30,10 @@ export function fetchTorrents() { configureAxios().get("/torrents") ) } + +export function searchTorrents(url) { + return request( + "TORRENTS_SEARCH", + configureAxios().get(url) + ) +} diff --git a/src/public/js/components/navbar.js b/src/public/js/components/navbar.js index 5c38db6..19ae1f9 100644 --- a/src/public/js/components/navbar.js +++ b/src/public/js/components/navbar.js @@ -64,7 +64,7 @@ export default class AppNavBar extends React.PureComponent { } {loggedAndActivated && - + } ) return( ); } + +function TorrentsDropdownTitle(props) { + return ( + + Torrents + {props.torrentsCount > 0 && + +   {props.torrentsCount} + + } + + ); +} diff --git a/src/public/js/components/torrents/search.js b/src/public/js/components/torrents/search.js new file mode 100644 index 0000000..dd2a2e9 --- /dev/null +++ b/src/public/js/components/torrents/search.js @@ -0,0 +1,212 @@ +import React from "react" +import { connect } from "react-redux" +import { bindActionCreators } from "redux" +import { addTorrent, searchTorrents } from "../../actions/torrents" +import Loader from "../loader/loader" + +import { OverlayTrigger, Tooltip } from "react-bootstrap" + +function mapStateToProps(state) { + return { + searching: state.torrentStore.get("searching"), + results: state.torrentStore.get("searchResults"), + }; +} +const mapDispatchToProps = (dispatch) => + bindActionCreators({ addTorrent, searchTorrents }, dispatch) + +class TorrentSearch extends React.PureComponent { + constructor(props) { + super(props); + this.handleSearchInput = this.handleSearchInput.bind(this); + this.state = { search: (this.props.router.params.search || "") }; + } + handleSearchInput() { + this.setState({ search: this.refs.search.value }); + } + handleClick(type) { + if (this.state.search === "") { return } + const url = `/torrents/search/${type}/${encodeURI(this.state.search)}`; + this.props.router.push(url); + } + render() { + const searchFromURL = this.props.router.params.search || ""; + const typeFromURL = this.props.router.params.type || ""; + return ( +
+
+
e.preventDefault()}> +
+ +
+
+
+
+ this.handleClick("movies")} + /> + this.handleClick("shows")} + /> +
+
+
+ +
+
+ ); + } +} + + +function SearchButton(props) { + const color = (props.type === props.typeFromURL) ? "primary" : "default"; + return ( +
+ +
+ + ); +} + +function TorrentList(props) { + if (props.searching) { + return (); + } + + if (props.searchFromURL === "") { + return null; + } + + if (props.results.size === 0) { + return ( +
+
+

No results

+
+
+ ); + } + + return ( +
+ {props.results.map(function(el, index) { + return ( + ); + })} +
+ ); +} + +function Torrent(props) { + return ( +
+
+ + + + + + + + + + + + + + +
+

+ +

+
+ {props.data.get("name")} + +

props.addTorrent(props.data.get("url"))}> + +

+
+ {props.data.get("quality")} + + {props.data.get("source")} + + {props.data.get("upload_user")} +
+
+
+ ); +} + +function TorrentHealth(props) { + const seeders = props.seeders || 0; + const leechers = props.leechers || 1; + + let color; + let health; + let ratio = seeders/leechers; + + if (seeders > 20) { + health = "good"; + color = "success"; + } else { + if (ratio > 1) { + health = "medium"; + color = "warning"; + } else { + health = "bad"; + color = "danger"; + } + } + + const className = `text text-${color}`; + const tooltip = ( + +

Health: {health}

+

Seeders: {seeders}

+

Leechers: {props.leechers}

+
+ ); + + return ( + + + + + + ); +} + +export default connect(mapStateToProps, mapDispatchToProps)(TorrentSearch); diff --git a/src/public/js/reducers/torrents.js b/src/public/js/reducers/torrents.js index 6e219ec..d00f985 100644 --- a/src/public/js/reducers/torrents.js +++ b/src/public/js/reducers/torrents.js @@ -2,15 +2,22 @@ import { Map, List, fromJS } from "immutable" const defaultState = Map({ "fetching": false, + "searching": false, "torrents": List(), + "searchResults": List(), }); const handlers = { - "TORRENTS_FETCH_PENDING": state => state.set("fetching", false), + "TORRENTS_FETCH_PENDING": state => state.set("fetching", true), "TORRENTS_FETCH_FULFILLED": (state, action) => state.merge(fromJS({ fetching: false, torrents: action.payload.response.data, })), + "TORRENTS_SEARCH_PENDING": state => state.set("searching", true), + "TORRENTS_SEARCH_FULFILLED": (state, action) => state.merge(fromJS({ + searching: false, + searchResults: action.payload.response.data, + })), } export default (state = defaultState, action) => diff --git a/src/public/js/routes.js b/src/public/js/routes.js index 3f1fcd1..20a0b91 100644 --- a/src/public/js/routes.js +++ b/src/public/js/routes.js @@ -6,9 +6,10 @@ import UserEdit from "./components/users/edit" import UserActivation from "./components/users/activation" import UserSignUp from "./components/users/signup" import TorrentList from "./components/torrents/list" +import TorrentSearch from "./components/torrents/search" import AdminPanel from "./components/admins/panel" -import { fetchTorrents } from "./actions/torrents" +import { fetchTorrents, searchTorrents } from "./actions/torrents" import { userLogout, getUserInfos } from "./actions/users" import { fetchMovies, getMovieExploreOptions } from "./actions/movies" import { fetchShows, fetchShowDetails, getShowExploreOptions } from "./actions/shows" @@ -240,7 +241,7 @@ export default function getRoutes(App) { }, }, { - path: "/torrents", + path: "/torrents/list", component: TorrentList, onEnter: function(nextState, replace, next) { loginCheck(nextState, replace, next, function() { @@ -248,6 +249,22 @@ export default function getRoutes(App) { }); }, }, + { + path: "/torrents/search", + component: TorrentSearch, + onEnter: function(nextState, replace, next) { + loginCheck(nextState, replace, next); + }, + }, + { + path: "/torrents/search/:type/:search", + component: TorrentSearch, + onEnter: function(nextState, replace, next) { + loginCheck(nextState, replace, next, function() { + store.dispatch(searchTorrents(`/torrents/search/${nextState.params.type}/${encodeURI(nextState.params.search)}`)); + }); + }, + }, { path: "/admin", component: AdminPanel, diff --git a/src/public/less/app.less b/src/public/less/app.less index c6fbf3f..0fecbf3 100644 --- a/src/public/less/app.less +++ b/src/public/less/app.less @@ -81,3 +81,25 @@ div.sweet-alert > h2 { margin-left: 3px; margin-right: 3px; } + +button.full-width { + width: 100%; +} + +table.torrent-search-result { + font-size: 15px; + margin-bottom: 5px; + border-bottom: 2px solid @gray-light; + td.torrent-small-width { + width: 1%; + } + td.torrent-label { + padding-top: 0px; + padding-bottom: 10px; + } + +} + +table.table-align-middle > tbody > tr > td { + vertical-align: middle; +} From 33f0036e110f2bf3c4109e38c8a11ee6356d616c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Delattre?= Date: Tue, 15 Aug 2017 14:01:08 +0200 Subject: [PATCH 4/7] Add a link to search for torrents for an episode --- src/public/js/components/shows/details.js | 51 +++++++++++++++++------ 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/src/public/js/components/shows/details.js b/src/public/js/components/shows/details.js index 389c2d9..6f0207b 100644 --- a/src/public/js/components/shows/details.js +++ b/src/public/js/components/shows/details.js @@ -12,6 +12,7 @@ import ImdbButton from "../buttons/imdb" import RefreshIndicator from "../buttons/refresh" import { OverlayTrigger, Tooltip } from "react-bootstrap" +import { Button, Dropdown, MenuItem } from "react-bootstrap" function mapStateToProps(state) { return { @@ -39,6 +40,7 @@ class ShowDetails extends React.Component { /> @@ -336,23 +344,40 @@ class TrackButton extends React.PureComponent { class GetDetailsButton extends React.PureComponent { constructor(props) { super(props); - this.handleClick = this.handleClick.bind(this); + this.handleFetchClick = this.handleFetchClick.bind(this); + this.handleAdvanceTorrentSearchClick = this.handleAdvanceTorrentSearchClick.bind(this); + this.state = { + imdbId: this.props.data.get("show_imdb_id"), + season: this.props.data.get("season"), + episode: this.props.data.get("episode"), + }; } - handleClick(e) { - e.preventDefault(); - if (this.props.data.get("fetching")) { - return - } - 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.getEpisodeDetails(imdbId, season, episode); + handleFetchClick() { + if (this.props.data.get("fetching")) { return } + this.props.getEpisodeDetails(this.state.imdbId, this.state.season, this.state.episode); + } + handleAdvanceTorrentSearchClick() { + const pad = (d) => (d < 10) ? "0" + d.toString() : d.toString(); + const search = `${this.props.showName} S${pad(this.state.season)}E${pad(this.state.episode)}`; + const url = `/torrents/search/shows/${encodeURI(search)}`; + this.props.router.push(url); } render() { + const id = `${this.state.imdbId}-${this.state.season}-${this.state.episode}-refresh-dropdown`; return ( - this.handleClick(e)}> - - + + + + + + + Advanced torrent search + + + + ); } } From 380c9c453a0160c949f4ea4e6f9cbeb2925364d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Delattre?= Date: Tue, 15 Aug 2017 14:19:09 +0200 Subject: [PATCH 5/7] Add link to search for torrents for a movie --- src/public/js/components/movies/list.js | 11 +++++------ src/public/js/components/movies/torrents.js | 12 ++++++++++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/public/js/components/movies/list.js b/src/public/js/components/movies/list.js index 8252a58..50fe53e 100644 --- a/src/public/js/components/movies/list.js +++ b/src/public/js/components/movies/list.js @@ -45,12 +45,11 @@ function MovieButtons(props) { lastFetchUrl={props.lastFetchUrl} /> - {props.movie.get("torrents") !== null && - - } + + Advanced + + Search + + {entries.length > 0 && + + } {entries.map(function(e, index) { switch (e.type) { case "header": @@ -41,6 +49,10 @@ export default class TorrentsButton extends React.PureComponent { } function buildMenuItems(torrents) { + if (!torrents) { + return []; + } + const t = torrents.groupBy((el) => el.get("source")); // Build the array of entries From 2f31f81d267fdf43646f4ed92dd7d7d311e03c43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Delattre?= Date: Tue, 15 Aug 2017 14:37:26 +0200 Subject: [PATCH 6/7] Fix remove torrent button --- src/public/js/actions/torrents.js | 2 +- src/public/js/components/torrents/list.js | 11 +---------- src/public/js/requests.js | 6 +++--- 3 files changed, 5 insertions(+), 14 deletions(-) diff --git a/src/public/js/actions/torrents.js b/src/public/js/actions/torrents.js index f6c7f0b..8af86d0 100644 --- a/src/public/js/actions/torrents.js +++ b/src/public/js/actions/torrents.js @@ -19,7 +19,7 @@ export function removeTorrent(id) { "REMOVE_TORRENT", configureAxios().delete(`/torrents/${id}`), [ - fetchTorrents(), + () => fetchTorrents(), ] ) } diff --git a/src/public/js/components/torrents/list.js b/src/public/js/components/torrents/list.js index 42e6ed1..856edbc 100644 --- a/src/public/js/components/torrents/list.js +++ b/src/public/js/components/torrents/list.js @@ -3,8 +3,6 @@ import { connect } from "react-redux" import { bindActionCreators } from "redux" import { addTorrent, removeTorrent } from "../../actions/torrents" -import { OverlayTrigger, Tooltip } from "react-bootstrap" - function mapStateToProps(state) { return { torrents: state.torrentStore.get("torrents") }; } @@ -116,7 +114,6 @@ class Torrent extends React.PureComponent { this.props.removeTorrent(this.props.data.getIn(["additional_infos", "id"])); } render() { - const id = this.props.data.getIn(["additional_infos", "id"]); const done = this.props.data.get("is_finished"); var progressStyle = "progress-bar progress-bar-info active"; if (done) { @@ -128,8 +125,6 @@ class Torrent extends React.PureComponent { percentDone = Number(percentDone).toFixed(1) + "%"; } - const tooltip = (Remove this torrent); - // Pretty sizes const downloadedSize = prettySize(this.props.data.get("downloaded_size")); const totalSize = prettySize(this.props.data.get("total_size")); @@ -138,11 +133,7 @@ class Torrent extends React.PureComponent {
{this.props.data.get("name")} - - - - - +
{started && diff --git a/src/public/js/requests.js b/src/public/js/requests.js index c47221f..0b6a8db 100644 --- a/src/public/js/requests.js +++ b/src/public/js/requests.js @@ -30,12 +30,12 @@ export function request(eventPrefix, promise, callbackEvents = null, mainPayload }) promise .then(response => { - if (response.data.status === "error") + if (response.status === "error") { dispatch({ type: "ADD_ALERT_ERROR", payload: { - message: response.data.message, + message: response.message, main: mainPayload, } }); @@ -50,7 +50,7 @@ export function request(eventPrefix, promise, callbackEvents = null, mainPayload }) if (callbackEvents) { for (let event of callbackEvents) { - if (typeof event === 'function') { + if (typeof event === "function") { event = event(); } dispatch(event); From 555e6c6a352c5eff0f9c2a1d8a29b7fa8fb9261e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Delattre?= Date: Tue, 15 Aug 2017 14:39:19 +0200 Subject: [PATCH 7/7] Fix admin panel torrent stat --- src/public/js/components/admins/stats.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/public/js/components/admins/stats.js b/src/public/js/components/admins/stats.js index 4b0b145..612dccf 100644 --- a/src/public/js/components/admins/stats.js +++ b/src/public/js/components/admins/stats.js @@ -44,7 +44,7 @@ function TorrentsStat(props) { return (No torrents); } - const percentage = Math.floor((props.data.torrentCount * 100) / props.data.count); + const percentage = Math.floor((props.data.torrentCountById * 100) / props.data.count); return ( {percentage}% with torrents