2017-05-15 01:31:17 +02:00
package common
2017-05-26 01:48:14 +02:00
import (
"context"
"encoding/json"
"net/http"
"strconv"
2017-05-30 00:28:21 +02:00
"github.com/gorilla/mux"
elastic "gopkg.in/olivere/elastic.v5"
2017-05-26 01:48:14 +02:00
"github.com/NyaaPantsu/nyaa/config"
"github.com/NyaaPantsu/nyaa/db"
"github.com/NyaaPantsu/nyaa/model"
"github.com/NyaaPantsu/nyaa/util/log"
)
2017-05-15 01:31:17 +02:00
// TorrentParam defines all parameters that can be provided when searching for a torrent
type TorrentParam struct {
All bool // True means ignore everything but Max and Offset
Full bool // True means load all members
2017-05-26 01:48:14 +02:00
Order bool // True means ascending
2017-05-15 01:31:17 +02:00
Status Status
Sort SortMode
Category Category
Max uint32
Offset uint32
UserID uint32
TorrentID uint32
2017-05-30 14:12:42 +02:00
FromID uint32
2017-05-15 01:31:17 +02:00
NotNull string // csv
Null string // csv
NameLike string // csv
}
2017-05-30 00:28:21 +02:00
// FromRequest : parse a request in torrent param
2017-05-26 01:48:14 +02:00
// TODO Should probably return an error ?
func ( p * TorrentParam ) FromRequest ( r * http . Request ) {
var err error
nameLike := r . URL . Query ( ) . Get ( "q" )
if nameLike == "" {
nameLike = "*"
}
page := mux . Vars ( r ) [ "page" ]
pagenum , err := strconv . ParseUint ( page , 10 , 32 )
if err != nil {
pagenum = 1
}
max , err := strconv . ParseUint ( r . URL . Query ( ) . Get ( "max" ) , 10 , 32 )
if err != nil {
2017-05-31 04:21:57 +02:00
max = uint64 ( config . Conf . Navigation . TorrentsPerPage )
} else if max > uint64 ( config . Conf . Navigation . MaxTorrentsPerPage ) {
max = uint64 ( config . Conf . Navigation . MaxTorrentsPerPage )
2017-05-26 01:48:14 +02:00
}
// FIXME 0 means no userId defined
2017-05-30 00:28:21 +02:00
userID , err := strconv . ParseUint ( r . URL . Query ( ) . Get ( "userID" ) , 10 , 32 )
2017-05-26 01:48:14 +02:00
if err != nil {
2017-05-30 00:28:21 +02:00
userID = 0
}
// FIXME 0 means no userId defined
2017-05-30 14:12:42 +02:00
fromID , err := strconv . ParseUint ( r . URL . Query ( ) . Get ( "fromID" ) , 10 , 32 )
2017-05-30 00:28:21 +02:00
if err != nil {
2017-05-30 14:12:42 +02:00
fromID = 0
2017-05-26 01:48:14 +02:00
}
var status Status
status . Parse ( r . URL . Query ( ) . Get ( "s" ) )
var category Category
category . Parse ( r . URL . Query ( ) . Get ( "c" ) )
var sortMode SortMode
sortMode . Parse ( r . URL . Query ( ) . Get ( "sort" ) )
ascending := false
if r . URL . Query ( ) . Get ( "order" ) == "true" {
ascending = true
}
p . NameLike = nameLike
p . Offset = uint32 ( pagenum )
p . Max = uint32 ( max )
2017-05-30 00:28:21 +02:00
p . UserID = uint32 ( userID )
2017-05-26 01:48:14 +02:00
// TODO Use All
p . All = false
// TODO Use Full
p . Full = false
p . Order = ascending
p . Status = status
p . Sort = sortMode
p . Category = category
// FIXME 0 means no TorrentId defined
2017-05-30 14:12:42 +02:00
// Do we even need that ?
p . TorrentID = 0
// Needed to display result after a certain torrentID
p . FromID = uint32 ( fromID )
2017-05-26 01:48:14 +02:00
}
2017-05-30 00:28:21 +02:00
// ToFilterQuery : Builds a query string with for es query string query defined here
2017-05-26 01:48:14 +02:00
// https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html
func ( p * TorrentParam ) ToFilterQuery ( ) string {
// Don't set sub category unless main category is set
query := ""
if p . Category . IsMainSet ( ) {
query += "category:" + strconv . FormatInt ( int64 ( p . Category . Main ) , 10 )
if p . Category . IsSubSet ( ) {
query += " sub_category:" + strconv . FormatInt ( int64 ( p . Category . Sub ) , 10 )
}
}
if p . UserID != 0 {
2017-05-27 15:07:10 +02:00
query += " uploader_id:" + strconv . FormatInt ( int64 ( p . UserID ) , 10 )
2017-05-26 01:48:14 +02:00
}
if p . Status != ShowAll {
2017-05-30 02:15:49 +02:00
if p . Status != FilterRemakes {
query += " status:" + p . Status . ToString ( )
} else {
/ * From the old nyaa behavior , FilterRemake means everything BUT
* remakes
* /
query += " !status:" + p . Status . ToString ( )
}
2017-05-26 01:48:14 +02:00
}
2017-05-30 00:28:21 +02:00
2017-05-30 14:12:42 +02:00
if p . FromID != 0 {
query += " id:>" + strconv . FormatInt ( int64 ( p . FromID ) , 10 )
2017-05-30 00:28:21 +02:00
}
2017-05-26 01:48:14 +02:00
return query
}
2017-05-30 00:28:21 +02:00
// Find :
2017-05-26 01:48:14 +02:00
/ * Uses elasticsearch to find the torrents based on TorrentParam
* We decided to fetch only the ids from ES and then query these ids to the
* database
* /
func ( p * TorrentParam ) Find ( client * elastic . Client ) ( int64 , [ ] model . Torrent , error ) {
// TODO Why is it needed, what does it do ?
ctx := context . Background ( )
query := elastic . NewSimpleQueryStringQuery ( p . NameLike ) .
Field ( "name" ) .
2017-05-31 04:21:57 +02:00
Analyzer ( config . Conf . Search . ElasticsearchAnalyzer ) .
2017-05-26 01:48:14 +02:00
DefaultOperator ( "AND" )
fsc := elastic . NewFetchSourceContext ( true ) .
Include ( "id" )
// TODO Find a better way to keep in sync with mapping in ansible
search := client . Search ( ) .
2017-05-31 04:21:57 +02:00
Index ( config . Conf . Search . ElasticsearchIndex ) .
2017-05-26 01:48:14 +02:00
Query ( query ) .
2017-05-31 04:21:57 +02:00
Type ( config . Conf . Search . ElasticsearchType ) .
2017-05-26 01:48:14 +02:00
From ( int ( ( p . Offset - 1 ) * p . Max ) ) .
Size ( int ( p . Max ) ) .
Sort ( p . Sort . ToESField ( ) , p . Order ) .
2017-05-30 00:28:21 +02:00
Sort ( "_score" , false ) . // Don't put _score before the field sort, it messes with the sorting
2017-05-26 01:48:14 +02:00
FetchSourceContext ( fsc )
filterQueryString := p . ToFilterQuery ( )
if filterQueryString != "" {
filterQuery := elastic . NewQueryStringQuery ( filterQueryString ) .
DefaultOperator ( "AND" )
search = search . PostFilter ( filterQuery )
}
result , err := search . Do ( ctx )
if err != nil {
return 0 , nil , err
}
log . Infof ( "Query '%s' took %d milliseconds." , p . NameLike , result . TookInMillis )
log . Infof ( "Amount of results %d." , result . TotalHits ( ) )
/ * TODO Cleanup this giant mess
* The raw query is used because we need to preserve the order of the id ' s
* in the IN clause , so we can ' t just do
* select * from torrents where torrent_id IN ( list_of_ids )
* This query is said to work on postgres 9.4 +
* /
{
// Temporary struct to hold the id
// INFO We are not using Hits.Id because the id in the index might not
// correspond to the id in the database later on.
type TId struct {
2017-05-27 03:54:41 +02:00
Id uint
2017-05-26 01:48:14 +02:00
}
var tid TId
var torrents [ ] model . Torrent
if len ( result . Hits . Hits ) > 0 {
torrents = make ( [ ] model . Torrent , len ( result . Hits . Hits ) )
hits := result . Hits . Hits
// Building a string of the form {id1,id2,id3}
source , _ := hits [ 0 ] . Source . MarshalJSON ( )
json . Unmarshal ( source , & tid )
2017-05-27 03:54:41 +02:00
idsToString := "{" + strconv . FormatUint ( uint64 ( tid . Id ) , 10 )
2017-05-26 01:48:14 +02:00
for _ , t := range hits [ 1 : ] {
source , _ = t . Source . MarshalJSON ( )
json . Unmarshal ( source , & tid )
2017-05-27 03:54:41 +02:00
idsToString += "," + strconv . FormatUint ( uint64 ( tid . Id ) , 10 )
2017-05-26 01:48:14 +02:00
}
idsToString += "}"
2017-05-31 04:21:57 +02:00
db . ORM . Raw ( "SELECT * FROM " + config . Conf . Models . TorrentsTableName +
2017-05-26 01:48:14 +02:00
" JOIN unnest('" + idsToString + "'::int[]) " +
" WITH ORDINALITY t(torrent_id, ord) USING (torrent_id) ORDER BY t.ord" ) . Find ( & torrents )
}
return result . TotalHits ( ) , torrents , nil
}
}
2017-05-30 00:28:21 +02:00
// Clone : To clone a torrent params
2017-05-15 01:31:17 +02:00
func ( p * TorrentParam ) Clone ( ) TorrentParam {
return TorrentParam {
Order : p . Order ,
Status : p . Status ,
Sort : p . Sort ,
Category : p . Category ,
Max : p . Max ,
Offset : p . Offset ,
UserID : p . UserID ,
TorrentID : p . TorrentID ,
2017-05-30 14:12:42 +02:00
FromID : p . FromID ,
2017-05-15 01:31:17 +02:00
NotNull : p . NotNull ,
Null : p . Null ,
NameLike : p . NameLike ,
}
}