Merge branch 'torrentSearch' into 'master'

Torrent search

See merge request !88
This commit is contained in:
Lucas 2017-08-15 12:57:09 +00:00
commit f05d0024fc
17 changed files with 422 additions and 50 deletions

View File

@ -17,6 +17,7 @@ movie:
- trakttv - trakttv
torrenters: torrenters:
- yts - yts
- thepiratebay
searchers: searchers:
- yts - yts
explorers: explorers:
@ -24,6 +25,7 @@ movie:
- trakttv - trakttv
show: show:
torrenters: torrenters:
- thepiratebay
- eztv - eztv
detailers: detailers:
- tvdb - tvdb
@ -34,6 +36,11 @@ show:
- trakttv - trakttv
- eztv - eztv
modules_params: modules_params:
- name: thepiratebay
show_users:
- EtHD
movie_users:
- YIFY
- name: trakttv - name: trakttv
client_id: my_trakttv_client_id client_id: my_trakttv_client_id
- name: tmdb - name: tmdb

View File

@ -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 // GetMovieTorrents fetch Torrents for movies
func (b *Backend) GetMovieTorrents(pmovie *polochon.Movie, log *logrus.Entry) error { func (b *Backend) GetMovieTorrents(pmovie *polochon.Movie, log *logrus.Entry) error {
movieTorrents, err := GetMovieTorrents(b.Database, pmovie.ImdbID) movieTorrents, err := GetMovieTorrents(b.Database, pmovie.ImdbID)

View File

@ -4,9 +4,12 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"net/http" "net/http"
"sort"
"strconv" "strconv"
"github.com/gorilla/mux" "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/auth"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/users" "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") 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 }

View File

@ -13,6 +13,7 @@ import (
_ "github.com/odwrtw/polochon/modules/pam" _ "github.com/odwrtw/polochon/modules/pam"
_ "github.com/odwrtw/polochon/modules/pushover" _ "github.com/odwrtw/polochon/modules/pushover"
_ "github.com/odwrtw/polochon/modules/tmdb" _ "github.com/odwrtw/polochon/modules/tmdb"
_ "github.com/odwrtw/polochon/modules/tpb"
_ "github.com/odwrtw/polochon/modules/trakttv" _ "github.com/odwrtw/polochon/modules/trakttv"
_ "github.com/odwrtw/polochon/modules/transmission" _ "github.com/odwrtw/polochon/modules/transmission"
_ "github.com/odwrtw/polochon/modules/tvdb" _ "github.com/odwrtw/polochon/modules/tvdb"

View File

@ -19,7 +19,7 @@ export function removeTorrent(id) {
"REMOVE_TORRENT", "REMOVE_TORRENT",
configureAxios().delete(`/torrents/${id}`), configureAxios().delete(`/torrents/${id}`),
[ [
fetchTorrents(), () => fetchTorrents(),
] ]
) )
} }
@ -30,3 +30,10 @@ export function fetchTorrents() {
configureAxios().get("/torrents") configureAxios().get("/torrents")
) )
} }
export function searchTorrents(url) {
return request(
"TORRENTS_SEARCH",
configureAxios().get(url)
)
}

View File

@ -44,7 +44,7 @@ function TorrentsStat(props) {
return (<span>No torrents</span>); return (<span>No torrents</span>);
} }
const percentage = Math.floor((props.data.torrentCount * 100) / props.data.count); const percentage = Math.floor((props.data.torrentCountById * 100) / props.data.count);
return ( return (
<span> <span>
{percentage}% with torrents {percentage}% with torrents

View File

@ -45,12 +45,11 @@ function MovieButtons(props) {
lastFetchUrl={props.lastFetchUrl} lastFetchUrl={props.lastFetchUrl}
/> />
{props.movie.get("torrents") !== null && <TorrentsButton
<TorrentsButton movieTitle={props.movie.get("title")}
torrents={props.movie.get("torrents")} torrents={props.movie.get("torrents")}
addTorrent={props.addTorrent} addTorrent={props.addTorrent}
/> />
}
<DownloadButton <DownloadButton
url={props.movie.get("polochon_url")} url={props.movie.get("polochon_url")}

View File

@ -13,8 +13,16 @@ export default class TorrentsButton extends React.PureComponent {
} }
render() { render() {
const entries = buildMenuItems(this.props.torrents); const entries = buildMenuItems(this.props.torrents);
const searchUrl = `#/torrents/search/movies/${encodeURI(this.props.movieTitle)}`;
return ( return (
<DropdownButton className="btn btn-default btn-sm" title="Torrents" id="download-torrents-button" dropup> <DropdownButton className="btn btn-default btn-sm" title="Torrents" id="download-torrents-button" dropup>
<MenuItem className="text-warning" header>Advanced</MenuItem>
<MenuItem href={searchUrl} >
<i className="fa fa-search" aria-hidden="true"></i> Search
</MenuItem>
{entries.length > 0 &&
<MenuItem divider></MenuItem>
}
{entries.map(function(e, index) { {entries.map(function(e, index) {
switch (e.type) { switch (e.type) {
case "header": case "header":
@ -41,6 +49,10 @@ export default class TorrentsButton extends React.PureComponent {
} }
function buildMenuItems(torrents) { function buildMenuItems(torrents) {
if (!torrents) {
return [];
}
const t = torrents.groupBy((el) => el.get("source")); const t = torrents.groupBy((el) => el.get("source"));
// Build the array of entries // Build the array of entries

View File

@ -64,7 +64,7 @@ export default class AppNavBar extends React.PureComponent {
<WishlistDropdown /> <WishlistDropdown />
} }
{loggedAndActivated && {loggedAndActivated &&
<Torrents torrentsCount={this.props.torrentCount} /> <TorrentsDropdown torrentsCount={this.props.torrentCount} />
} }
<UserDropdown <UserDropdown
username={this.props.username} username={this.props.username}
@ -199,20 +199,31 @@ function WishlistDropdown() {
); );
} }
function Torrents(props) { function TorrentsDropdown(props) {
const title = (<TorrentsDropdownTitle torrentsCount={props.torrentsCount} />)
return( return(
<Nav> <Nav>
<LinkContainer to="/torrents"> <NavDropdown title={title} id="navbar-wishlit-dropdown">
<NavItem> <LinkContainer to="/torrents/list">
Torrents <MenuItem>Downloads</MenuItem>
{props.torrentsCount > 0 && </LinkContainer>
<span> <LinkContainer to="/torrents/search">
&nbsp; <MenuItem>Search</MenuItem>
<span className="label label-info">{props.torrentsCount}</span> </LinkContainer>
</span> </NavDropdown>
}
</NavItem>
</LinkContainer>
</Nav> </Nav>
); );
} }
function TorrentsDropdownTitle(props) {
return (
<span>
Torrents
{props.torrentsCount > 0 &&
<span>
&nbsp; <span className="label label-info">{props.torrentsCount}</span>
</span>
}
</span>
);
}

View File

@ -12,6 +12,7 @@ import ImdbButton from "../buttons/imdb"
import RefreshIndicator from "../buttons/refresh" import RefreshIndicator from "../buttons/refresh"
import { OverlayTrigger, Tooltip } from "react-bootstrap" import { OverlayTrigger, Tooltip } from "react-bootstrap"
import { Button, Dropdown, MenuItem } from "react-bootstrap"
function mapStateToProps(state) { function mapStateToProps(state) {
return { return {
@ -39,6 +40,7 @@ class ShowDetails extends React.Component {
/> />
<SeasonsList <SeasonsList
data={this.props.show} data={this.props.show}
router={this.props.router}
addTorrent={this.props.addTorrent} addTorrent={this.props.addTorrent}
addToWishlist={this.props.addShowToWishlist} addToWishlist={this.props.addShowToWishlist}
getEpisodeDetails={this.props.getEpisodeDetails} getEpisodeDetails={this.props.getEpisodeDetails}
@ -112,6 +114,8 @@ function SeasonsList(props){
<Season <Season
data={data} data={data}
season={season} season={season}
showName={props.data.get("title")}
router={props.router}
addTorrent={props.addTorrent} addTorrent={props.addTorrent}
addToWishlist={props.addToWishlist} addToWishlist={props.addToWishlist}
getEpisodeDetails={props.getEpisodeDetails} getEpisodeDetails={props.getEpisodeDetails}
@ -158,6 +162,8 @@ class Season extends React.Component {
<Episode <Episode
key={key} key={key}
data={episode} data={episode}
showName={this.props.showName}
router={this.props.router}
addTorrent={this.props.addTorrent} addTorrent={this.props.addTorrent}
addToWishlist={this.props.addToWishlist} addToWishlist={this.props.addToWishlist}
getEpisodeDetails={this.props.getEpisodeDetails} getEpisodeDetails={this.props.getEpisodeDetails}
@ -215,6 +221,8 @@ function Episode(props) {
xs xs
/> />
<GetDetailsButton <GetDetailsButton
showName={props.showName}
router={props.router}
data={props.data} data={props.data}
getEpisodeDetails={props.getEpisodeDetails} getEpisodeDetails={props.getEpisodeDetails}
/> />
@ -336,23 +344,40 @@ class TrackButton extends React.PureComponent {
class GetDetailsButton extends React.PureComponent { class GetDetailsButton extends React.PureComponent {
constructor(props) { constructor(props) {
super(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) { handleFetchClick() {
e.preventDefault(); if (this.props.data.get("fetching")) { return }
if (this.props.data.get("fetching")) { this.props.getEpisodeDetails(this.state.imdbId, this.state.season, this.state.episode);
return }
} handleAdvanceTorrentSearchClick() {
const imdbId = this.props.data.get("show_imdb_id"); const pad = (d) => (d < 10) ? "0" + d.toString() : d.toString();
const season = this.props.data.get("season"); const search = `${this.props.showName} S${pad(this.state.season)}E${pad(this.state.episode)}`;
const episode = this.props.data.get("episode"); const url = `/torrents/search/shows/${encodeURI(search)}`;
this.props.getEpisodeDetails(imdbId, season, episode); this.props.router.push(url);
} }
render() { render() {
const id = `${this.state.imdbId}-${this.state.season}-${this.state.episode}-refresh-dropdown`;
return ( return (
<a type="button" className="btn btn-xs btn-info" onClick={(e) => this.handleClick(e)}> <Dropdown id={id} dropup>
<RefreshIndicator refresh={this.props.data.get("fetching")} /> <Button className="btn-xs" bsStyle="info" onClick={this.handleFetchClick}>
</a> <RefreshIndicator refresh={this.props.data.get("fetching")} />
</Button>
<Dropdown.Toggle className="btn-xs" bsStyle="info"/>
<Dropdown.Menu>
<MenuItem onClick={this.handleAdvanceTorrentSearchClick}>
<span>
<i className="fa fa-magnet"></i> Advanced torrent search
</span>
</MenuItem>
</Dropdown.Menu>
</Dropdown>
); );
} }
} }

View File

@ -3,8 +3,6 @@ import { connect } from "react-redux"
import { bindActionCreators } from "redux" import { bindActionCreators } from "redux"
import { addTorrent, removeTorrent } from "../../actions/torrents" import { addTorrent, removeTorrent } from "../../actions/torrents"
import { OverlayTrigger, Tooltip } from "react-bootstrap"
function mapStateToProps(state) { function mapStateToProps(state) {
return { torrents: state.torrentStore.get("torrents") }; 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"])); this.props.removeTorrent(this.props.data.getIn(["additional_infos", "id"]));
} }
render() { render() {
const id = this.props.data.getIn(["additional_infos", "id"]);
const done = this.props.data.get("is_finished"); const done = this.props.data.get("is_finished");
var progressStyle = "progress-bar progress-bar-info active"; var progressStyle = "progress-bar progress-bar-info active";
if (done) { if (done) {
@ -128,8 +125,6 @@ class Torrent extends React.PureComponent {
percentDone = Number(percentDone).toFixed(1) + "%"; percentDone = Number(percentDone).toFixed(1) + "%";
} }
const tooltip = (<Tooltip id={id}>Remove this torrent</Tooltip>);
// Pretty sizes // Pretty sizes
const downloadedSize = prettySize(this.props.data.get("downloaded_size")); const downloadedSize = prettySize(this.props.data.get("downloaded_size"));
const totalSize = prettySize(this.props.data.get("total_size")); const totalSize = prettySize(this.props.data.get("total_size"));
@ -138,11 +133,7 @@ class Torrent extends React.PureComponent {
<div className="panel panel-default"> <div className="panel panel-default">
<div className="panel-heading"> <div className="panel-heading">
{this.props.data.get("name")} {this.props.data.get("name")}
<span className="clickable pull-right" onClick={this.handleClick}> <span className="fa fa-trash clickable pull-right" onClick={this.handleClick}></span>
<OverlayTrigger placement="top" overlay={tooltip}>
<span className="fa fa-trash"></span>
</OverlayTrigger>
</span>
</div> </div>
<div className="panel-body"> <div className="panel-body">
{started && {started &&

View File

@ -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 (
<div>
<div className="col-xs-12">
<form className="form-horizontal" onSubmit={(e) => e.preventDefault()}>
<div className="form-group">
<input
type="text"
className="form-control"
placeholder="Search torrents"
value={this.state.search}
onChange={this.handleSearchInput}
ref="search"
/>
</div>
</form>
</div>
<div className="row">
<SearchButton
text="Search movies"
type="movies"
typeFromURL={typeFromURL}
handleClick={() => this.handleClick("movies")}
/>
<SearchButton
text="Search shows"
type="shows"
typeFromURL={typeFromURL}
handleClick={() => this.handleClick("shows")}
/>
</div>
<hr />
<div className="row">
<TorrentList
searching={this.props.searching}
results={this.props.results}
addTorrent={this.props.addTorrent}
searchFromURL={searchFromURL}
/>
</div>
</div>
);
}
}
function SearchButton(props) {
const color = (props.type === props.typeFromURL) ? "primary" : "default";
return (
<div className="col-xs-6">
<button
className={`btn btn-${color} full-width`}
type="button"
onClick={props.handleClick}
>
<i className="fa fa-search" aria-hidden="true"></i> {props.text}
</button>
</div>
);
}
function TorrentList(props) {
if (props.searching) {
return (<Loader />);
}
if (props.searchFromURL === "") {
return null;
}
if (props.results.size === 0) {
return (
<div className="col-xs-12">
<div className="well well-lg">
<h2>No results</h2>
</div>
</div>
);
}
return (
<div className="col-xs-12">
{props.results.map(function(el, index) {
return (
<Torrent
key={index}
data={el}
addTorrent={props.addTorrent}
/>);
})}
</div>
);
}
function Torrent(props) {
return (
<div className="row">
<div className="col-xs-12">
<table className="table responsive table-align-middle torrent-search-result">
<tbody>
<tr>
<td rowSpan="2" className="col-xs-1 torrent-small-width">
<h4>
<TorrentHealth
url={props.data.get("url")}
seeders={props.data.get("seeders")}
leechers={props.data.get("leechers")}
/>
</h4>
</td>
<td colSpan="4" className="col-xs-9 title">
<span className="torrent-title">{props.data.get("name")}</span>
</td>
<td rowSpan="2" className="col-xs-1 torrent-small-width">
<h4 className="pull-right clickable" onClick={() => props.addTorrent(props.data.get("url"))}>
<i className="fa fa-cloud-download" aria-hidden="true"></i>
</h4>
</td>
</tr>
<tr>
<td className="col-xs-1 torrent-label">
<span className="label label-warning">{props.data.get("quality")}</span>
</td>
<td className="col-xs-1 torrent-label">
<span className="label label-success">{props.data.get("source")}</span>
</td>
<td className="col-xs-1 torrent-label">
<span className="label label-info">{props.data.get("upload_user")}</span>
</td>
<td className="col-xs-7 torrent-label"></td>
</tr>
</tbody>
</table>
</div>
</div>
);
}
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 = (
<Tooltip id={`tooltip-health-${props.url}`}>
<p><span className={className}>Health: {health}</span></p>
<p>Seeders: {seeders}</p>
<p>Leechers: {props.leechers}</p>
</Tooltip>
);
return (
<OverlayTrigger placement="right" overlay={tooltip}>
<span className={className}>
<i className="fa fa-circle" aria-hidden="true"></i>
</span>
</OverlayTrigger>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(TorrentSearch);

View File

@ -2,15 +2,22 @@ import { Map, List, fromJS } from "immutable"
const defaultState = Map({ const defaultState = Map({
"fetching": false, "fetching": false,
"searching": false,
"torrents": List(), "torrents": List(),
"searchResults": List(),
}); });
const handlers = { 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({ "TORRENTS_FETCH_FULFILLED": (state, action) => state.merge(fromJS({
fetching: false, fetching: false,
torrents: action.payload.response.data, 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) => export default (state = defaultState, action) =>

View File

@ -30,12 +30,12 @@ export function request(eventPrefix, promise, callbackEvents = null, mainPayload
}) })
promise promise
.then(response => { .then(response => {
if (response.data.status === "error") if (response.status === "error")
{ {
dispatch({ dispatch({
type: "ADD_ALERT_ERROR", type: "ADD_ALERT_ERROR",
payload: { payload: {
message: response.data.message, message: response.message,
main: mainPayload, main: mainPayload,
} }
}); });
@ -50,7 +50,7 @@ export function request(eventPrefix, promise, callbackEvents = null, mainPayload
}) })
if (callbackEvents) { if (callbackEvents) {
for (let event of callbackEvents) { for (let event of callbackEvents) {
if (typeof event === 'function') { if (typeof event === "function") {
event = event(); event = event();
} }
dispatch(event); dispatch(event);

View File

@ -6,9 +6,10 @@ import UserEdit from "./components/users/edit"
import UserActivation from "./components/users/activation" import UserActivation from "./components/users/activation"
import UserSignUp from "./components/users/signup" import UserSignUp from "./components/users/signup"
import TorrentList from "./components/torrents/list" import TorrentList from "./components/torrents/list"
import TorrentSearch from "./components/torrents/search"
import AdminPanel from "./components/admins/panel" import AdminPanel from "./components/admins/panel"
import { fetchTorrents } from "./actions/torrents" import { fetchTorrents, searchTorrents } from "./actions/torrents"
import { userLogout, getUserInfos } from "./actions/users" import { userLogout, getUserInfos } from "./actions/users"
import { fetchMovies, getMovieExploreOptions } from "./actions/movies" import { fetchMovies, getMovieExploreOptions } from "./actions/movies"
import { fetchShows, fetchShowDetails, getShowExploreOptions } from "./actions/shows" import { fetchShows, fetchShowDetails, getShowExploreOptions } from "./actions/shows"
@ -240,7 +241,7 @@ export default function getRoutes(App) {
}, },
}, },
{ {
path: "/torrents", path: "/torrents/list",
component: TorrentList, component: TorrentList,
onEnter: function(nextState, replace, next) { onEnter: function(nextState, replace, next) {
loginCheck(nextState, replace, next, function() { 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", path: "/admin",
component: AdminPanel, component: AdminPanel,

View File

@ -81,3 +81,25 @@ div.sweet-alert > h2 {
margin-left: 3px; margin-left: 3px;
margin-right: 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;
}

View File

@ -54,6 +54,7 @@ func setupRoutes(env *web.Env) {
env.Handle("/torrents", torrents.DownloadHandler).WithRole(users.UserRole).Methods("POST") env.Handle("/torrents", torrents.DownloadHandler).WithRole(users.UserRole).Methods("POST")
env.Handle("/torrents", torrents.ListHandler).WithRole(users.UserRole).Methods("GET") 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/{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 // Route to refresh all movies and shows
env.Handle("/refresh", extmedias.RefreshHandler).WithRole(users.AdminRole).Methods("POST") env.Handle("/refresh", extmedias.RefreshHandler).WithRole(users.AdminRole).Methods("POST")