Albirew/nyaa-pantsu
Archivé
1
0
Bifurcation 0

Merge branch 'master' of github.com:ewhal/nyaa into api

Cette révision appartient à :
ayame-git 2017-05-12 00:59:35 +03:00
révision af2c47c2f2
46 fichiers modifiés avec 1023 ajouts et 507 suppressions

Voir le fichier

@ -7,9 +7,9 @@ before_install:
script:
# Downloads deps automatically. No need to add manually.
- go list -f '{{.Deps}}' | tr "[" " " | tr "]" " " | xargs go list -e -f '{{if not .Standard}}{{.ImportPath}}{{end}}' | grep -v 'github.com/ewhal/nyaa' | xargs go get -v
- go get
- go get
- go build
- go vet
- go vet
- go test -v ./...
before_deploy:
- ./package.sh

Voir le fichier

@ -64,17 +64,18 @@ Access the website by going to [localhost:9999](http://localhost:9999).
> nyaa_psql.backup.
## TODO
* improve scraping
* fix up cache bug
* import sukebei torrents into db/work on sukebei
* sukebei
* get sukebei_torrents table working
* add config option for sukebei or maybe make things all in one
* sukebei categories and category images
* Get code up to standard of go lint recommendations
* Write tests
* fix sukebei categories
* Daily DB dumps
* Site theme
* Site theme
* original nyaa theme
* API improvement
* Scraping of fan subbing RSS feeds
* Scraping of fan subbing RSS feeds(WIP)
# LICENSE
This project is licensed under the MIT License - see the LICENSE.md file for details

139
cache/cache.go externe
Voir le fichier

@ -1,132 +1,37 @@
package cache
import (
"container/list"
"sync"
"time"
"github.com/ewhal/nyaa/cache/memcache"
"github.com/ewhal/nyaa/cache/native"
"github.com/ewhal/nyaa/cache/nop"
"github.com/ewhal/nyaa/common"
"github.com/ewhal/nyaa/config"
"github.com/ewhal/nyaa/model"
"errors"
)
const expiryTime = time.Minute
var (
cache = make(map[common.SearchParam]*list.Element, 10)
ll = list.New()
totalUsed int
mu sync.Mutex
// Size sets the maximum size of the cache before evicting unread data in MB
Size float64 = 1 << 10
)
// Key stores the ID of either a thread or board page
type Key struct {
LastN uint8
Board string
ID uint64
// Cache defines interface for caching search results
type Cache interface {
Get(key common.SearchParam, r func() ([]model.Torrent, int, error)) ([]model.Torrent, int, error)
ClearAll()
}
// Single cache entry
type store struct {
sync.Mutex // Controls general access to the contents of the struct
lastFetched time.Time
key common.SearchParam
data []model.Torrent
count, size int
}
var ErrInvalidCacheDialect = errors.New("invalid cache dialect")
// Check the cache for and existing record. If miss, run fn to retrieve fresh
// values.
func Get(key common.SearchParam, fn func() ([]model.Torrent, int, error)) (
data []model.Torrent, count int, err error,
) {
s := getStore(key)
// Impl cache implementation instance
var Impl Cache
// Also keeps multiple requesters from simultaneously requesting the same
// data
s.Lock()
defer s.Unlock()
if s.isFresh() {
return s.data, s.count, nil
}
data, count, err = fn()
if err != nil {
func Configure(conf *config.CacheConfig) (err error) {
switch conf.Dialect {
case "native":
Impl = native.New(conf.Size)
return
case "memcache":
Impl = memcache.New()
return
default:
Impl = nop.New()
}
s.update(data, count)
return
}
// Retrieve a store from the cache or create a new one
func getStore(k common.SearchParam) (s *store) {
mu.Lock()
defer mu.Unlock()
el := cache[k]
if el == nil {
s = &store{key: k}
cache[k] = ll.PushFront(s)
} else {
ll.MoveToFront(el)
s = el.Value.(*store)
}
return s
}
// Clear the cache. Only used for testing.
func Clear() {
mu.Lock()
defer mu.Unlock()
ll = list.New()
cache = make(map[common.SearchParam]*list.Element, 10)
}
// Update the total used memory counter and evict, if over limit
func updateUsedSize(delta int) {
mu.Lock()
defer mu.Unlock()
totalUsed += delta
for totalUsed > int(Size)<<20 {
e := ll.Back()
if e == nil {
break
}
s := ll.Remove(e).(*store)
delete(cache, s.key)
totalUsed -= s.size
}
}
// Return, if the data can still be considered fresh, without querying the DB
func (s *store) isFresh() bool {
if s.lastFetched.IsZero() { // New store
return false
}
return s.lastFetched.Add(expiryTime).After(time.Now())
}
// Stores the new values of s. Calculates and stores the new size. Passes the
// delta to the central cache to fire eviction checks.
func (s *store) update(data []model.Torrent, count int) {
newSize := 0
for _, d := range data {
newSize += d.Size()
}
s.data = data
s.count = count
delta := newSize - s.size
s.size = newSize
s.lastFetched = time.Now()
// Technically it is possible to update the size even when the store is
// already evicted, but that should never happen, unless you have a very
// small cache, very large stored datasets and a lot of traffic.
updateUsedSize(delta)
}

21
cache/memcache/memcache.go externe Fichier normal
Voir le fichier

@ -0,0 +1,21 @@
package memcache
import (
"github.com/ewhal/nyaa/common"
"github.com/ewhal/nyaa/model"
)
type Memcache struct {
}
func (c *Memcache) Get(key common.SearchParam, r func() ([]model.Torrent, int, error)) (torrents []model.Torrent, num int, err error) {
return
}
func (c *Memcache) ClearAll() {
}
func New() *Memcache {
return &Memcache{}
}

143
cache/native/native.go externe Fichier normal
Voir le fichier

@ -0,0 +1,143 @@
package native
import (
"container/list"
"sync"
"time"
"github.com/ewhal/nyaa/common"
"github.com/ewhal/nyaa/model"
)
const expiryTime = time.Minute
// NativeCache implements cache.Cache
type NativeCache struct {
cache map[common.SearchParam]*list.Element
ll *list.List
totalUsed int
mu sync.Mutex
// Size sets the maximum size of the cache before evicting unread data in MB
Size float64
}
// New Creates New Native Cache instance
func New(sz float64) *NativeCache {
return &NativeCache{
cache: make(map[common.SearchParam]*list.Element, 10),
Size: sz,
ll: list.New(),
}
}
// Key stores the ID of either a thread or board page
type Key struct {
LastN uint8
Board string
ID uint64
}
// Single cache entry
type store struct {
sync.Mutex // Controls general access to the contents of the struct
lastFetched time.Time
key common.SearchParam
data []model.Torrent
count, size int
n *NativeCache
}
// Check the cache for and existing record. If miss, run fn to retrieve fresh
// values.
func (n *NativeCache) Get(key common.SearchParam, fn func() ([]model.Torrent, int, error)) (
data []model.Torrent, count int, err error,
) {
s := n.getStore(key)
// Also keeps multiple requesters from simultaneously requesting the same
// data
s.Lock()
defer s.Unlock()
if s.isFresh() {
return s.data, s.count, nil
}
data, count, err = fn()
if err != nil {
return
}
s.update(data, count)
return
}
// Retrieve a store from the cache or create a new one
func (n *NativeCache) getStore(k common.SearchParam) (s *store) {
n.mu.Lock()
defer n.mu.Unlock()
el := n.cache[k]
if el == nil {
s = &store{key: k, n: n}
n.cache[k] = n.ll.PushFront(s)
} else {
n.ll.MoveToFront(el)
s = el.Value.(*store)
}
return s
}
// Clear the cache. Only used for testing.
func (n *NativeCache) ClearAll() {
n.mu.Lock()
defer n.mu.Unlock()
n.ll = list.New()
n.cache = make(map[common.SearchParam]*list.Element, 10)
}
// Update the total used memory counter and evict, if over limit
func (n *NativeCache) updateUsedSize(delta int) {
n.mu.Lock()
defer n.mu.Unlock()
n.totalUsed += delta
for n.totalUsed > int(n.Size)<<20 {
e := n.ll.Back()
if e == nil {
break
}
s := n.ll.Remove(e).(*store)
delete(n.cache, s.key)
n.totalUsed -= s.size
}
}
// Return, if the data can still be considered fresh, without querying the DB
func (s *store) isFresh() bool {
if s.lastFetched.IsZero() { // New store
return false
}
return s.lastFetched.Add(expiryTime).After(time.Now())
}
// Stores the new values of s. Calculates and stores the new size. Passes the
// delta to the central cache to fire eviction checks.
func (s *store) update(data []model.Torrent, count int) {
newSize := 0
for _, d := range data {
newSize += d.Size()
}
s.data = data
s.count = count
delta := newSize - s.size
s.size = newSize
s.lastFetched = time.Now()
// Technically it is possible to update the size even when the store is
// already evicted, but that should never happen, unless you have a very
// small cache, very large stored datasets and a lot of traffic.
s.n.updateUsedSize(delta)
}

22
cache/nop/nop.go externe Fichier normal
Voir le fichier

@ -0,0 +1,22 @@
package nop
import (
"github.com/ewhal/nyaa/common"
"github.com/ewhal/nyaa/model"
)
type NopCache struct {
}
func (c *NopCache) Get(key common.SearchParam, fn func() ([]model.Torrent, int, error)) ([]model.Torrent, int, error) {
return fn()
}
func (c *NopCache) ClearAll() {
}
// New creates a new Cache that does NOTHING :D
func New() *NopCache {
return &NopCache{}
}

Voir le fichier

@ -19,6 +19,9 @@ const (
Date
Downloads
Size
Seeders
Leechers
Completed
)
type Category struct {
@ -44,5 +47,6 @@ type SearchParam struct {
Page int
UserID uint
Max uint
NotNull string
Query string
}

14
config/cache.go Fichier normal
Voir le fichier

@ -0,0 +1,14 @@
package config
// CacheConfig is config struct for caching strategy
type CacheConfig struct {
Dialect string
URL string
Size float64
}
const DefaultCacheSize = 1 << 10
var DefaultCacheConfig = CacheConfig{
Dialect: "nop",
}

Voir le fichier

@ -22,13 +22,18 @@ type Config struct {
// DBParams will be directly passed to Gorm, and its internal
// structure depends on the dialect for each db type
DBParams string `json:"db_params"`
DBLogMode string `json:"db_logmode"`
// tracker scraper config (required)
Scrape ScraperConfig `json:"scraper"`
// cache config
Cache CacheConfig `json:"cache"`
// search config
Search SearchConfig `json:"search"`
// optional i2p configuration
I2P *I2PConfig `json:"i2p"`
}
var Defaults = Config{"localhost", 9999, "sqlite3", "./nyaa.db?cache_size=50", DefaultScraperConfig, nil}
var Defaults = Config{"localhost", 9999, "sqlite3", "./nyaa.db?cache_size=50", "default", DefaultScraperConfig, DefaultCacheConfig, DefaultSearchConfig, nil}
var allowedDatabaseTypes = map[string]bool{
"sqlite3": true,
@ -37,13 +42,21 @@ var allowedDatabaseTypes = map[string]bool{
"mssql": true,
}
var allowedDBLogModes = map[string]bool{
"default": true, // errors only
"detailed": true,
"silent": true,
}
func New() *Config {
var config Config
config.Host = Defaults.Host
config.Port = Defaults.Port
config.DBType = Defaults.DBType
config.DBParams = Defaults.DBParams
config.DBLogMode = Defaults.DBLogMode
config.Scrape = Defaults.Scrape
config.Cache = Defaults.Cache
return &config
}
@ -55,6 +68,7 @@ func (config *Config) BindFlags() func() error {
host := flag.String("host", Defaults.Host, "binding address of the server")
port := flag.Int("port", Defaults.Port, "port of the server")
dbParams := flag.String("dbparams", Defaults.DBParams, "parameters to open the database (see Gorm's doc)")
dbLogMode := flag.String("dblogmode", Defaults.DBLogMode, "database log verbosity (errors only by default)")
return func() error {
// You can override fields in the config file with flags.
@ -65,6 +79,10 @@ func (config *Config) BindFlags() func() error {
if err != nil {
return err
}
err = config.SetDBLogMode(*dbLogMode)
if err != nil {
return err
}
err = config.HandleConfFileFlag(*confFile)
return err
}
@ -93,6 +111,14 @@ func (config *Config) SetDBType(db_type string) error {
return nil
}
func (config *Config) SetDBLogMode(db_logmode string) error {
if !allowedDBLogModes[db_logmode] {
return fmt.Errorf("unknown database log mode '%s'", db_logmode)
}
config.DBLogMode = db_logmode
return nil
}
func (config *Config) Read(input io.Reader) error {
return json.NewDecoder(input).Decode(config)
}

6
config/search.go Fichier normal
Voir le fichier

@ -0,0 +1,6 @@
package config
type SearchConfig struct {
}
var DefaultSearchConfig = SearchConfig{}

Voir le fichier

@ -10,6 +10,5 @@ var Trackers = []string{
"udp://explodie.org:6969",
"udp://tracker.opentrackr.org:1337",
"udp://tracker.internetwarriors.net:1337/announce",
"udp://eddie4.nl:6969/announce",
"http://mgtracker.org:6969/announce",
"http://tracker.baka-sub.cf/announce"}

Voir le fichier

@ -9,16 +9,28 @@ import (
_ "github.com/jinzhu/gorm/dialects/sqlite"
)
type Logger interface {
Print(v ...interface{})
}
// use the default gorm logger that prints to stdout
var DefaultLogger Logger = nil
var ORM *gorm.DB
var IsSqlite bool
// GormInit init gorm ORM.
func GormInit(conf *config.Config) (*gorm.DB, error) {
func GormInit(conf *config.Config, logger Logger) (*gorm.DB, error) {
db, openErr := gorm.Open(conf.DBType, conf.DBParams)
if openErr != nil {
log.CheckError(openErr)
return nil, openErr
}
IsSqlite = conf.DBType == "sqlite"
connectionErr := db.DB().Ping()
if connectionErr != nil {
log.CheckError(connectionErr)
@ -31,9 +43,29 @@ func GormInit(conf *config.Config) (*gorm.DB, error) {
db.LogMode(true)
}
switch conf.DBLogMode {
case "detailed":
db.LogMode(true)
case "silent":
db.LogMode(false)
}
if logger != nil {
db.SetLogger(logger)
}
db.AutoMigrate(&model.User{}, &model.UserFollows{}, &model.UserUploadsOld{})
if db.Error != nil {
return db, db.Error
}
db.AutoMigrate(&model.Torrent{}, &model.TorrentReport{})
if db.Error != nil {
return db, db.Error
}
db.AutoMigrate(&model.Comment{}, &model.OldComment{})
if db.Error != nil {
return db, db.Error
}
return db, nil
}

Voir le fichier

@ -1,22 +1,81 @@
package db
import (
"fmt"
"testing"
"github.com/azhao12345/gorm"
"github.com/ewhal/nyaa/config"
)
func TestGormInit(t *testing.T) {
type errorLogger struct {
t *testing.T
}
func (logger *errorLogger) Print(values ...interface{}) {
if len(values) > 1 {
message := gorm.LogFormatter(values...)
level := values[0]
if level == "log" {
logger.t.Error(message...)
}
fmt.Println(message...)
}
}
func TestGormInitSqlite(t *testing.T) {
conf := config.New()
conf.DBType = "sqlite3"
conf.DBParams = ":memory:?cache=shared&mode=memory"
conf.DBLogMode = "detailed"
db, err := GormInit(conf)
db, err := GormInit(conf, &errorLogger{t})
if err != nil {
t.Errorf("failed to initialize database: %v", err)
return
}
if db == nil {
return
}
err = db.Close()
if err != nil {
t.Errorf("failed to close database: %v", err)
}
}
// This test requires a running postgres instance. To run it in CI build add these settings in the .travis.yml
// services:
// - postgresql
// before_script:
// - psql -c "CREATE DATABASE nyaapantsu;" -U postgres
// - psql -c "CREATE USER nyaapantsu WITH PASSWORD 'nyaapantsu';" -U postgres
//
// Then enable the test by setting this variable to "true" via ldflags:
// go test ./... -v -ldflags="-X github.com/ewhal/nyaa/db.testPostgres=true"
var testPostgres = "false"
func TestGormInitPostgres(t *testing.T) {
if testPostgres != "true" {
t.Skip("skip", testPostgres)
}
conf := config.New()
conf.DBType = "postgres"
conf.DBParams = "host=localhost user=nyaapantsu dbname=nyaapantsu sslmode=disable password=nyaapantsu"
conf.DBLogMode = "detailed"
db, err := GormInit(conf, &errorLogger{t})
if err != nil {
t.Errorf("failed to initialize database: %v", err)
}
if db == nil {
return
}
err = db.Close()
if err != nil {
t.Errorf("failed to close database: %v", err)

15
main.go
Voir le fichier

@ -16,6 +16,7 @@ import (
"github.com/ewhal/nyaa/router"
"github.com/ewhal/nyaa/service/scraper"
"github.com/ewhal/nyaa/util/log"
"github.com/ewhal/nyaa/util/search"
"github.com/ewhal/nyaa/util/signals"
"github.com/nicksnyder/go-i18n/i18n"
)
@ -84,6 +85,7 @@ func RunScraper(conf *config.Config) {
signals.RegisterCloser(scraper)
// run udp scraper worker
for workers > 0 {
log.Infof("starting up worker %d", workers)
go scraper.RunWorker(pc)
workers--
}
@ -97,7 +99,8 @@ func main() {
processFlags := conf.BindFlags()
defaults := flag.Bool("print-defaults", false, "print the default configuration file on stdout")
mode := flag.String("mode", "webapp", "which mode to run daemon in, either webapp or scraper")
flag.Float64Var(&cache.Size, "c", cache.Size, "size of the search cache in MB")
flag.Float64Var(&conf.Cache.Size, "c", config.DefaultCacheSize, "size of the search cache in MB")
flag.Parse()
if *defaults {
stdout := bufio.NewWriter(os.Stdout)
@ -115,11 +118,19 @@ func main() {
if err != nil {
log.CheckError(err)
}
db.ORM, err = db.GormInit(conf)
db.ORM, err = db.GormInit(conf, db.DefaultLogger)
if err != nil {
log.Fatal(err.Error())
}
initI18N()
err = cache.Configure(&conf.Cache)
if err != nil {
log.Fatal(err.Error())
}
err = search.Configure(&conf.Search)
if err != nil {
log.Fatal(err.Error())
}
go signals.Handle()
if len(config.TorrentFileStorage) > 0 {
err := os.MkdirAll(config.TorrentFileStorage, 0700)

Voir le fichier

@ -12,8 +12,9 @@ type User struct {
Status int `gorm:"column:status"`
CreatedAt time.Time `gorm:"column:created_at"`
UpdatedAt time.Time `gorm:"column:updated_at"`
Token string `gorm:"column:api_token"`
TokenExpiration time.Time `gorm:"column:api_token_expiry"`
// Currently unused (auth is stateless now)
/*Token string `gorm:"column:api_token"`
TokenExpiration time.Time `gorm:"column:api_token_expiry"`*/
Language string `gorm:"column:language"`
// TODO: move this to PublicUser
@ -42,7 +43,7 @@ func (u User) Size() (s int) {
4*3 + //time.Time
3*2 + // arrays
// string arrays
len(u.Username) + len(u.Password) + len(u.Email) + len(u.Token) + len(u.MD5) + len(u.Language)
len(u.Username) + len(u.Password) + len(u.Email) + len(u.MD5) + len(u.Language)
s *= 8
// Ignoring foreign key users. Fuck them.

Voir le fichier

@ -143,4 +143,9 @@ a:hover {
}
.modal-content .close {
color: #fff;
}
}
.text-error {
background: #29363d;
color: #cf9fff;
}

Voir le fichier

@ -383,3 +383,8 @@ footer {
font-size: smaller;
width: auto; /* Undo bootstrap's fixed width */
}
.text-error {
background: white;
color: #cf9fff;
}

Voir le fichier

@ -1,15 +1,10 @@
// Night mode
var night = localStorage.getItem("night");
if (night == "true") {
$("head").append('<link id="style-dark" rel="stylesheet" type="text/css" href="/css/style-night.css">');
}
function toggleNightMode() {
var night = localStorage.getItem("night");
if(night == "true") {
$("#style-dark")[0].remove()
document.getElementById("style-dark").remove()
} else {
$("head").append('<link id="style-dark" rel="stylesheet" type="text/css" href="/css/style-night.css">');
document.getElementsByTagName("head")[0].append(darkStyleLink);
}
localStorage.setItem("night", (night == "true") ? "false" : "true");
}

Voir le fichier

@ -43,7 +43,8 @@ func HomeHandler(w http.ResponseWriter, r *http.Request) {
Max: uint(maxPerPage),
Page: pagenum,
}
torrents, nbTorrents, err := cache.Get(search, func() ([]model.Torrent, int, error) {
torrents, nbTorrents, err := cache.Impl.Get(search, func() ([]model.Torrent, int, error) {
torrents, nbTorrents, err := torrentService.GetAllTorrents(maxPerPage, maxPerPage*(pagenum-1))
if !log.CheckError(err) {
util.SendError(w, err, 400)

Voir le fichier

@ -3,9 +3,7 @@ package router
import (
"fmt"
"html"
"html/template"
"net/http"
"path/filepath"
"strconv"
"github.com/ewhal/nyaa/db"
@ -23,23 +21,6 @@ import (
"github.com/gorilla/mux"
)
var panelIndex, panelTorrentList, panelUserList, panelCommentList, panelTorrentEd, panelTorrentReportList *template.Template
func init() {
panelTorrentList = template.Must(template.New("torrentlist").Funcs(FuncMap).ParseFiles(filepath.Join(TemplateDir, "admin_index.html"), filepath.Join(TemplateDir, "admin/torrentlist.html")))
panelTorrentList = template.Must(panelTorrentList.ParseGlob(filepath.Join(TemplateDir, "_*.html")))
panelUserList = template.Must(template.New("userlist").Funcs(FuncMap).ParseFiles(filepath.Join(TemplateDir, "admin_index.html"), filepath.Join(TemplateDir, "admin/userlist.html")))
panelUserList = template.Must(panelUserList.ParseGlob(filepath.Join(TemplateDir, "_*.html")))
panelCommentList = template.Must(template.New("commentlist").Funcs(FuncMap).ParseFiles(filepath.Join(TemplateDir, "admin_index.html"), filepath.Join(TemplateDir, "admin/commentlist.html")))
panelCommentList = template.Must(panelCommentList.ParseGlob(filepath.Join(TemplateDir, "_*.html")))
panelIndex = template.Must(template.New("indexPanel").Funcs(FuncMap).ParseFiles(filepath.Join(TemplateDir, "admin_index.html"), filepath.Join(TemplateDir, "admin/panelindex.html")))
panelIndex = template.Must(panelIndex.ParseGlob(filepath.Join(TemplateDir, "_*.html")))
panelTorrentEd = template.Must(template.New("torrent_ed").Funcs(FuncMap).ParseFiles(filepath.Join(TemplateDir, "admin_index.html"), filepath.Join(TemplateDir, "admin/paneltorrentedit.html")))
panelTorrentEd = template.Must(panelTorrentEd.ParseGlob(filepath.Join(TemplateDir, "_*.html")))
panelTorrentReportList = template.Must(template.New("torrent_report").Funcs(FuncMap).ParseFiles(filepath.Join(TemplateDir, "admin_index.html"), filepath.Join(TemplateDir, "admin/torrent_report.html")))
panelTorrentReportList = template.Must(panelTorrentReportList.ParseGlob(filepath.Join(TemplateDir, "_*.html")))
}
func IndexModPanel(w http.ResponseWriter, r *http.Request) {
currentUser := GetUser(r)
if userPermission.HasAdmin(currentUser) {
@ -217,19 +198,19 @@ func TorrentPostEditModPanel(w http.ResponseWriter, r *http.Request) {
err := form.NewErrors()
infos := form.NewInfos()
torrent, _ := torrentService.GetTorrentById(id)
if (torrent.ID > 0) {
if torrent.ID > 0 {
errUp := uploadForm.ExtractEditInfo(r)
if errUp != nil {
err["errors"] = append(err["errors"], "Failed to update torrent!")
}
if (len(err) == 0) {
if len(err) == 0 {
// update some (but not all!) values
torrent.Name = uploadForm.Name
torrent.Category = uploadForm.CategoryID
torrent.Name = uploadForm.Name
torrent.Category = uploadForm.CategoryID
torrent.SubCategory = uploadForm.SubCategoryID
torrent.Status = uploadForm.Status
torrent.Status = uploadForm.Status
torrent.Description = uploadForm.Description
torrent.Uploader = nil // GORM will create a new user otherwise (wtf?!)
torrent.Uploader = nil // GORM will create a new user otherwise (wtf?!)
db.ORM.Save(&torrent)
infos["infos"] = append(infos["infos"], "Torrent details updated.")
}

Voir le fichier

@ -4,7 +4,7 @@ import (
"net/http"
"github.com/ewhal/nyaa/service/captcha"
"github.com/gorilla/handlers"
// "github.com/gorilla/handlers"
"github.com/gorilla/mux"
)
@ -15,102 +15,102 @@ func init() {
cssHandler := http.FileServer(http.Dir("./public/css/"))
jsHandler := http.FileServer(http.Dir("./public/js/"))
imgHandler := http.FileServer(http.Dir("./public/img/"))
gzipHomeHandler := http.HandlerFunc(HomeHandler)
gzipAPIHandler := http.HandlerFunc(ApiHandler)
gzipAPIViewHandler := http.HandlerFunc(ApiViewHandler)
gzipViewHandler := http.HandlerFunc(ViewHandler)
gzipUserProfileHandler := http.HandlerFunc(UserProfileHandler)
gzipUserDetailsHandler := http.HandlerFunc(UserDetailsHandler)
gzipUserProfileFormHandler := http.HandlerFunc(UserProfileFormHandler)
/*
// Enable GZIP compression for all handlers except imgHandler and captcha
gzipCSSHandler := handlers.CompressHandler(cssHandler)
gzipJSHandler := handlers.CompressHandler(jsHandler)
gzipHomeHandler := handlers.CompressHandler(http.HandlerFunc(HomeHandler))
gzipSearchHandler := handlers.CompressHandler(http.HandlerFunc(SearchHandler))
gzipAPIHandler := handlers.CompressHandler(http.HandlerFunc(ApiHandler))
gzipAPIViewHandler := handlers.CompressHandler(http.HandlerFunc(ApiViewHandler))
gzipAPIUploadHandler := handlers.CompressHandler(http.HandlerFunc(ApiUploadHandler))
gzipAPIUpdateHandler := handlers.CompressHandler(http.HandlerFunc(ApiUpdateHandler))
gzipFaqHandler := handlers.CompressHandler(http.HandlerFunc(FaqHandler))
gzipRSSHandler := handlers.CompressHandler(http.HandlerFunc(RSSHandler))
gzipViewHandler := handlers.CompressHandler(http.HandlerFunc(ViewHandler))
gzipUploadHandler := handlers.CompressHandler(http.HandlerFunc(UploadHandler))
gzipUserRegisterFormHandler := handlers.CompressHandler(http.HandlerFunc(UserRegisterFormHandler))
gzipUserLoginFormHandler := handlers.CompressHandler(http.HandlerFunc(UserLoginFormHandler))
gzipUserVerifyEmailHandler := handlers.CompressHandler(http.HandlerFunc(UserVerifyEmailHandler))
gzipUserRegisterPostHandler := handlers.CompressHandler(http.HandlerFunc(UserRegisterPostHandler))
gzipUserLoginPostHandler := handlers.CompressHandler(http.HandlerFunc(UserLoginPostHandler))
gzipUserLogoutHandler := handlers.CompressHandler(http.HandlerFunc(UserLogoutHandler))
gzipUserProfileHandler := handlers.CompressHandler(http.HandlerFunc(UserProfileHandler))
gzipUserFollowHandler := handlers.CompressHandler(http.HandlerFunc(UserFollowHandler))
gzipUserDetailsHandler := handlers.CompressHandler(http.HandlerFunc(UserDetailsHandler))
gzipUserProfileFormHandler := handlers.CompressHandler(http.HandlerFunc(UserProfileFormHandler))
gzipCSSHandler := cssHandler)
gzipJSHandler:= jsHandler)
gzipSearchHandler:= http.HandlerFunc(SearchHandler)
gzipAPIUploadHandler := http.HandlerFunc(ApiUploadHandler)
gzipAPIUpdateHandler := http.HandlerFunc(ApiUpdateHandler)
gzipFaqHandler := http.HandlerFunc(FaqHandler)
gzipRSSHandler := http.HandlerFunc(RSSHandler)
gzipUploadHandler := http.HandlerFunc(UploadHandler)
gzipUserRegisterFormHandler := http.HandlerFunc(UserRegisterFormHandler)
gzipUserLoginFormHandler := http.HandlerFunc(UserLoginFormHandler)
gzipUserVerifyEmailHandler := http.HandlerFunc(UserVerifyEmailHandler)
gzipUserRegisterPostHandler := http.HandlerFunc(UserRegisterPostHandler)
gzipUserLoginPostHandler := http.HandlerFunc(UserLoginPostHandler)
gzipUserLogoutHandler := http.HandlerFunc(UserLogoutHandler)
gzipUserFollowHandler := http.HandlerFunc(UserFollowHandler)
gzipIndexModPanel := handlers.CompressHandler(http.HandlerFunc(IndexModPanel))
gzipTorrentsListPanel := handlers.CompressHandler(http.HandlerFunc(TorrentsListPanel))
gzipTorrentReportListPanel := handlers.CompressHandler(http.HandlerFunc(TorrentReportListPanel))
gzipUsersListPanel := handlers.CompressHandler(http.HandlerFunc(UsersListPanel))
gzipCommentsListPanel := handlers.CompressHandler(http.HandlerFunc(CommentsListPanel))
gzipTorrentEditModPanel := handlers.CompressHandler(http.HandlerFunc(TorrentEditModPanel))
gzipTorrentPostEditModPanel := handlers.CompressHandler(http.HandlerFunc(TorrentPostEditModPanel))
gzipCommentDeleteModPanel := handlers.CompressHandler(http.HandlerFunc(CommentDeleteModPanel))
gzipTorrentDeleteModPanel := handlers.CompressHandler(http.HandlerFunc(TorrentDeleteModPanel))
gzipTorrentReportDeleteModPanel := handlers.CompressHandler(http.HandlerFunc(TorrentReportDeleteModPanel))
gzipIndexModPanel := http.HandlerFunc(IndexModPanel)
gzipTorrentsListPanel := http.HandlerFunc(TorrentsListPanel)
gzipTorrentReportListPanel := http.HandlerFunc(TorrentReportListPanel)
gzipUsersListPanel := http.HandlerFunc(UsersListPanel)
gzipCommentsListPanel := http.HandlerFunc(CommentsListPanel)
gzipTorrentEditModPanel := http.HandlerFunc(TorrentEditModPanel)
gzipTorrentPostEditModPanel := http.HandlerFunc(TorrentPostEditModPanel)
gzipCommentDeleteModPanel := http.HandlerFunc(CommentDeleteModPanel)
gzipTorrentDeleteModPanel := http.HandlerFunc(TorrentDeleteModPanel)
gzipTorrentReportDeleteModPanel := http.HandlerFunc(TorrentReportDeleteModPanel)*/
//gzipTorrentReportCreateHandler := handlers.CompressHandler(http.HandlerFunc(CreateTorrentReportHandler))
//gzipTorrentReportDeleteHandler := handlers.CompressHandler(http.HandlerFunc(DeleteTorrentReportHandler))
//gzipTorrentDeleteHandler := handlers.CompressHandler(http.HandlerFunc(DeleteTorrentHandler))
//gzipTorrentReportCreateHandler := http.HandlerFunc(CreateTorrentReportHandler)
//gzipTorrentReportDeleteHandler := http.HandlerFunc(DeleteTorrentReportHandler)
//gzipTorrentDeleteHandler := http.HandlerFunc(DeleteTorrentHandler)
Router = mux.NewRouter()
// Routes
http.Handle("/css/", http.StripPrefix("/css/", wrapHandler(gzipCSSHandler)))
http.Handle("/js/", http.StripPrefix("/js/", wrapHandler(gzipJSHandler)))
http.Handle("/img/", http.StripPrefix("/img/", wrapHandler(imgHandler)))
Router.Handle("/", gzipHomeHandler).Name("home")
http.Handle("/css/", http.StripPrefix("/css/", cssHandler))
http.Handle("/js/", http.StripPrefix("/js/", jsHandler))
http.Handle("/img/", http.StripPrefix("/img/", imgHandler))
Router.Handle("/", wrapHandler(gzipHomeHandler)).Name("home")
Router.Handle("/page/{page:[0-9]+}", wrapHandler(gzipHomeHandler)).Name("home_page")
Router.Handle("/search", gzipSearchHandler).Name("search")
Router.Handle("/search/{page}", gzipSearchHandler).Name("search_page")
Router.Handle("/api", gzipAPIHandler).Methods("GET")
Router.HandleFunc("/search", SearchHandler).Name("search")
Router.HandleFunc("/search/{page}", SearchHandler).Name("search_page")
Router.Handle("/api", wrapHandler(gzipAPIHandler)).Methods("GET")
Router.Handle("/api/{page:[0-9]*}", wrapHandler(gzipAPIHandler)).Methods("GET")
Router.Handle("/api/view/{id}", wrapHandler(gzipAPIViewHandler)).Methods("GET")
Router.Handle("/api/upload", gzipAPIUploadHandler).Methods("POST")
Router.Handle("/api/update", gzipAPIUpdateHandler).Methods("PUT")
Router.Handle("/faq", gzipFaqHandler).Name("faq")
Router.Handle("/feed", gzipRSSHandler).Name("feed")
Router.HandleFunc("/api/upload", ApiUploadHandler).Methods("POST")
Router.HandleFunc("/api/update", ApiUpdateHandler).Methods("PUT")
Router.HandleFunc("/faq", FaqHandler).Name("faq")
Router.HandleFunc("/feed", RSSHandler).Name("feed")
Router.Handle("/view/{id}", wrapHandler(gzipViewHandler)).Methods("GET").Name("view_torrent")
Router.HandleFunc("/view/{id}", PostCommentHandler).Methods("POST").Name("post_comment")
Router.Handle("/upload", gzipUploadHandler).Name("upload")
Router.Handle("/user/register", gzipUserRegisterFormHandler).Name("user_register").Methods("GET")
Router.Handle("/user/login", gzipUserLoginFormHandler).Name("user_login").Methods("GET")
Router.Handle("/verify/email/{token}", gzipUserVerifyEmailHandler).Name("user_verify").Methods("GET")
Router.Handle("/user/register", gzipUserRegisterPostHandler).Name("user_register").Methods("POST")
Router.Handle("/user/login", gzipUserLoginPostHandler).Name("user_login").Methods("POST")
Router.Handle("/user/logout", gzipUserLogoutHandler).Name("user_logout")
Router.HandleFunc("/upload", UploadHandler).Name("upload")
Router.HandleFunc("/user/register", UserRegisterFormHandler).Name("user_register").Methods("GET")
Router.HandleFunc("/user/login", UserLoginFormHandler).Name("user_login").Methods("GET")
Router.HandleFunc("/verify/email/{token}", UserVerifyEmailHandler).Name("user_verify").Methods("GET")
Router.HandleFunc("/user/register", UserRegisterPostHandler).Name("user_register").Methods("POST")
Router.HandleFunc("/user/login", UserLoginPostHandler).Name("user_login").Methods("POST")
Router.HandleFunc("/user/logout", UserLogoutHandler).Name("user_logout")
Router.Handle("/user/{id}/{username}", wrapHandler(gzipUserProfileHandler)).Name("user_profile").Methods("GET")
Router.Handle("/user/{id}/{username}/follow", gzipUserFollowHandler).Name("user_follow").Methods("GET")
Router.HandleFunc("/user/{id}/{username}/follow", UserFollowHandler).Name("user_follow").Methods("GET")
Router.Handle("/user/{id}/{username}/edit", wrapHandler(gzipUserDetailsHandler)).Name("user_profile_details").Methods("GET")
Router.Handle("/user/{id}/{username}/edit", wrapHandler(gzipUserProfileFormHandler)).Name("user_profile_edit").Methods("POST")
Router.Handle("/mod", gzipIndexModPanel).Name("mod_index")
Router.Handle("/mod/torrents", gzipTorrentsListPanel).Name("mod_tlist")
Router.Handle("/mod/torrents/{page}", gzipTorrentsListPanel).Name("mod_tlist_page")
Router.Handle("/mod/reports", gzipTorrentReportListPanel).Name("mod_trlist")
Router.Handle("/mod/reports/{page}", gzipTorrentReportListPanel).Name("mod_trlist_page")
Router.Handle("/mod/users", gzipUsersListPanel).Name("mod_ulist")
Router.Handle("/mod/users/{page}", gzipUsersListPanel).Name("mod_ulist_page")
Router.Handle("/mod/comments", gzipCommentsListPanel).Name("mod_clist")
Router.Handle("/mod/comments/{page}", gzipCommentsListPanel).Name("mod_clist_page")
Router.Handle("/mod/comment", gzipCommentsListPanel).Name("mod_cedit") // TODO
Router.Handle("/mod/torrent/", gzipTorrentEditModPanel).Name("mod_tedit").Methods("GET")
Router.Handle("/mod/torrent/", gzipTorrentPostEditModPanel).Name("mod_ptedit").Methods("POST")
Router.Handle("/mod/torrent/delete", gzipTorrentDeleteModPanel).Name("mod_tdelete")
Router.Handle("/mod/report/delete", gzipTorrentReportDeleteModPanel).Name("mod_trdelete")
Router.Handle("/mod/comment/delete", gzipCommentDeleteModPanel).Name("mod_cdelete")
Router.HandleFunc("/mod", IndexModPanel).Name("mod_index")
Router.HandleFunc("/mod/torrents", TorrentsListPanel).Name("mod_tlist")
Router.HandleFunc("/mod/torrents/{page}", TorrentsListPanel).Name("mod_tlist_page")
Router.HandleFunc("/mod/reports", TorrentReportListPanel).Name("mod_trlist")
Router.HandleFunc("/mod/reports/{page}", TorrentReportListPanel).Name("mod_trlist_page")
Router.HandleFunc("/mod/users", UsersListPanel).Name("mod_ulist")
Router.HandleFunc("/mod/users/{page}", UsersListPanel).Name("mod_ulist_page")
Router.HandleFunc("/mod/comments", CommentsListPanel).Name("mod_clist")
Router.HandleFunc("/mod/comments/{page}", CommentsListPanel).Name("mod_clist_page")
Router.HandleFunc("/mod/comment", CommentsListPanel).Name("mod_cedit") // TODO
Router.HandleFunc("/mod/torrent/", TorrentEditModPanel).Name("mod_tedit").Methods("GET")
Router.HandleFunc("/mod/torrent/", TorrentPostEditModPanel).Name("mod_ptedit").Methods("POST")
Router.HandleFunc("/mod/torrent/delete", TorrentDeleteModPanel).Name("mod_tdelete")
Router.HandleFunc("/mod/report/delete", TorrentReportDeleteModPanel).Name("mod_trdelete")
Router.HandleFunc("/mod/comment/delete", CommentDeleteModPanel).Name("mod_cdelete")
//reporting a torrent
Router.HandleFunc("/report/{id}", ReportTorrentHandler).Methods("POST").Name("post_comment")
Router.PathPrefix("/captcha").Methods("GET").HandlerFunc(captcha.ServeFiles)
//Router.Handle("/report/create", gzipTorrentReportCreateHandler).Name("torrent_report_create").Methods("POST")
//Router.HandleFunc("/report/create", gzipTorrentReportCreateHandler).Name("torrent_report_create").Methods("POST")
// TODO Allow only moderators to access /moderation/*
//Router.Handle("/moderation/report/delete", gzipTorrentReportDeleteHandler).Name("torrent_report_delete").Methods("POST")
//Router.Handle("/moderation/torrent/delete", gzipTorrentDeleteHandler).Name("torrent_delete").Methods("POST")
//Router.HandleFunc("/moderation/report/delete", gzipTorrentReportDeleteHandler).Name("torrent_report_delete").Methods("POST")
//Router.HandleFunc("/moderation/torrent/delete", gzipTorrentDeleteHandler).Name("torrent_delete").Methods("POST")
Router.NotFoundHandler = http.HandlerFunc(NotFoundHandler)
}

Voir le fichier

@ -26,22 +26,21 @@ func RSSHandler(w http.ResponseWriter, r *http.Request) {
Link: &feeds.Link{Href: "https://" + config.WebAddress + "/"},
Created: createdAsTime,
}
feed.Items = []*feeds.Item{}
feed.Items = make([]*feeds.Item, len(torrents))
for i := range torrents {
torrentJSON := torrents[i].ToJSON()
for i, torrent := range torrents {
torrentJSON := torrent.ToJSON()
feed.Items[i] = &feeds.Item{
// need a torrent view first
Id: "https://" + config.WebAddress + "/view/" + strconv.FormatUint(uint64(torrents[i].ID), 10),
Title: torrents[i].Name,
Title: torrent.Name,
Link: &feeds.Link{Href: string(torrentJSON.Magnet)},
Description: "",
Created: torrents[0].Date,
Updated: torrents[0].Date,
Description: string(torrentJSON.Description),
Created: torrent.Date,
Updated: torrent.Date,
}
}
// allow cross domain AJAX requests
w.Header().Set("Access-Control-Allow-Origin", "*")
rss, rssErr := feed.ToRss()
if rssErr != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)

Voir le fichier

@ -9,15 +9,18 @@ var TemplateDir = "templates"
var homeTemplate, searchTemplate, faqTemplate, uploadTemplate, viewTemplate, viewRegisterTemplate, viewLoginTemplate, viewRegisterSuccessTemplate, viewVerifySuccessTemplate, viewProfileTemplate, viewProfileEditTemplate, viewUserDeleteTemplate, notFoundTemplate *template.Template
var panelIndex, panelTorrentList, panelUserList, panelCommentList, panelTorrentEd, panelTorrentReportList *template.Template
type templateLoader struct {
templ **template.Template
file string
name string
templ **template.Template
file string
indexFile string
name string
}
// ReloadTemplates reloads templates on runtime
func ReloadTemplates() {
templs := []templateLoader{
pubTempls := []templateLoader{
templateLoader{
templ: &homeTemplate,
name: "home",
@ -46,37 +49,37 @@ func ReloadTemplates() {
templateLoader{
templ: &viewRegisterTemplate,
name: "user_register",
file: "user/register.html",
file: filepath.Join("user", "register.html"),
},
templateLoader{
templ: &viewRegisterSuccessTemplate,
name: "user_register_success",
file: "user/signup_success.html",
file: filepath.Join("user", "signup_success.html"),
},
templateLoader{
templ: &viewVerifySuccessTemplate,
name: "user_verify_success",
file: "user/verify_success.html",
file: filepath.Join("user", "verify_success.html"),
},
templateLoader{
templ: &viewLoginTemplate,
name: "user_login",
file: "user/login.html",
file: filepath.Join("user", "login.html"),
},
templateLoader{
templ: &viewProfileTemplate,
name: "user_profile",
file: "user/profile.html",
file: filepath.Join("user", "profile.html"),
},
templateLoader{
templ: &viewProfileEditTemplate,
name: "user_profile",
file: "user/profile_edit.html",
file: filepath.Join("user", "profile_edit.html"),
},
templateLoader{
templ: &viewUserDeleteTemplate,
name: "user_delete",
file: "user/delete_success.html",
file: filepath.Join("user", "delete_success.html"),
},
templateLoader{
templ: &notFoundTemplate,
@ -84,10 +87,56 @@ func ReloadTemplates() {
file: "404.html",
},
}
for _, templ := range templs {
t := template.Must(template.New(templ.name).Funcs(FuncMap).ParseFiles(filepath.Join(TemplateDir, "index.html"), filepath.Join(TemplateDir, templ.file)))
t = template.Must(t.ParseGlob(filepath.Join(TemplateDir, "_*.html")))
for idx := range pubTempls {
pubTempls[idx].indexFile = filepath.Join(TemplateDir, "index.html")
}
modTempls := []templateLoader{
templateLoader{
templ: &panelTorrentList,
name: "torrentlist",
file: filepath.Join("admin", "torrentlist.html"),
},
templateLoader{
templ: &panelUserList,
name: "userlist",
file: filepath.Join("admin", "userlist.html"),
},
templateLoader{
templ: &panelCommentList,
name: "commentlist",
file: filepath.Join("admin", "commentlist.html"),
},
templateLoader{
templ: &panelIndex,
name: "indexPanel",
file: filepath.Join("admin", "panelindex.html"),
},
templateLoader{
templ: &panelTorrentEd,
name: "torrent_ed",
file: filepath.Join("admin", "paneltorrentedit.html"),
},
templateLoader{
templ: &panelTorrentReportList,
name: "torrent_report",
file: filepath.Join("admin", "torrent_report.html"),
},
}
for idx := range modTempls {
modTempls[idx].indexFile = filepath.Join(TemplateDir, "admin_index.html")
}
templs := make([]templateLoader, 0, len(modTempls)+len(pubTempls))
templs = append(templs, pubTempls...)
templs = append(templs, modTempls...)
for _, templ := range templs {
t := template.Must(template.New(templ.name).Funcs(FuncMap).ParseFiles(templ.indexFile, filepath.Join(TemplateDir, templ.file)))
t = template.Must(t.ParseGlob(filepath.Join(TemplateDir, "_*.html")))
*templ.templ = t
}
}

Voir le fichier

@ -95,7 +95,7 @@ func (f *UploadForm) ExtractInfo(r *http.Request) error {
f.Name = util.TrimWhitespaces(f.Name)
f.Description = p.Sanitize(util.TrimWhitespaces(f.Description))
f.Magnet = util.TrimWhitespaces(f.Magnet)
cache.Clear()
cache.Impl.ClearAll()
catsSplit := strings.Split(f.Category, "_")
// need this to prevent out of index panics

Voir le fichier

@ -156,19 +156,19 @@ func UserProfileFormHandler(w http.ResponseWriter, r *http.Request) {
if len(err) == 0 {
modelHelper.BindValueForm(&b, r)
if !userPermission.HasAdmin(currentUser) {
b.Username = currentUser.Username
b.Status = currentUser.Status
b.Username = userProfile.Username
b.Status = userProfile.Status
} else {
if b.Status == 2 {
if userProfile.Status != b.Status && b.Status == 2 {
err["errors"] = append(err["errors"], "Elevating status to moderator is prohibited")
}
}
err = modelHelper.ValidateForm(&b, err)
if len(err) == 0 {
if b.Email != currentUser.Email {
if b.Email != userProfile.Email {
userService.SendVerificationToUser(*currentUser, b.Email)
infos["infos"] = append(infos["infos"], fmt.Sprintf(T("email_changed"), b.Email))
b.Email = currentUser.Email // reset, it will be set when user clicks verification
b.Email = userProfile.Email // reset, it will be set when user clicks verification
}
userProfile, _, errorUser = userService.UpdateUser(w, &b, currentUser, id)
if errorUser != nil {

Voir le fichier

@ -39,17 +39,19 @@ func PostCommentHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
if strings.TrimSpace(r.FormValue("comment")) == "" {
http.Error(w, "comment empty", 406)
}
userCaptcha := captcha.Extract(r)
if !captcha.Authenticate(userCaptcha) {
http.Error(w, "bad captcha", 403)
return
}
currentUser := GetUser(r)
content := p.Sanitize(r.FormValue("comment"))
if strings.TrimSpace(content) == "" {
http.Error(w, "comment empty", 406)
return
}
idNum, err := strconv.Atoi(id)
userID := currentUser.ID
@ -76,6 +78,7 @@ func ReportTorrentHandler(w http.ResponseWriter, r *http.Request) {
userCaptcha := captcha.Extract(r)
if !captcha.Authenticate(userCaptcha) {
http.Error(w, "bad captcha", 403)
return
}
currentUser := GetUser(r)

Voir le fichier

@ -28,15 +28,42 @@ func (b *Bucket) NewTransaction(swarms []model.Torrent) (t *Transaction) {
t = &Transaction{
TransactionID: id,
bucket: b,
swarms: swarms,
swarms: make([]model.Torrent, len(swarms)),
state: stateSendID,
}
copy(t.swarms[:], swarms[:])
b.transactions[id] = t
b.access.Unlock()
return
}
func (b *Bucket) ForEachTransaction(v func(uint32, *Transaction)) {
clone := make(map[uint32]*Transaction)
b.access.Lock()
for k := range b.transactions {
clone[k] = b.transactions[k]
}
b.access.Unlock()
for k := range clone {
v(k, clone[k])
}
}
func (b *Bucket) Forget(tid uint32) {
b.access.Lock()
_, ok := b.transactions[tid]
if ok {
delete(b.transactions, tid)
}
b.access.Unlock()
}
func (b *Bucket) VisitTransaction(tid uint32, v func(*Transaction)) {
b.access.Lock()
t, ok := b.transactions[tid]

Voir le fichier

@ -13,15 +13,20 @@ import (
// MTU yes this is the ipv6 mtu
const MTU = 1500
// max number of scrapes per packet
const ScrapesPerPacket = 74
// bittorrent scraper
type Scraper struct {
done chan int
sendQueue chan *SendEvent
recvQueue chan *RecvEvent
errQueue chan error
trackers map[string]*Bucket
ticker *time.Ticker
interval time.Duration
done chan int
sendQueue chan *SendEvent
recvQueue chan *RecvEvent
errQueue chan error
trackers map[string]*Bucket
ticker *time.Ticker
cleanup *time.Ticker
interval time.Duration
PacketsPerSecond uint
}
func New(conf *config.ScraperConfig) (sc *Scraper, err error) {
@ -31,9 +36,15 @@ func New(conf *config.ScraperConfig) (sc *Scraper, err error) {
recvQueue: make(chan *RecvEvent, 1024),
errQueue: make(chan error),
trackers: make(map[string]*Bucket),
ticker: time.NewTicker(time.Second),
ticker: time.NewTicker(time.Second * 10),
interval: time.Second * time.Duration(conf.IntervalSeconds),
cleanup: time.NewTicker(time.Minute),
}
if sc.PacketsPerSecond == 0 {
sc.PacketsPerSecond = 10
}
for idx := range conf.Trackers {
err = sc.AddTracker(&conf.Trackers[idx])
if err != nil {
@ -144,29 +155,55 @@ func (sc *Scraper) RunWorker(pc net.PacketConn) (err error) {
func (sc *Scraper) Run() {
for {
<-sc.ticker.C
sc.Scrape()
select {
case <-sc.ticker.C:
sc.Scrape(sc.PacketsPerSecond)
break
case <-sc.cleanup.C:
sc.removeStale()
break
}
}
}
func (sc *Scraper) Scrape() {
now := time.Now().Add(0 - sc.interval)
func (sc *Scraper) removeStale() {
rows, err := db.ORM.Raw("SELECT torrent_id, torrent_hash FROM torrents WHERE last_scrape IS NULL OR last_scrape < ? ORDER BY torrent_id DESC LIMIT 700", now).Rows()
for k := range sc.trackers {
sc.trackers[k].ForEachTransaction(func(tid uint32, t *Transaction) {
if t == nil || t.IsTimedOut() {
sc.trackers[k].Forget(tid)
}
})
}
}
func (sc *Scraper) Scrape(packets uint) {
now := time.Now().Add(0 - sc.interval)
// only scrape torretns uploaded within 90 days
oldest := now.Add(0 - (time.Hour * 24 * 90))
rows, err := db.ORM.Raw("SELECT torrent_id, torrent_hash FROM torrents WHERE ( last_scrape IS NULL OR last_scrape < ? ) AND date > ? ORDER BY torrent_id DESC LIMIT ?", now, oldest, packets*ScrapesPerPacket).Rows()
if err == nil {
counter := 0
var scrape [70]model.Torrent
var scrape [ScrapesPerPacket]model.Torrent
for rows.Next() {
idx := counter % 70
idx := counter % ScrapesPerPacket
rows.Scan(&scrape[idx].ID, &scrape[idx].Hash)
counter++
if idx == 0 {
if counter%ScrapesPerPacket == 0 {
for _, b := range sc.trackers {
t := b.NewTransaction(scrape[:])
sc.sendQueue <- t.SendEvent(b.Addr)
}
}
}
idx := counter % ScrapesPerPacket
if idx > 0 {
for _, b := range sc.trackers {
t := b.NewTransaction(scrape[:idx])
sc.sendQueue <- t.SendEvent(b.Addr)
}
}
log.Infof("scrape %d", counter)
rows.Close()
} else {

Voir le fichier

@ -11,6 +11,9 @@ import (
"github.com/ewhal/nyaa/util/log"
)
// TransactionTimeout 30 second timeout for transactions
const TransactionTimeout = time.Second * 30
const stateSendID = 0
const stateRecvID = 1
const stateTransact = 2
@ -27,13 +30,12 @@ type Transaction struct {
bucket *Bucket
state uint8
swarms []model.Torrent
lastData time.Time
}
// Done marks this transaction as done and removes it from parent
func (t *Transaction) Done() {
t.bucket.access.Lock()
delete(t.bucket.transactions, t.TransactionID)
t.bucket.access.Unlock()
t.bucket.Forget(t.TransactionID)
}
func (t *Transaction) handleScrapeReply(data []byte) {
@ -51,18 +53,22 @@ func (t *Transaction) handleScrapeReply(data []byte) {
}
}
const pgQuery = "UPDATE torrents SET seeders = $1 , leechers = $2 , completed = $3 , last_scrape = $4 WHERE torrent_id = $5"
const sqliteQuery = "UPDATE torrents SET seeders = ? , leechers = ? , completed = ? , last_scrape = ? WHERE torrent_id = ?"
// Sync syncs models with database
func (t *Transaction) Sync() (err error) {
for idx := range t.swarms {
err = db.ORM.Model(&t.swarms[idx]).Updates(map[string]interface{}{
"seeders": t.swarms[idx].Seeders,
"leechers": t.swarms[idx].Leechers,
"completed": t.swarms[idx].Completed,
"last_scrape": t.swarms[idx].LastScrape,
}).Error
if err != nil {
break
q := pgQuery
if db.IsSqlite {
q = sqliteQuery
}
tx, e := db.ORM.DB().Begin()
err = e
if err == nil {
for idx := range t.swarms {
_, err = tx.Exec(q, t.swarms[idx].Seeders, t.swarms[idx].Leechers, t.swarms[idx].Completed, t.swarms[idx].LastScrape, t.swarms[idx].ID)
}
tx.Commit()
}
return
}
@ -95,6 +101,7 @@ func (t *Transaction) SendEvent(to net.Addr) (ev *SendEvent) {
binary.BigEndian.PutUint32(ev.Data[12:], t.TransactionID)
t.state = stateRecvID
}
t.lastData = time.Now()
return
}
@ -104,7 +111,7 @@ func (t *Transaction) handleError(msg string) {
// handle data for transaction
func (t *Transaction) GotData(data []byte) (done bool) {
t.lastData = time.Now()
if len(data) > 4 {
cmd := binary.BigEndian.Uint32(data)
switch cmd {
@ -132,3 +139,8 @@ func (t *Transaction) GotData(data []byte) (done bool) {
}
return
}
func (t *Transaction) IsTimedOut() bool {
return t.lastData.Add(TransactionTimeout).Before(time.Now())
}

Voir le fichier

@ -5,96 +5,98 @@ import (
"github.com/ewhal/nyaa/db"
"github.com/ewhal/nyaa/model"
formStruct "github.com/ewhal/nyaa/service/user/form"
"github.com/ewhal/nyaa/util/log"
"github.com/ewhal/nyaa/util/modelHelper"
"github.com/ewhal/nyaa/util/timeHelper"
"github.com/gorilla/securecookie"
"golang.org/x/crypto/bcrypt"
"net/http"
"strconv"
"time"
)
const CookieName = "session"
// If you want to keep login cookies between restarts you need to make these permanent
var cookieHandler = securecookie.New(
securecookie.GenerateRandomKey(64),
securecookie.GenerateRandomKey(32))
func Token(r *http.Request) (string, error) {
var token string
cookie, err := r.Cookie("session")
// Encoding & Decoding of the cookie value
func DecodeCookie(cookie_value string) (uint, error) {
value := make(map[string]string)
err := cookieHandler.Decode(CookieName, cookie_value, &value)
if err != nil {
return token, err
return 0, err
}
cookieValue := make(map[string]string)
err = cookieHandler.Decode("session", cookie.Value, &cookieValue)
if err != nil {
return token, err
time_int, _ := strconv.ParseInt(value["t"], 10, 0)
if timeHelper.IsExpired(time.Unix(time_int, 0)) {
return 0, errors.New("Cookie is expired")
}
token = cookieValue["token"]
if len(token) == 0 {
return token, errors.New("token is empty")
}
return token, nil
ret, err := strconv.ParseUint(value["u"], 10, 0)
return uint(ret), err
}
// SetCookie sets a cookie.
func SetCookie(w http.ResponseWriter, token string) (int, error) {
func EncodeCookie(user_id uint) (string, error) {
validUntil := timeHelper.FewDaysLater(7) // 1 week
value := map[string]string{
"token": token,
"u": strconv.FormatUint(uint64(user_id), 10),
"t": strconv.FormatInt(validUntil.Unix(), 10),
}
encoded, err := cookieHandler.Encode("session", value)
if err != nil {
return http.StatusInternalServerError, err
}
cookie := &http.Cookie{
Name: "session",
Value: encoded,
Path: "/",
}
http.SetCookie(w, cookie)
return http.StatusOK, nil
return cookieHandler.Encode(CookieName, value)
}
// ClearCookie clears a cookie.
func ClearCookie(w http.ResponseWriter) (int, error) {
cookie := &http.Cookie{
Name: "session",
Name: CookieName,
Value: "",
Path: "/",
HttpOnly: true,
MaxAge: -1,
}
http.SetCookie(w, cookie)
return http.StatusOK, nil
}
// SetCookieHandler sets a cookie with email and password.
// SetCookieHandler sets the authentication cookie
func SetCookieHandler(w http.ResponseWriter, email string, pass string) (int, error) {
if email != "" && pass != "" {
var user model.User
isValidEmail, _ := formStruct.EmailValidation(email, formStruct.NewErrors())
if isValidEmail {
log.Debug("User entered valid email.")
if db.ORM.Where("email = ?", email).First(&user).RecordNotFound() {
return http.StatusNotFound, errors.New("User not found")
}
} else {
log.Debug("User entered username.")
if db.ORM.Where("username = ?", email).First(&user).RecordNotFound() {
return http.StatusNotFound, errors.New("User not found")
}
}
err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(pass))
if err != nil {
return http.StatusUnauthorized, errors.New("Password incorrect")
}
if user.Status == -1 {
return http.StatusUnauthorized, errors.New("Account banned")
}
status, err := SetCookie(w, user.Token)
if err != nil {
return status, err
}
w.Header().Set("X-Auth-Token", user.Token)
return http.StatusOK, nil
if email == "" || pass == "" {
return http.StatusNotFound, errors.New("No username/password entered")
}
return http.StatusNotFound, errors.New("user not found")
var user model.User
// search by email or username
isValidEmail, _ := formStruct.EmailValidation(email, formStruct.NewErrors())
if isValidEmail {
if db.ORM.Where("email = ?", email).First(&user).RecordNotFound() {
return http.StatusNotFound, errors.New("User not found")
}
} else {
if db.ORM.Where("username = ?", email).First(&user).RecordNotFound() {
return http.StatusNotFound, errors.New("User not found")
}
}
err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(pass))
if err != nil {
return http.StatusUnauthorized, errors.New("Password incorrect")
}
if user.Status == -1 {
return http.StatusUnauthorized, errors.New("Account banned")
}
encoded, err := EncodeCookie(user.ID)
if err != nil {
return http.StatusInternalServerError, err
}
cookie := &http.Cookie{
Name: CookieName,
Value: encoded,
Path: "/",
HttpOnly: true,
}
http.SetCookie(w, cookie)
// also set response header for convenience
w.Header().Set("X-Auth-Token", encoded)
return http.StatusOK, nil
}
// RegisterHanderFromForm sets cookie from a RegistrationForm.
@ -111,24 +113,31 @@ func RegisterHandler(w http.ResponseWriter, r *http.Request) (int, error) {
return RegisterHanderFromForm(w, registrationForm)
}
// CurrentUser get a current user.
// CurrentUser determines the current user from the request
func CurrentUser(r *http.Request) (model.User, error) {
var user model.User
var token string
var err error
token = r.Header.Get("X-Auth-Token")
if len(token) > 0 {
log.Debug("header token exists")
} else {
token, err = Token(r)
log.Debug("header token does not exist")
var encoded string
encoded = r.Header.Get("X-Auth-Token")
if len(encoded) == 0 {
// check cookie instead
cookie, err := r.Cookie(CookieName)
if err != nil {
return user, err
}
encoded = cookie.Value
}
if db.ORM.Where("api_token = ?", token).First(&user).RecordNotFound() {
return user, errors.New("user not found")
user_id, err := DecodeCookie(encoded)
if err != nil {
return user, err
}
err = db.ORM.Model(&user).Error
return user, err
if db.ORM.Where("user_id = ?", user_id).First(&user).RecordNotFound() {
return user, errors.New("User not found")
}
if user.Status == -1 {
// recheck as user might've been banned in the meantime
return user, errors.New("Account banned")
}
return user, nil
}

Voir le fichier

@ -7,7 +7,6 @@ import (
"strconv"
"time"
"github.com/ewhal/nyaa/config"
"github.com/ewhal/nyaa/db"
"github.com/ewhal/nyaa/model"
formStruct "github.com/ewhal/nyaa/service/user/form"
@ -15,7 +14,6 @@ import (
"github.com/ewhal/nyaa/util/crypto"
"github.com/ewhal/nyaa/util/log"
"github.com/ewhal/nyaa/util/modelHelper"
"github.com/ewhal/nyaa/util/timeHelper"
"golang.org/x/crypto/bcrypt"
)
@ -69,15 +67,8 @@ func CreateUserFromForm(registrationForm formStruct.RegistrationForm) (model.Use
return user, err
}
}
token, err := crypto.GenerateRandomToken32()
if err != nil {
return user, errors.New("token not generated")
}
user.Email = "" // unset email because it will be verified later
user.Token = token
user.TokenExpiration = timeHelper.FewDaysLater(config.AuthTokenExpirationDay)
log.Debugf("user %+v\n", user)
if db.ORM.Create(&user).Error != nil {
return user, errors.New("user not created")
}
@ -157,17 +148,13 @@ func UpdateUserCore(user *model.User) (int, error) {
}
}
token, err := crypto.GenerateRandomToken32()
user.UpdatedAt = time.Now()
err := db.ORM.Save(user).Error
if err != nil {
return http.StatusInternalServerError, err
}
user.Token = token
user.TokenExpiration = timeHelper.FewDaysLater(config.AuthTokenExpirationDay)
if db.ORM.Save(user).Error != nil {
return http.StatusInternalServerError, err
}
user.UpdatedAt = time.Now()
return http.StatusOK, nil
}
@ -197,18 +184,13 @@ func UpdateUser(w http.ResponseWriter, form *formStruct.UserForm, currentUser *m
form.Username = user.Username
}
if (form.Email != user.Email) {
// send verification to new email and keep old
SendVerificationToUser(user, form.Email)
form.Email = user.Email
}
log.Debugf("form %+v\n", form)
modelHelper.AssignValue(&user, form)
status, err := UpdateUserCore(&user)
if err != nil {
return user, status, err
}
if userPermission.CurrentUserIdentical(currentUser, user.ID) {
status, err = SetCookie(w, user.Token)
}
return user, status, err
}

Voir le fichier

@ -45,7 +45,6 @@ udp://tracker.leechers-paradise.org:6969
udp://explodie.org:6969
udp://tracker.opentrackr.org:1337
udp://tracker.internetwarriors.net:1337/announce
udp://eddie4.nl:6969/announce
http://mgtracker.org:6969/announce
http://tracker.baka-sub.cf/announce</pre>

Voir le fichier

@ -14,7 +14,7 @@
<div class="col-lg-8">
<input class="form-control" type="text" name="email" id="email" value="{{.Email}}">
{{ range (index $.FormErrors "email")}}
<p class="bg-danger">{{ . }}</p>
<p class="text-error">{{ . }}</p>
{{end}}
</div>
</div>
@ -30,7 +30,7 @@
</select>
</div>
{{ range (index $.FormErrors "language")}}
<p class="bg-danger">{{ . }}</p>
<p class="text-error">{{ . }}</p>
{{end}}
</div>
</div>
@ -40,7 +40,7 @@
<div class="col-md-8">
<input class="form-control" name="current_password" id="current_password" type="password">
{{ range (index $.FormErrors "current_password")}}
<p class="bg-danger">{{ . }}</p>
<p class="text-error">{{ . }}</p>
{{end}}
</div>
</div>
@ -50,7 +50,7 @@
<div class="col-md-8">
<input class="form-control" name="password" id="password" type="password">
{{ range (index $.FormErrors "password")}}
<p class="bg-danger">{{ . }}</p>
<p class="text-error">{{ . }}</p>
{{end}}
</div>
</div>
@ -59,7 +59,7 @@
<div class="col-md-8">
<input class="form-control" name="password_confirmation" id="password_confirmation" type="password">
{{ range (index $.FormErrors "password_confirmation")}}
<p class="bg-danger">{{ . }}</p>
<p class="text-error">{{ . }}</p>
{{end}}
</div>
</div>
@ -70,7 +70,7 @@
<div class="col-md-8">
<input class="form-control" name="username" id="username" type="text" value="{{.Username}}">
{{ range (index $.FormErrors "username")}}
<p class="bg-danger">{{ . }}</p>
<p class="text-error">{{ . }}</p>
{{end}}
</div>
</div>
@ -81,12 +81,14 @@
<select id="status" name="status" class="form-control">
<option value="-1" {{ if eq .Status -1 }}selected{{end}}>{{ T "banned"}}</option>
<option value="0" {{ if eq .Status 0 }}selected{{end}}>{{ T "member"}} ({{ T "default" }})</option>
<option value="1" {{ if eq .Status 1 }}selected{{end}}>{{ T "trusted_member"}} </option>
<!-- <option value="2" {{ if eq .Status 2 }}selected{{end}}>{{ T "moderator"}} </option> -->
<option value="1" {{ if eq .Status 1 }}selected{{end}}>{{ T "trusted_member"}}</option>
{{ if eq .Status 2 }} <!-- just so that it shows correctly -->
<option value="2" selected>{{ T "moderator"}}</option>
{{end}}
</select>
</div>
{{ range (index $.FormErrors "status")}}
<p class="bg-danger">{{ . }}</p>
<p class="text-error">{{ . }}</p>
{{end}}
</div>
</div>

Voir le fichier

@ -26,7 +26,18 @@
<!-- Website CSS -->
<link rel="stylesheet" id="style" href="{{.URL.Parse "/css/style.css"}}">
<script type="text/javascript">
// Night mode
var night = localStorage.getItem("night");
var darkStyleLink = document.createElement('link');
darkStyleLink.id = "style-dark";
darkStyleLink.rel = "stylesheet";
darkStyleLink.type = "text/css";
darkStyleLink.href = "/css/style-night.css"
if (night == "true") {
document.getElementsByTagName("head")[0].append(darkStyleLink);
}
</script>
</head>
<body>
<nav class="navbar navbar-default" id="mainmenu">
@ -77,7 +88,6 @@
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
<script type="text/javascript" charset="utf-8" src="{{.URL.Parse "/js/main.js"}}"></script>
{{block "js_footer" .}}{{end}}
</body>
</html>

Voir le fichier

@ -5,7 +5,7 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
<title>Nyaa Pantsu - {{block "title" .}}{{ T "error_404" }}{{end}}</title>
<title>Nyaa Pantsu - {{block "title" .}}{{ T "error_404" }}{{end}}</title>
<link rel="icon" type="image/png" href="/img/favicon.png?v=3" />
<!-- RSS Feed with Context -->
@ -30,6 +30,18 @@
<!-- Website CSS -->
<link rel="stylesheet" id="style" href="{{.URL.Parse "/css/style.css"}}">
<script type="text/javascript">
// Night mode
var night = localStorage.getItem("night");
var darkStyleLink = document.createElement('link');
darkStyleLink.id = "style-dark";
darkStyleLink.rel = "stylesheet";
darkStyleLink.type = "text/css";
darkStyleLink.href = "/css/style-night.css"
if (night == "true") {
document.getElementsByTagName("head")[0].append(darkStyleLink);
}
</script>
</head>
<body>
<nav class="navbar navbar-default" id="mainmenu">

Voir le fichier

@ -1,4 +1,4 @@
{{define "title"}}{{ T "sign_in_title" }}{{end}}
{{define "title"}}{{ T "sign_in_title" }}{{end}}
{{define "contclass"}}cont-view{{end}}
{{define "content"}}
<div class="blockBody">
@ -12,20 +12,20 @@
<div class="alert alert-danger">{{ . }}</div>
{{end}}
<div class="form-group">
<input type="text" name="username" id="username" class="form-control input-lg" placeholder="{{ T "email_address_or_username"}}">
<input type="text" name="username" id="username" class="form-control input-lg" autofocus="" placeholder="{{ T "email_address_or_username"}}">
{{ range (index $.FormErrors "username")}}
<p class="bg-danger">{{ . }}</p>
<p class="text-error">{{ . }}</p>
{{end}}
</div>
<div class="form-group">
<input type="password" name="password" id="password" class="form-control input-lg" placeholder="{{ T "password"}}">
{{ range (index $.FormErrors "password")}}
<p class="bg-danger">{{ . }}</p>
<p class="text-error">{{ . }}</p>
{{end}}
</div>
<span class="button-checkbox">
<button type="button" class="btn hidden" data-color="info">{{ T "remember_me"}}</button>
<input type="checkbox" name="remember_me" id="remember_me" checked="checked">
<!-- <button type="button" class="btn hidden" data-color="info">{{ T "remember_me"}}</button>
<input type="checkbox" name="remember_me" id="remember_me" checked="checked"> -->
<a href="" class="btn btn-link pull-right">{{ T "forgot_password"}}</a>
</span>
<hr class="colorgraph">

Voir le fichier

@ -14,13 +14,13 @@
<div class="form-group">
<input type="text" name="username" id="display_name" class="form-control input-lg" placeholder="{{T "username" }}" tabindex="1" value="{{ .Username }}">
{{ range (index $.FormErrors "username")}}
<p class="bg-danger">{{ . }}</p>
<p class="text-error">{{ . }}</p>
{{end}}
</div>
<div class="form-group">
<input type="email" name="email" id="email" class="form-control input-lg" placeholder="{{T "email_address" }}" tabindex="2" value="{{ .Email }}">
{{ range (index $.FormErrors "email")}}
<p class="bg-danger">{{ . }}</p>
<p class="text-error">{{ . }}</p>
{{end}}
</div>
<div class="row">
@ -28,7 +28,7 @@
<div class="form-group">
<input type="password" name="password" id="password" class="form-control input-lg" placeholder="{{T "password" }}" tabindex="3" value="{{ .Password }}">
{{ range (index $.FormErrors "password")}}
<p class="bg-danger">{{ . }}</p>
<p class="text-error">{{ . }}</p>
{{end}}
</div>
</div>
@ -36,7 +36,7 @@
<div class="form-group">
<input type="password" name="password_confirmation" id="password_confirmation" class="form-control input-lg" placeholder="{{T "confirm_password" }}" tabindex="4">
{{ range (index $.FormErrors "password_confirmation")}}
<p class="bg-danger">{{ . }}</p>
<p class="text-error">{{ . }}</p>
{{end}}
</div>
</div>
@ -47,7 +47,7 @@
<button type="button" class="btn hidden" data-color="info" tabindex="5">{{T "i_agree" }}</button>
<input type="checkbox" name="t_and_c" id="t_and_c" value="1">
{{ range (index $.FormErrors "t_and_c")}}
<p class="bg-danger">{{ . }}</p>
<p class="text-error">{{ . }}</p>
{{end}}
</span>
</div>

Voir le fichier

@ -5,37 +5,43 @@
{{with .Torrent}}
<hr>
<div class="content" style="margin-bottom: 2em;">
<div class="row">
<div class="col-md-8">
<div class="row">
<div class="col-md-12">
<h3 style="word-break:break-all" {{if eq .Status 2}}class="remake" {{end}} {{if eq .Status 3}}class="trusted" {{end}} {{if eq .Status 4}}class="aplus"{{end}}>{{.Name}}</h3>
<div class="uploaded_by">
</div>
</div>
<div class="row">
<div class="col-md-8">
<div class="row" id="description">
<div class="col-md-12">
<div style="float: left;">
<div class="uploaded_by">
<img style="float:left; margin-right: 1em;" src="{{$.URL.Parse (printf "/img/torrents/%s.png" .SubCategory) }}">
<h4>Uploaded by <a href="{{$.URL.Parse (printf "/user/%d/-" .UploaderID) }}">{{.UploaderName}}</a></h4>
</div>
</div>
</div>
<br />
<div class="row">
<div class="col-md-12">
<a style="margin: 5px;" aria-label="Magnet Button" href="{{.Magnet}}" type="button" class="btn btn-lg btn-success download-btn">
<div style="float:right;">
<a style="margin: 5px;" aria-label="Magnet Button" href="{{.Magnet}}" type="button" class="btn btn-lg btn-success">
<span class="glyphicon glyphicon-magnet" aria-hidden="true"></span> Download!
</a>
{{if ne .TorrentLink ""}}
<a style="margin: 5px;" aria-label="Torrent file" href="{{.TorrentLink}}" type="button" class="btn btn-lg btn-success download-btn">
<a style="margin: 5px;" aria-label="Torrent file" href="{{.TorrentLink}}" type="button" class="btn btn-lg btn-success">
<span class="glyphicon glyphicon-floppy-save" aria-hidden="true"></span> Torrent file
</a>
{{end}}
<a style="margin: 5px;" aria-label="Report button" type="button" data-toggle="modal" data-target="#reportModal" class="btn btn-danger btn-lg">
<a style="margin: 5px;" aria-label="Report button" data-toggle="modal" data-target="#reportModal" class="btn btn-danger btn-sm">
<span class="glyphicon glyphicon-remove" aria-hidden="true"></span> Report!
</a>
{{ if HasAdmin $.User}}
<a href="{{ genRoute "mod_tdelete" }}?id={{ .ID }}" class="btn btn-danger btn-lg" onclick="if (!confirm('Are you sure?')) return false;"><i class="glyphicon glyphicon-trash"></i></a>
{{end}}
</div>
<div style="clear: both;"></div>
</div>
<div class="col-md-12">
<h4>{{T "description"}}</h4>
<div style="word-break:break-all;">{{.Description}}</div>
</div>
</div>
</div>
@ -73,12 +79,6 @@
<hr>
</div>
</div>
<div class="row" id="description">
<div class="col-md-12">
<h4>{{T "description"}}</h4>
<div style="word-break:break-all;">{{.Description}}</div>
</div>
</div>
<div class="row" id="comments">
<div class="col-md-12">
<h4>{{T "comments"}}</h4>

Voir le fichier

@ -590,5 +590,17 @@
{
"id": "torrent_status_remake",
"translation": "Remake"
},
{
"id": "seeders",
"translation": "Seeder"
},
{
"id": "leechers",
"translation": "Leecher"
},
{
"id": "completed",
"translation": "Komplett"
}
]

Voir le fichier

@ -606,5 +606,17 @@
{
"id":"date_format",
"translation": "2006-01-02 15:04"
},
{
"id": "seeders",
"translation": "Seeders"
},
{
"id": "leechers",
"translation": "Leechers"
},
{
"id": "completed",
"translation": "Completed"
}
]

Voir le fichier

@ -606,5 +606,21 @@
{
"id": "profile_edit_page",
"translation": "Éditer le profil de %s"
},
{
"id":"date_format",
"translation": "2006-01-02 15:04"
},
{
"id": "seeders",
"translation": "Seeders"
},
{
"id": "leechers",
"translation": "Leechers"
},
{
"id": "completed",
"translation": "Terminé"
}
]

Voir le fichier

@ -602,5 +602,21 @@
{
"id": "profile_edit_page",
"translation": "Modifica il profilo di %s"
}
},
{
"id":"date_format",
"translation": "2006-01-02 15:04"
},
{
"id": "seeders",
"translation": "Seeders"
},
{
"id": "leechers",
"translation": "Leechers"
},
{
"id": "completed",
"translation": "Completato"
}
]

Voir le fichier

@ -45,15 +45,15 @@
},
{
"id":"confirm_password",
"translation": "パスワードを確認する"
"translation": "パスワードの再入力"
},
{
"id":"i_agree",
"translation": "同意します"
"translation": "同意"
},
{
"id":"terms_conditions_confirm",
"translation": "<strong class=\"label label-primary\">登録する</strong> をクリックすることにより、Cookie の使用を含む、本サイトで規定されている <a href=\"#\" data-toggle=\"modal\" data-target=\"#t_and_c_m\"> 利用規約</a> に同意するものとします。"
"translation": "<strong class=\"label label-primary\">登録</strong> をクリックすることにより、Cookie の使用を含む、本サイトの <a href=\"#\" data-toggle=\"modal\" data-target=\"#t_and_c_m\">利用規約</a> に同意したものとみなします。"
},
{
"id":"signin",
@ -213,7 +213,7 @@
},
{
"id": "notice_keep_seeding",
"translation": "お願いDHT 機能を有効にし、なるべくシードを継続してください"
"translation": "お願い: DHT 機能を有効にし、なるべくシードを継続してください"
},
{
"id": "official_nyaapocalipse_faq",
@ -289,7 +289,7 @@
},
{
"id": "magnet_link_should_look_like",
"translation": "magnet リンクは次のようになっているはずです"
"translation": "magnet リンクは次のようになっているはずです:"
},
{
"id": "which_trackers_do_you_recommend",
@ -297,7 +297,7 @@
},
{
"id": "answer_which_trackers_do_you_recommend",
"translation": "トラッカーに Torrent のアップロードを拒否された場合は、この中のいくつかを追加する必要があります"
"translation": "トラッカーに Torrent のアップロードを拒否された場合は、この中のいくつかを追加する必要があります:"
},
{
"id": "how_can_i_help",
@ -321,7 +321,7 @@
},
{
"id": "nyaa_pantsu_dont_host_files",
"translation": " nyaa.pantsu.cat と sukebei.pantsu.cat はいかなるファイルもホストしていません。"
"translation": "nyaa.pantsu.cat と sukebei.pantsu.cat はいかなるファイルもホストしていません。"
},
{
"id": "upload_magnet",
@ -449,7 +449,7 @@
},
{
"id": "filter_remakes",
"translation": "リメイクされたフィルター"
"translation": "再構成されたフィルター"
},
{
"id": "trusted",
@ -550,5 +550,49 @@
{
"id": "delete_success",
"translation": "アカウントが削除されました。"
},
{
"id": "moderation",
"translation": "緩和"
},
{
"id": "who_is_renchon",
"translation": "「れんちょん」って誰やねん"
},
{
"id": "renchon_anon_explanation",
"translation": "「れんちょん」は匿名でアップロードもしくはコメントを書き込んだ際に割り当てられるユーザー名なのん。オリジナルのアップロード者と一緒に表示されることがあるけど、オリジナルの nyaa からインポートされた Torrent にも使用されるのん。"
},
{
"id": "mark_as_remake",
"translation": "再構成としてマーク"
},
{
"id": "email_changed",
"translation": "メールアドレスが変更されました。なお、送信されたリンクをクリックして認証を済ませる必要があります: %s"
},
{
"id": "torrent_status",
"translation": "Torrent の状態"
},
{
"id": "torrent_status_hidden",
"translation": "非表示"
},
{
"id": "torrent_status_normal",
"translation": "通常"
},
{
"id": "torrent_status_remake",
"translation": "再構成"
},
{
"id": "profile_edit_page",
"translation": "%s のプロフィールを編集"
},
{
"id":"date_format",
"translation": "2006/01/02 15:04"
}
]

Voir le fichier

@ -602,5 +602,21 @@
{
"id": "profile_edit_page",
"translation": "แก้ไขโปรไฟล์ของ %s"
},
{
"id":"date_format",
"translation": "2006/01/02 15:04"
},
{
"id": "seeders",
"translation": "ผู้ปล่อย"
},
{
"id": "leechers",
"translation": "ผู้โหลด"
},
{
"id": "completed",
"translation": "โหลดเสร็จแล้ว"
}
]

Voir le fichier

@ -2,8 +2,8 @@ package languages
import (
"fmt"
"github.com/nicksnyder/go-i18n/i18n"
"github.com/ewhal/nyaa/service/user"
"github.com/nicksnyder/go-i18n/i18n"
"html/template"
"net/http"
)
@ -22,7 +22,7 @@ func TfuncWithFallback(language string, languages ...string) (i18n.TranslateFunc
if err1 != nil && err2 != nil {
// fallbackT is still a valid function even with the error, it returns translationID.
return fallbackT, err2;
return fallbackT, err2
}
return func(translationID string, args ...interface{}) string {
@ -42,7 +42,7 @@ func GetAvailableLanguages() (languages map[string]string) {
/* Translation files should have an ID with the translated language name.
If they don't, just use the languageTag */
if languageName := T("language_name"); languageName != "language_name" {
languages[languageTag] = languageName;
languages[languageTag] = languageName
} else {
languages[languageTag] = languageTag
}
@ -67,7 +67,7 @@ func SetTranslationFromRequest(tmpl *template.Template, r *http.Request, default
userLanguage := ""
user, _, err := userService.RetrieveCurrentUser(r)
if err == nil {
userLanguage = user.Language;
userLanguage = user.Language
}
cookie, err := r.Cookie("lang")
@ -78,5 +78,6 @@ func SetTranslationFromRequest(tmpl *template.Template, r *http.Request, default
// go-i18n supports the format of the Accept-Language header, thankfully.
headerLanguage := r.Header.Get("Accept-Language")
r.Header.Add("Vary", "Accept-Encoding")
return SetTranslation(tmpl, userLanguage, cookieLanguage, headerLanguage, defaultLanguage)
}

Voir le fichier

@ -9,6 +9,7 @@ import (
"github.com/ewhal/nyaa/cache"
"github.com/ewhal/nyaa/common"
"github.com/ewhal/nyaa/config"
"github.com/ewhal/nyaa/db"
"github.com/ewhal/nyaa/model"
"github.com/ewhal/nyaa/service"
@ -16,6 +17,18 @@ import (
"github.com/ewhal/nyaa/util/log"
)
var searchOperator string
func Configure(conf *config.SearchConfig) (err error) {
// SQLite has case-insensitive LIKE, but no ILIKE
if db.ORM.Dialect().GetName() == "sqlite3" {
searchOperator = "LIKE ?"
} else {
searchOperator = "ILIKE ?"
}
return
}
func SearchByQuery(r *http.Request, pagenum int) (search common.SearchParam, tor []model.Torrent, count int, err error) {
search, tor, count, err = searchByQuery(r, pagenum, true)
return
@ -40,7 +53,7 @@ func searchByQuery(r *http.Request, pagenum int, countAll bool) (
search.Page = pagenum
search.Query = r.URL.Query().Get("q")
userID, _ := strconv.Atoi(r.URL.Query().Get("userID"))
search.UserID = uint(userID)
search.UserID = uint(userID)
switch s := r.URL.Query().Get("s"); s {
case "1":
@ -75,22 +88,36 @@ func searchByQuery(r *http.Request, pagenum int, countAll bool) (
case "1":
search.Sort = common.Name
orderBy += "torrent_name"
break
case "2":
search.Sort = common.Date
orderBy += "date"
break
case "3":
search.Sort = common.Downloads
orderBy += "downloads"
break
case "4":
search.Sort = common.Size
orderBy += "filesize"
break
case "5":
search.Sort = common.Seeders
orderBy += "seeders"
search.NotNull += "seeders IS NOT NULL "
break
case "6":
search.Sort = common.Leechers
orderBy += "leechers"
search.NotNull += "leechers IS NOT NULL "
break
case "7":
search.Sort = common.Completed
orderBy += "completed"
search.NotNull += "completed IS NOT NULL "
break
default:
search.Sort = common.ID
orderBy += "torrent_id"
}
@ -103,68 +130,68 @@ func searchByQuery(r *http.Request, pagenum int, countAll bool) (
default:
orderBy += "desc"
}
parameters := serviceBase.WhereParams{
Params: make([]interface{}, 0, 64),
}
conditions := make([]string, 0, 64)
tor, count, err = cache.Get(search, func() (tor []model.Torrent, count int, err error) {
parameters := serviceBase.WhereParams{
Params: make([]interface{}, 0, 64),
if search.Category.Main != 0 {
conditions = append(conditions, "category = ?")
parameters.Params = append(parameters.Params, string(catString[0]))
}
if search.UserID != 0 {
conditions = append(conditions, "uploader = ?")
parameters.Params = append(parameters.Params, search.UserID)
}
if search.Category.Sub != 0 {
conditions = append(conditions, "sub_category = ?")
parameters.Params = append(parameters.Params, string(catString[2]))
}
if search.Status != 0 {
if search.Status == common.FilterRemakes {
conditions = append(conditions, "status <> ?")
} else {
conditions = append(conditions, "status >= ?")
}
conditions := make([]string, 0, 64)
if search.Category.Main != 0 {
conditions = append(conditions, "category = ?")
parameters.Params = append(parameters.Params, string(catString[0]))
}
if search.UserID != 0 {
conditions = append(conditions, "uploader = ?")
parameters.Params = append(parameters.Params, search.UserID)
}
if search.Category.Sub != 0 {
conditions = append(conditions, "sub_category = ?")
parameters.Params = append(parameters.Params, string(catString[2]))
}
if search.Status != 0 {
if search.Status == common.FilterRemakes {
conditions = append(conditions, "status > ?")
} else {
conditions = append(conditions, "status >= ?")
}
parameters.Params = append(parameters.Params, strconv.Itoa(int(search.Status)+1))
parameters.Params = append(parameters.Params, strconv.Itoa(int(search.Status)+1))
}
if len(search.NotNull) > 0 {
conditions = append(conditions, search.NotNull)
}
if len(search.NotNull) > 0 {
conditions = append(conditions, search.NotNull)
}
searchQuerySplit := strings.Fields(search.Query)
for i, word := range searchQuerySplit {
firstRune, _ := utf8.DecodeRuneInString(word)
if len(word) == 1 && unicode.IsPunct(firstRune) {
// some queries have a single punctuation character
// which causes a full scan instead of using the index
// and yields no meaningful results.
// due to len() == 1 we're just looking at 1-byte/ascii
// punctuation characters.
continue
}
searchQuerySplit := strings.Fields(search.Query)
for i, word := range searchQuerySplit {
firstRune, _ := utf8.DecodeRuneInString(word)
if len(word) == 1 && unicode.IsPunct(firstRune) {
// some queries have a single punctuation character
// which causes a full scan instead of using the index
// and yields no meaningful results.
// due to len() == 1 we're just looking at 1-byte/ascii
// punctuation characters.
continue
}
// TODO: make this faster ?
conditions = append(conditions, "torrent_name "+searchOperator)
parameters.Params = append(parameters.Params, "%"+searchQuerySplit[i]+"%")
}
// SQLite has case-insensitive LIKE, but no ILIKE
var operator string
if db.ORM.Dialect().GetName() == "sqlite3" {
operator = "LIKE ?"
} else {
operator = "ILIKE ?"
}
parameters.Conditions = strings.Join(conditions[:], " AND ")
// TODO: make this faster ?
conditions = append(conditions, "torrent_name "+operator)
parameters.Params = append(parameters.Params, "%"+searchQuerySplit[i]+"%")
}
log.Infof("SQL query is :: %s\n", parameters.Conditions)
tor, count, err = cache.Impl.Get(search, func() (tor []model.Torrent, count int, err error) {
parameters.Conditions = strings.Join(conditions[:], " AND ")
log.Infof("SQL query is :: %s\n", parameters.Conditions)
if countAll {
tor, count, err = torrentService.GetTorrentsOrderBy(&parameters, orderBy, int(search.Max), int(search.Max)*(search.Page-1))
} else {
tor, err = torrentService.GetTorrentsOrderByNoCount(&parameters, orderBy, int(search.Max), int(search.Max)*(search.Page-1))
}
return
})
return
}
}