révision
63e870c0bb
|
@ -10,7 +10,8 @@ that anyone will be able to deploy locally or remotely.
|
|||
* Golang
|
||||
|
||||
# Installation
|
||||
* Install [Golang](https://golang.org/doc/install)
|
||||
Ubuntu 17.04 fails to build, use a different OS or docker
|
||||
* Install [Golang](https://golang.org/doc/install) (version >=1.8)
|
||||
* `go get github.com/ewhal/nyaa`
|
||||
* `go build`
|
||||
* Download DB and place it in your root folder named as "nyaa.db"
|
||||
|
@ -64,11 +65,13 @@ Access the website by going to [localhost:9999](http://localhost:9999).
|
|||
> nyaa_psql.backup.
|
||||
|
||||
## TODO
|
||||
* Remove gorm
|
||||
* Use elastic search or sphinix search
|
||||
* 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
|
||||
|
||||
|
|
4
cache/cache.go
externe
4
cache/cache.go
externe
|
@ -7,8 +7,6 @@ import (
|
|||
"github.com/ewhal/nyaa/common"
|
||||
"github.com/ewhal/nyaa/config"
|
||||
"github.com/ewhal/nyaa/model"
|
||||
|
||||
"errors"
|
||||
)
|
||||
|
||||
// Cache defines interface for caching search results
|
||||
|
@ -17,8 +15,6 @@ type Cache interface {
|
|||
ClearAll()
|
||||
}
|
||||
|
||||
var ErrInvalidCacheDialect = errors.New("invalid cache dialect")
|
||||
|
||||
// Impl cache implementation instance
|
||||
var Impl Cache
|
||||
|
||||
|
|
8
cache/native/native.go
externe
8
cache/native/native.go
externe
|
@ -111,7 +111,9 @@ func (n *NativeCache) updateUsedSize(delta int) {
|
|||
}
|
||||
s := n.ll.Remove(e).(*store)
|
||||
delete(n.cache, s.key)
|
||||
s.Lock()
|
||||
n.totalUsed -= s.size
|
||||
s.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -136,8 +138,6 @@ func (s *store) update(data []model.Torrent, count int) {
|
|||
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)
|
||||
// In a separate goroutine, to ensure there is never any lock intersection
|
||||
go s.n.updateUsedSize(delta)
|
||||
}
|
||||
|
|
37
cache/native/native_test.go
externe
Fichier normal
37
cache/native/native_test.go
externe
Fichier normal
|
@ -0,0 +1,37 @@
|
|||
package native
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/ewhal/nyaa/common"
|
||||
"github.com/ewhal/nyaa/model"
|
||||
)
|
||||
|
||||
// Basic test for deadlocks and race conditions
|
||||
func TestConcurrency(t *testing.T) {
|
||||
c := New(0.000001)
|
||||
|
||||
fn := func() ([]model.Torrent, int, error) {
|
||||
return []model.Torrent{{}, {}, {}}, 10, nil
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(300)
|
||||
for i := 0; i < 3; i++ {
|
||||
go func() {
|
||||
for j := 0; j < 100; j++ {
|
||||
go func(j int) {
|
||||
defer wg.Done()
|
||||
k := common.SearchParam{
|
||||
Page: j,
|
||||
}
|
||||
if _, _, err := c.Get(k, fn); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}(j)
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
|
@ -16,4 +16,4 @@ const (
|
|||
EmailTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
var EmailTokenHashKey = []byte("CHANGE_THIS_BEFORE_DEPLOYING_YOU_RETARD")
|
||||
var EmailTokenHashKey = []byte("CHANGE_THIS_BEFORE_DEPLOYING_YOU_GIT")
|
||||
|
|
|
@ -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"}
|
||||
|
|
4
main.go
4
main.go
|
@ -38,8 +38,8 @@ func RunServer(conf *config.Config) {
|
|||
|
||||
// Set up server,
|
||||
srv := &http.Server{
|
||||
WriteTimeout: 15 * time.Second,
|
||||
ReadTimeout: 15 * time.Second,
|
||||
WriteTimeout: 24 * time.Second,
|
||||
ReadTimeout: 8 * time.Second,
|
||||
}
|
||||
l, err := network.CreateHTTPListener(conf)
|
||||
log.CheckError(err)
|
||||
|
|
|
@ -29,8 +29,30 @@ type TorrentReportJson struct {
|
|||
|
||||
/* Model Conversion to Json */
|
||||
|
||||
func getReportDescription(d string) string {
|
||||
if d == "illegal" {
|
||||
return "Illegal content"
|
||||
} else if d == "spam" {
|
||||
return "Spam / Garbage"
|
||||
} else if d == "wrongcat" {
|
||||
return "Wrong category"
|
||||
} else if d == "dup" {
|
||||
return "Duplicate / Deprecated"
|
||||
}
|
||||
return "???"
|
||||
}
|
||||
|
||||
func (report *TorrentReport) ToJson() TorrentReportJson {
|
||||
json := TorrentReportJson{report.ID, report.Description, report.Torrent.ToJSON(), report.User.ToJSON()}
|
||||
// FIXME: report.Torrent and report.User should never be nil
|
||||
var t TorrentJSON = TorrentJSON{}
|
||||
if report.Torrent != nil {
|
||||
t = report.Torrent.ToJSON()
|
||||
}
|
||||
var u UserJSON = UserJSON{}
|
||||
if report.User != nil {
|
||||
u = report.User.ToJSON()
|
||||
}
|
||||
json := TorrentReportJson{report.ID, getReportDescription(report.Description), t, u}
|
||||
return json
|
||||
}
|
||||
|
||||
|
|
|
@ -12,8 +12,8 @@ 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"`
|
||||
ApiToken string `gorm:"column:api_token"`
|
||||
ApiTokenExpiry time.Time `gorm:"column:api_token_expiry"`
|
||||
Language string `gorm:"column:language"`
|
||||
|
||||
// TODO: move this to PublicUser
|
||||
|
@ -42,7 +42,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.ApiToken) + len(u.MD5) + len(u.Language)
|
||||
s *= 8
|
||||
|
||||
// Ignoring foreign key users. Fuck them.
|
||||
|
|
|
@ -345,6 +345,12 @@ footer {
|
|||
text-shadow: -1px -1px #999999;
|
||||
}
|
||||
|
||||
select#bottom_language_selector {
|
||||
width: 20%;
|
||||
margin: auto;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
/* Force images on description to fit width */
|
||||
#description img {
|
||||
display: block;
|
||||
|
|
|
@ -47,3 +47,36 @@ for(var i in list) {
|
|||
var date = new Date(e.innerText);
|
||||
e.innerText = date.toDateString() + " " + date.toLocaleTimeString();
|
||||
}
|
||||
|
||||
function loadLanguages() {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState == 4 && xhr.status == 200) {
|
||||
var selector = document.getElementById("bottom_language_selector");
|
||||
selector.hidden = false
|
||||
/* Response format is
|
||||
* { "current": "(user current language)",
|
||||
* "languages": {
|
||||
* "(language_code)": "(language_name"),
|
||||
* }} */
|
||||
var response = JSON.parse(xhr.responseText);
|
||||
for (var language in response.languages) {
|
||||
if (!response.languages.hasOwnProperty(language)) continue;
|
||||
|
||||
var opt = document.createElement("option")
|
||||
opt.value = language
|
||||
opt.innerHTML = response.languages[language]
|
||||
if (language == response.current) {
|
||||
opt.selected = true
|
||||
}
|
||||
|
||||
selector.appendChild(opt)
|
||||
}
|
||||
}
|
||||
}
|
||||
xhr.open("GET", "/language?format=json", true)
|
||||
xhr.send()
|
||||
}
|
||||
|
||||
loadLanguages();
|
||||
|
||||
|
|
62
router/changeLanguageHandler.go
Fichier normal
62
router/changeLanguageHandler.go
Fichier normal
|
@ -0,0 +1,62 @@
|
|||
package router;
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/ewhal/nyaa/util/languages"
|
||||
"github.com/ewhal/nyaa/service/user"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
type LanguagesJSONResponse struct {
|
||||
Current string `json:"current"`
|
||||
Languages map[string]string `json:"languages"`
|
||||
}
|
||||
|
||||
func SeeLanguagesHandler(w http.ResponseWriter, r *http.Request) {
|
||||
_, Tlang := languages.GetTfuncAndLanguageFromRequest(r, "en-us")
|
||||
availableLanguages := languages.GetAvailableLanguages()
|
||||
|
||||
format := r.URL.Query().Get("format")
|
||||
if format == "json" {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
err := json.NewEncoder(w).Encode(LanguagesJSONResponse{Tlang.Tag, availableLanguages})
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
clv := ChangeLanguageVariables{NewSearchForm(), Navigation{}, Tlang.Tag, availableLanguages, GetUser(r), r.URL, mux.CurrentRoute(r)}
|
||||
languages.SetTranslationFromRequest(changeLanguageTemplate, r, "en-us")
|
||||
err := changeLanguageTemplate.ExecuteTemplate(w, "index.html", clv)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ChangeLanguageHandler(w http.ResponseWriter, r *http.Request) {
|
||||
lang := r.FormValue("language")
|
||||
|
||||
availableLanguages := languages.GetAvailableLanguages()
|
||||
if _, exists := availableLanguages[lang]; !exists {
|
||||
http.Error(w, "Language not available", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// If logged in, update user language; if not, set cookie.
|
||||
user, err := userService.CurrentUser(r)
|
||||
if err == nil {
|
||||
user.Language = lang
|
||||
// I don't know if I should use this...
|
||||
userService.UpdateUserCore(&user)
|
||||
} else {
|
||||
http.SetCookie(w, &http.Cookie{Name: "lang", Value: lang})
|
||||
}
|
||||
|
||||
url, _ := Router.Get("home").URL()
|
||||
http.Redirect(w, r, url.String(), http.StatusSeeOther)
|
||||
}
|
||||
|
|
@ -32,8 +32,9 @@ func IndexModPanel(w http.ResponseWriter, r *http.Request) {
|
|||
torrentReports, _, _ := reportService.GetAllTorrentReports(offset, 0)
|
||||
|
||||
languages.SetTranslationFromRequest(panelIndex, r, "en-us")
|
||||
htv := PanelIndexVbs{torrents, torrentReports, users, comments, NewSearchForm(), currentUser, r.URL}
|
||||
_ = panelIndex.ExecuteTemplate(w, "admin_index.html", htv)
|
||||
htv := PanelIndexVbs{torrents, model.TorrentReportsToJSON(torrentReports), users, comments, NewSearchForm(), currentUser, r.URL}
|
||||
err := panelIndex.ExecuteTemplate(w, "admin_index.html", htv)
|
||||
log.CheckError(err)
|
||||
} else {
|
||||
http.Error(w, "admins only", http.StatusForbidden)
|
||||
}
|
||||
|
@ -217,7 +218,8 @@ func TorrentPostEditModPanel(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
languages.SetTranslationFromRequest(panelTorrentEd, r, "en-us")
|
||||
htv := PanelTorrentEdVbs{uploadForm, NewSearchForm(), currentUser, err, infos, r.URL}
|
||||
_ = panelTorrentEd.ExecuteTemplate(w, "admin_index.html", htv)
|
||||
err_ := panelTorrentEd.ExecuteTemplate(w, "admin_index.html", htv)
|
||||
log.CheckError(err_)
|
||||
}
|
||||
|
||||
func CommentDeleteModPanel(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
|
@ -112,5 +112,8 @@ func init() {
|
|||
//Router.HandleFunc("/moderation/report/delete", gzipTorrentReportDeleteHandler).Name("torrent_report_delete").Methods("POST")
|
||||
//Router.HandleFunc("/moderation/torrent/delete", gzipTorrentDeleteHandler).Name("torrent_delete").Methods("POST")
|
||||
|
||||
Router.HandleFunc("/language", SeeLanguagesHandler).Methods("GET").Name("see_languages")
|
||||
Router.HandleFunc("/language", ChangeLanguageHandler).Methods("POST").Name("change_language")
|
||||
|
||||
Router.NotFoundHandler = http.HandlerFunc(NotFoundHandler)
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ import (
|
|||
"github.com/ewhal/nyaa/util/search"
|
||||
"github.com/gorilla/feeds"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
@ -31,7 +30,7 @@ func RSSHandler(w http.ResponseWriter, r *http.Request) {
|
|||
for i, torrent := range torrents {
|
||||
torrentJSON := torrent.ToJSON()
|
||||
feed.Items[i] = &feeds.Item{
|
||||
Id: "https://" + config.WebAddress + "/view/" + strconv.FormatUint(uint64(torrents[i].ID), 10),
|
||||
Id: "https://" + config.WebAddress + "/view/" + torrentJSON.ID,
|
||||
Title: torrent.Name,
|
||||
Link: &feeds.Link{Href: string(torrentJSON.Magnet)},
|
||||
Description: string(torrentJSON.Description),
|
||||
|
@ -43,11 +42,11 @@ func RSSHandler(w http.ResponseWriter, r *http.Request) {
|
|||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
rss, rssErr := feed.ToRss()
|
||||
if rssErr != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
http.Error(w, rssErr.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
_, writeErr := w.Write([]byte(rss))
|
||||
if writeErr != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
http.Error(w, writeErr.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
|
||||
var TemplateDir = "templates"
|
||||
|
||||
var homeTemplate, searchTemplate, faqTemplate, uploadTemplate, viewTemplate, viewRegisterTemplate, viewLoginTemplate, viewRegisterSuccessTemplate, viewVerifySuccessTemplate, viewProfileTemplate, viewProfileEditTemplate, viewUserDeleteTemplate, notFoundTemplate *template.Template
|
||||
var homeTemplate, searchTemplate, faqTemplate, uploadTemplate, viewTemplate, viewRegisterTemplate, viewLoginTemplate, viewRegisterSuccessTemplate, viewVerifySuccessTemplate, viewProfileTemplate, viewProfileEditTemplate, viewUserDeleteTemplate, notFoundTemplate, changeLanguageTemplate *template.Template
|
||||
|
||||
var panelIndex, panelTorrentList, panelUserList, panelCommentList, panelTorrentEd, panelTorrentReportList *template.Template
|
||||
|
||||
|
@ -86,6 +86,11 @@ func ReloadTemplates() {
|
|||
name: "404",
|
||||
file: "404.html",
|
||||
},
|
||||
templateLoader{
|
||||
templ: &changeLanguageTemplate,
|
||||
name: "change_language",
|
||||
file: "change_language.html",
|
||||
},
|
||||
}
|
||||
for idx := range pubTempls {
|
||||
pubTempls[idx].indexFile = filepath.Join(TemplateDir, "index.html")
|
||||
|
|
|
@ -19,10 +19,20 @@ var FuncMap = template.FuncMap{
|
|||
}
|
||||
return "error"
|
||||
},
|
||||
"genRouteWithQuery": func(name string, currentUrl *url.URL, params ...string) template.HTML {
|
||||
"genRouteWithQuery": func(name string, currentUrl *url.URL, params ...string) template.URL {
|
||||
url, err := Router.Get(name).URL(params...)
|
||||
if err == nil {
|
||||
return template.HTML(template.HTMLEscapeString(url.String() + "?" + currentUrl.RawQuery)) // TODO: Review application of character escaping
|
||||
return template.URL(url.String() + "?" + currentUrl.RawQuery)
|
||||
}
|
||||
return "error"
|
||||
},
|
||||
"genViewTorrentRoute": func(torrent_id uint) string {
|
||||
// Helper for when you have an uint while genRoute("view_torrent", ...) takes a string
|
||||
// FIXME better solution?
|
||||
s := strconv.FormatUint(uint64(torrent_id), 10)
|
||||
url, err := Router.Get("view_torrent").URL("id", s)
|
||||
if err == nil {
|
||||
return url.String()
|
||||
}
|
||||
return "error"
|
||||
},
|
||||
|
@ -69,6 +79,7 @@ var FuncMap = template.FuncMap{
|
|||
"CurrentOrAdmin": userPermission.CurrentOrAdmin,
|
||||
"CurrentUserIdentical": userPermission.CurrentUserIdentical,
|
||||
"HasAdmin": userPermission.HasAdmin,
|
||||
"NeedsCaptcha": userPermission.NeedsCaptcha,
|
||||
"GetRole": userPermission.GetRole,
|
||||
"IsFollower": userPermission.IsFollower,
|
||||
"NoEncode": func(str string) template.HTML {
|
||||
|
|
|
@ -6,7 +6,6 @@ import (
|
|||
|
||||
"github.com/ewhal/nyaa/common"
|
||||
"github.com/ewhal/nyaa/model"
|
||||
"github.com/ewhal/nyaa/service/captcha"
|
||||
"github.com/ewhal/nyaa/service/user"
|
||||
userForms "github.com/ewhal/nyaa/service/user/form"
|
||||
"github.com/gorilla/mux"
|
||||
|
@ -36,7 +35,7 @@ type NotFoundTemplateVariables struct {
|
|||
|
||||
type ViewTemplateVariables struct {
|
||||
Torrent model.TorrentJSON
|
||||
Captcha captcha.Captcha
|
||||
CaptchaID string
|
||||
Search SearchForm
|
||||
Navigation Navigation
|
||||
User *model.User
|
||||
|
@ -114,11 +113,22 @@ type UploadTemplateVariables struct {
|
|||
Route *mux.Route
|
||||
}
|
||||
|
||||
type ChangeLanguageVariables struct {
|
||||
Search SearchForm
|
||||
Navigation Navigation
|
||||
Language string
|
||||
Languages map[string]string
|
||||
User *model.User
|
||||
URL *url.URL
|
||||
Route *mux.Route
|
||||
}
|
||||
|
||||
|
||||
/* MODERATION Variables */
|
||||
|
||||
type PanelIndexVbs struct {
|
||||
Torrents []model.Torrent
|
||||
TorrentReports []model.TorrentReport
|
||||
TorrentReports []model.TorrentReportJson
|
||||
Users []model.User
|
||||
Comments []model.Comment
|
||||
Search SearchForm
|
||||
|
|
|
@ -17,7 +17,6 @@ import (
|
|||
|
||||
"github.com/ewhal/nyaa/cache"
|
||||
"github.com/ewhal/nyaa/config"
|
||||
"github.com/ewhal/nyaa/service/captcha"
|
||||
"github.com/ewhal/nyaa/service/upload"
|
||||
"github.com/ewhal/nyaa/util"
|
||||
"github.com/ewhal/nyaa/util/metainfo"
|
||||
|
@ -33,7 +32,7 @@ type UploadForm struct {
|
|||
Remake bool
|
||||
Description string
|
||||
Status int
|
||||
captcha.Captcha
|
||||
CaptchaID string
|
||||
|
||||
Infohash string
|
||||
CategoryID int
|
||||
|
@ -84,12 +83,6 @@ func (f *UploadForm) ExtractInfo(r *http.Request) error {
|
|||
f.Status, _ = strconv.Atoi(r.FormValue(UploadFormStatus))
|
||||
f.Magnet = r.FormValue(UploadFormMagnet)
|
||||
f.Remake = r.FormValue(UploadFormRemake) == "on"
|
||||
f.Captcha = captcha.Extract(r)
|
||||
|
||||
if !captcha.Authenticate(f.Captcha) {
|
||||
// TODO: Prettier passing of mistyped Captcha errors
|
||||
return errors.New(captcha.ErrInvalidCaptcha.Error())
|
||||
}
|
||||
|
||||
// trim whitespace
|
||||
f.Name = util.TrimWhitespaces(f.Name)
|
||||
|
@ -189,7 +182,7 @@ func (f *UploadForm) ExtractInfo(r *http.Request) error {
|
|||
}
|
||||
hash16 := make([]byte, hex.EncodedLen(len(data)))
|
||||
hex.Encode(hash16, data)
|
||||
f.Infohash = string(hash16)
|
||||
f.Infohash = strings.ToUpper(string(hash16))
|
||||
}
|
||||
|
||||
f.Filesize = 0
|
||||
|
|
|
@ -10,7 +10,7 @@ import (
|
|||
"github.com/ewhal/nyaa/db"
|
||||
"github.com/ewhal/nyaa/model"
|
||||
"github.com/ewhal/nyaa/service/captcha"
|
||||
"github.com/ewhal/nyaa/service/user"
|
||||
"github.com/ewhal/nyaa/service/user/permission"
|
||||
"github.com/ewhal/nyaa/util/languages"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
@ -23,26 +23,32 @@ func UploadHandler(w http.ResponseWriter, r *http.Request) {
|
|||
var uploadForm UploadForm
|
||||
if r.Method == "POST" {
|
||||
defer r.Body.Close()
|
||||
user := GetUser(r)
|
||||
if userPermission.NeedsCaptcha(user) {
|
||||
userCaptcha := captcha.Extract(r)
|
||||
if !captcha.Authenticate(userCaptcha) {
|
||||
http.Error(w, captcha.ErrInvalidCaptcha.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// validation is done in ExtractInfo()
|
||||
err := uploadForm.ExtractInfo(r)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
user, _, err := userService.RetrieveCurrentUser(r)
|
||||
if err != nil {
|
||||
fmt.Printf("error %+v\n", err)
|
||||
}
|
||||
status := 1 // normal
|
||||
if uploadForm.Remake { // overrides trusted
|
||||
status = 2
|
||||
} else if user.Status == 1 {
|
||||
status = 3 // mark as trusted if user is trusted
|
||||
}
|
||||
var sameTorrents int
|
||||
db.ORM.Model(&model.Torrent{}).Where("torrent_hash = ?", uploadForm.Infohash).Count(&sameTorrents)
|
||||
if (sameTorrents == 0) {
|
||||
//add to db and redirect depending on result
|
||||
|
||||
var sameTorrents int
|
||||
db.ORM.Model(&model.Torrent{}).Where("torrent_hash = ?", uploadForm.Infohash).Count(&sameTorrents)
|
||||
if (sameTorrents == 0) {
|
||||
// add to db and redirect
|
||||
torrent := model.Torrent{
|
||||
Name: uploadForm.Name,
|
||||
Category: uploadForm.CategoryID,
|
||||
|
@ -54,7 +60,6 @@ func UploadHandler(w http.ResponseWriter, r *http.Request) {
|
|||
Description: uploadForm.Description,
|
||||
UploaderID: user.ID}
|
||||
db.ORM.Create(&torrent)
|
||||
fmt.Printf("%+v\n", torrent)
|
||||
url, err := Router.Get("view_torrent").URL("id", strconv.FormatUint(uint64(torrent.ID), 10))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
@ -66,7 +71,14 @@ func UploadHandler(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
} else if r.Method == "GET" {
|
||||
uploadForm.CaptchaID = captcha.GetID()
|
||||
user := GetUser(r)
|
||||
if userPermission.NeedsCaptcha(user) {
|
||||
uploadForm.CaptchaID = captcha.GetID()
|
||||
} else {
|
||||
uploadForm.CaptchaID = ""
|
||||
}
|
||||
|
||||
|
||||
htv := UploadTemplateVariables{uploadForm, NewSearchForm(), Navigation{}, GetUser(r), r.URL, mux.CurrentRoute(r)}
|
||||
languages.SetTranslationFromRequest(uploadTemplate, r, "en-us")
|
||||
err := uploadTemplate.ExecuteTemplate(w, "index.html", htv)
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/ewhal/nyaa/model"
|
||||
"github.com/ewhal/nyaa/service/captcha"
|
||||
"github.com/ewhal/nyaa/service/torrent"
|
||||
"github.com/ewhal/nyaa/service/user/permission"
|
||||
"github.com/ewhal/nyaa/util"
|
||||
"github.com/ewhal/nyaa/util/languages"
|
||||
"github.com/ewhal/nyaa/util/log"
|
||||
|
@ -26,7 +27,12 @@ func ViewHandler(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
b := torrent.ToJSON()
|
||||
htv := ViewTemplateVariables{b, captcha.Captcha{CaptchaID: captcha.GetID()}, NewSearchForm(), Navigation{}, GetUser(r), r.URL, mux.CurrentRoute(r)}
|
||||
captchaID := ""
|
||||
user := GetUser(r)
|
||||
if userPermission.NeedsCaptcha(user) {
|
||||
captchaID = captcha.GetID()
|
||||
}
|
||||
htv := ViewTemplateVariables{b, captchaID, NewSearchForm(), Navigation{}, user, r.URL, mux.CurrentRoute(r)}
|
||||
|
||||
languages.SetTranslationFromRequest(viewTemplate, r, "en-us")
|
||||
err = viewTemplate.ExecuteTemplate(w, "index.html", htv)
|
||||
|
@ -39,12 +45,14 @@ func PostCommentHandler(w http.ResponseWriter, r *http.Request) {
|
|||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
userCaptcha := captcha.Extract(r)
|
||||
if !captcha.Authenticate(userCaptcha) {
|
||||
http.Error(w, "bad captcha", 403)
|
||||
return
|
||||
}
|
||||
currentUser := GetUser(r)
|
||||
if userPermission.NeedsCaptcha(currentUser) {
|
||||
userCaptcha := captcha.Extract(r)
|
||||
if !captcha.Authenticate(userCaptcha) {
|
||||
http.Error(w, "bad captcha", 403)
|
||||
return
|
||||
}
|
||||
}
|
||||
content := p.Sanitize(r.FormValue("comment"))
|
||||
|
||||
if strings.TrimSpace(content) == "" {
|
||||
|
@ -75,12 +83,14 @@ func ReportTorrentHandler(w http.ResponseWriter, r *http.Request) {
|
|||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
userCaptcha := captcha.Extract(r)
|
||||
if !captcha.Authenticate(userCaptcha) {
|
||||
http.Error(w, "bad captcha", 403)
|
||||
return
|
||||
}
|
||||
currentUser := GetUser(r)
|
||||
if userPermission.NeedsCaptcha(currentUser) {
|
||||
userCaptcha := captcha.Extract(r)
|
||||
if !captcha.Authenticate(userCaptcha) {
|
||||
http.Error(w, "bad captcha", 403)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
idNum, err := strconv.Atoi(id)
|
||||
userID := currentUser.ID
|
||||
|
|
|
@ -46,4 +46,4 @@ func (wh *wrappedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
func wrapHandler(handler http.Handler) http.Handler {
|
||||
return &wrappedHandler{handler}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -125,7 +125,7 @@ func validateHash(r *TorrentRequest) (error, int) {
|
|||
}
|
||||
hash16 := make([]byte, hex.EncodedLen(len(data)))
|
||||
hex.Encode(hash16, data)
|
||||
r.Hash = string(hash16)
|
||||
r.Hash = strings.ToUpper(string(hash16))
|
||||
}
|
||||
return nil, http.StatusOK
|
||||
}
|
||||
|
|
|
@ -48,7 +48,6 @@ func getTorrentReportsOrderBy(parameters *serviceBase.WhereParams, orderBy strin
|
|||
return
|
||||
}
|
||||
}
|
||||
// TODO: Vulnerable to injections. Use query builder. (is it?)
|
||||
|
||||
// build custom db query for performance reasons
|
||||
dbQuery := "SELECT * FROM torrent_reports"
|
||||
|
@ -63,7 +62,7 @@ func getTorrentReportsOrderBy(parameters *serviceBase.WhereParams, orderBy strin
|
|||
if limit != 0 || offset != 0 { // if limits provided
|
||||
dbQuery = dbQuery + " LIMIT " + strconv.Itoa(limit) + " OFFSET " + strconv.Itoa(offset)
|
||||
}
|
||||
err = db.ORM.Preload("Torrent").Preload("User").Raw(dbQuery, params...).Find(&torrentReports).Error //fixed !!!!
|
||||
err = db.ORM.Preload("Torrent").Preload("User").Raw(dbQuery, params...).Find(&torrentReports).Error
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -129,9 +129,10 @@ func getTorrentsOrderBy(parameters *serviceBase.WhereParams, orderBy string, lim
|
|||
if conditions != "" {
|
||||
dbQuery = dbQuery + " WHERE " + conditions
|
||||
}
|
||||
if strings.Contains(conditions, "torrent_name") {
|
||||
/* This makes all queries take roughly the same amount of time (lots)...
|
||||
if strings.Contains(conditions, "torrent_name") && offset > 0 {
|
||||
dbQuery = "WITH t AS (SELECT * FROM torrents WHERE " + conditions + ") SELECT * FROM t"
|
||||
}
|
||||
}*/
|
||||
|
||||
if orderBy == "" { // default OrderBy
|
||||
orderBy = "torrent_id DESC"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"github.com/ewhal/nyaa/util/log"
|
||||
)
|
||||
|
||||
|
||||
// HasAdmin checks that user has an admin permission.
|
||||
func HasAdmin(user *model.User) bool {
|
||||
return user.Status == 2
|
||||
|
@ -18,11 +19,16 @@ func CurrentOrAdmin(user *model.User, userID uint) bool {
|
|||
}
|
||||
|
||||
// CurrentUserIdentical check that userID is same as current user's ID.
|
||||
// TODO: Inline this
|
||||
// TODO: Inline this (won't go do this for us?)
|
||||
func CurrentUserIdentical(user *model.User, userID uint) bool {
|
||||
return user.ID == userID
|
||||
}
|
||||
|
||||
func NeedsCaptcha(user *model.User) bool {
|
||||
// Trusted members & Moderators don't
|
||||
return !(user.Status == 1 || user.Status == 2)
|
||||
}
|
||||
|
||||
func GetRole(user *model.User) string {
|
||||
switch user.Status {
|
||||
case -1:
|
||||
|
|
|
@ -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,20 +67,16 @@ 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.CreatedAt = time.Now()
|
||||
// currently unused but needs to be set:
|
||||
user.ApiToken = ""
|
||||
user.ApiTokenExpiry = time.Unix(0, 0)
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
user.CreatedAt = time.Now()
|
||||
return user, nil
|
||||
}
|
||||
|
||||
|
@ -157,17 +151,12 @@ 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 +186,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
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
@ -64,8 +63,5 @@ http://tracker.baka-sub.cf/announce</pre>
|
|||
<br />
|
||||
|
||||
<img style="max-width: 100%" src="https://my.mixtape.moe/omrskw.png" alt="funny meme">
|
||||
|
||||
<br />
|
||||
<h2>{{T "nyaa_pantsu_dont_host_files"}}</h2>
|
||||
</div>
|
||||
{{end}}
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
{{define "captcha"}}
|
||||
{{/* unset if user doesn't need captcha */}}
|
||||
{{if ne .CaptchaID ""}}
|
||||
<div class="form-group captcha-container">
|
||||
<label for="solution">Captcha</label>
|
||||
<input type="text" name="captchaID" value="{{.CaptchaID}}" hidden>
|
||||
<img src="/captcha/{{.CaptchaID}}.png">
|
||||
<input type="text" name="solution" class="form-control" placeholder="Captcha" autocomplete="off" required>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
|
|
@ -7,9 +7,12 @@
|
|||
<th class="col-xs-1">Uploader</th>
|
||||
<th class="col-xs-1">Action</th>
|
||||
</tr>
|
||||
{{ range .Torrents}}
|
||||
<tr><td><a href="{{ genRoute "mod_tedit" }}?id={{.ID}}">{{ .Name }}</a></td><td><a href="{{ genRoute "mod_tlist" }}?userID={{.UploaderID}}">{{ .UploaderID }}</a></td>
|
||||
<td><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>{{ T "delete" }}</a></td></tr>
|
||||
{{range .Torrents}}
|
||||
<tr>
|
||||
<td><a href="{{ genViewTorrentRoute .ID }}">{{ .Name }}</a> (<a href="{{ genRoute "mod_tedit" }}?id={{.ID}}">Edit</a>)</td>
|
||||
<td><a href="{{ genRoute "mod_tlist" }}?userID={{.UploaderID}}">{{ .UploaderID }}</a></td>
|
||||
<td><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>{{ T "delete" }}</a></td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</table>
|
||||
<nav class="torrentNav" aria-label="Page navigation">
|
||||
|
@ -21,7 +24,6 @@
|
|||
|
||||
<h3 id="torrents">Last Torrents Report</h3>
|
||||
<table class="table">
|
||||
{{ range .TorrentReports}}
|
||||
<tr>
|
||||
<th class="col-xs-9">Torrent Name</th>
|
||||
<th class="col-xs-1">User</th>
|
||||
|
@ -29,8 +31,13 @@
|
|||
<th class="col-xs-1">Action</th>
|
||||
</tr>
|
||||
|
||||
<tr><td><a href="{{ genRoute "mod_tedit" }}?id={{.Torrent.ID}}">{{ .Torrent.Name }}</a></td><td>{{.User.Username}}</td><td>{{.Description}}</td>
|
||||
<td><a href="{{ genRoute "mod_trdelete" }}?id={{ .ID }}" class="btn btn-danger btn-lg" onclick="if (!confirm('Are you sure?')) return false;"><i class="glyphicon glyphicon-trash"></i>{{ T "delete" }}</a></td></tr>
|
||||
{{range .TorrentReports}}
|
||||
<tr>
|
||||
<td><a href="{{ genRoute "view_torrent" "id" .Torrent.ID }}">{{ .Torrent.Name }}</a> (<a href="{{ genRoute "mod_tedit" }}?id={{.Torrent.ID}}">Edit</a>)</td>
|
||||
<td>{{.User.Username}}</td>
|
||||
<td>{{.Description}}</td>
|
||||
<td><a href="{{ genRoute "mod_trdelete" }}?id={{ .ID }}" class="btn btn-danger btn-lg" onclick="if (!confirm('Are you sure?')) return false;"><i class="glyphicon glyphicon-trash"></i>{{ T "delete" }}</a></td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</table>
|
||||
<nav class="torrentNav" aria-label="Page navigation">
|
||||
|
@ -70,7 +77,7 @@
|
|||
|
||||
{{ range .Comments}}
|
||||
|
||||
<tr><td><a href="{{ genRoute "mod_cedit" }}?id={{.ID}}">{{ .Content }}</a></td><td><a href="{{ genRoute "mod_cedit" }}?id={{.ID}}">{{ .User.Username }}</a></td>
|
||||
<tr><td><a href="{{ genRoute "mod_cedit" }}?id={{.ID}}">{{ .Content }}</a></td><td><a href="{{ genRoute "mod_cedit" }}?id={{.ID}}">{{.UserID}}</a></td>
|
||||
<td><a href="{{ genRoute "mod_cdelete" }}?id={{ .ID }}" class="btn btn-danger btn-lg" onclick="if (!confirm('Are you sure?')) return false;"><i class="glyphicon glyphicon-trash"></i>{{ T "delete" }}</a></td></tr>
|
||||
{{end}}
|
||||
</table>
|
||||
|
|
|
@ -6,16 +6,15 @@
|
|||
<th class="col-xs-1">User</th>
|
||||
<th class="col-xs-1">Reason</th>
|
||||
<th class="col-xs-1">Action</th>
|
||||
<th class="col-xs-1">Action</th>
|
||||
</tr>
|
||||
|
||||
{{ range .TorrentReports}}
|
||||
{{range .TorrentReports}}
|
||||
<tr>
|
||||
<td><a href="{{ genRoute "mod_tedit"}}?id={{.Torrent.ID}}">{{.Torrent.Name}}</a></td>
|
||||
<td><a href="{{ genRoute "view_torrent" "id" .Torrent.ID }}">{{ .Torrent.Name }}</a> (<a href="{{ genRoute "mod_tedit" }}?id={{.Torrent.ID}}">Edit</a>)</td>
|
||||
<td>{{.User.Username}}</td>
|
||||
<td>{{.Description}}</td>
|
||||
<td><a href="{{ genRoute "mod_tdelete" }}?id={{ .Torrent.ID }}">Delete</a></td>
|
||||
<td><a href="{{ genRoute "mod_trdelete" }}?id={{ .ID }}">Delete Report</a></td>
|
||||
<td><a href="{{ genRoute "mod_tdelete" }}?id={{ .Torrent.ID }}" onclick="if (!confirm('Are you sure?')) return false;">Delete</a><br />
|
||||
<a href="{{ genRoute "mod_trdelete" }}?id={{ .ID }}">Delete Report</a></td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</table>
|
||||
|
|
|
@ -10,9 +10,11 @@
|
|||
</tr>
|
||||
|
||||
{{ range .Torrents}}
|
||||
|
||||
<tr><td><a href="{{ genRoute "mod_tedit" }}?id={{.ID}}">{{ .Name }}</a></td><td><a href="{{ genRoute "mod_tlist" }}?userID={{.UploaderID}}">{{ .UploaderID }}</a></td>
|
||||
<td><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>{{ T "delete" }}</a></td></tr>
|
||||
<tr>
|
||||
<td><a href="{{ genViewTorrentRoute .ID }}">{{ .Name }}</a> (<a href="{{ genRoute "mod_tedit" }}?id={{.ID}}">Edit</a>)</td>
|
||||
<td><a href="{{ genRoute "mod_tlist" }}?userID={{.UploaderID}}">{{ .UploaderID }}</a></td>
|
||||
<td><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>{{ T "delete" }}</a></td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</table>
|
||||
<nav class="torrentNav" aria-label="Page navigation">
|
||||
|
|
24
templates/change_language.html
Fichier normal
24
templates/change_language.html
Fichier normal
|
@ -0,0 +1,24 @@
|
|||
{{define "title"}}{{T "change_language"}}{{end}}
|
||||
{{define "content"}}
|
||||
<div class="blockBody">
|
||||
<hr>
|
||||
<form role="form" method="POST">
|
||||
<div class="form-group">
|
||||
<label for="language">{{T "language"}}</label>
|
||||
<div class="ui-select">
|
||||
<select id="language" name="language" class="form-control">
|
||||
{{ $currentLanguage := .Language }}
|
||||
{{ range $tag, $translatedName := $.Languages }}
|
||||
<option value="{{ $tag }}" {{ if eq $currentLanguage $tag }}selected{{end}}>{{ $translatedName }}</option>
|
||||
{{ end }}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-success">{{T "save_changes"}}</button>
|
||||
</form>
|
||||
<div style="padding-bottom: 1em"></div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
|
|
@ -96,6 +96,13 @@
|
|||
Powered by NyaaPantsu
|
||||
</footer>
|
||||
|
||||
<form method="POST" action="{{ .URL.Parse "/language" }}" id="bottom_language_selector_form">
|
||||
<select id="bottom_language_selector" name="language" onchange="javascript:document.getElementById('bottom_language_selector_form').submit()" hidden class="form-control"></select>
|
||||
</form>
|
||||
<noscript>
|
||||
<center><a href="{{ .URL.Parse "/language" }}">{{ T "change_language" }}</a></center>
|
||||
</noscript>
|
||||
|
||||
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
|
||||
<!-- Include all compiled plugins (below), or include individual files as needed -->
|
||||
|
|
|
@ -21,7 +21,8 @@
|
|||
</div>
|
||||
<div class="form-group">
|
||||
<label for="c">{{T "category"}}</label>
|
||||
<select name="c" class="form-control input-sm">
|
||||
<select name="c" class="form-control input-sm" required>
|
||||
<option value="">{{T "select_a_torrent_category"}}</option>
|
||||
<option value="3_12" {{if eq .Category "3_12"}}selected{{end}}>{{T "anime_amv"}}</option>
|
||||
<option value="3_5" {{if eq .Category "3_5"}}selected{{end}}>{{T "anime_english_translated"}}</option>
|
||||
<option value="3_13" {{if eq .Category "3_13"}}selected{{end}}>{{T "anime_non_english_translated"}}</option>
|
||||
|
|
|
@ -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">
|
||||
|
@ -23,11 +23,11 @@
|
|||
<p class="text-error">{{ . }}</p>
|
||||
{{end}}
|
||||
</div>
|
||||
<span class="button-checkbox">
|
||||
<!-- <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">
|
||||
<a href="" class="btn btn-link pull-right">{{ T "forgot_password"}}</a>
|
||||
</span>
|
||||
</span> -->
|
||||
<hr class="colorgraph">
|
||||
<div class="row">
|
||||
<div class="col-xs-6 col-sm-6 col-md-6">
|
||||
|
|
|
@ -35,6 +35,7 @@
|
|||
|
||||
{{ 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>
|
||||
<a href="{{ genRoute "mod_tedit" }}?id={{ .ID }}" class="btn btn-warning btn-lg"><i class="glyphicon glyphicon-pencil"></i></a>
|
||||
{{end}}
|
||||
</div>
|
||||
<div style="clear: both;"></div>
|
||||
|
@ -116,7 +117,7 @@
|
|||
<label for="comment">{{ if gt .User.ID 0}} {{T "submit_a_comment_as_username" .User.Username}} {{else}} {{T "submit_a_comment_as_anonymous"}} {{end}}</label>
|
||||
<textarea name="comment" class="form-control" rows="5"></textarea>
|
||||
</div>
|
||||
{{with .Captcha}} {{block "captcha" .}}{{end}} {{end}}
|
||||
{{block "captcha" .}}{{end}}
|
||||
<button type="submit" class="btn btn-success">{{T " submit "}}</button>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -134,10 +135,12 @@
|
|||
<div class="modal-body">
|
||||
<b>Report type:</b>
|
||||
<form method="post" action="/report/{{.ID}}">
|
||||
<input type="radio" name="report_type" value="illegal"> Illegal content <br/>
|
||||
<input type="radio" name="report_type" value="spam"> Spam / garbage
|
||||
<input type="radio" name="report_type" value="illegal"> Illegal content<br />
|
||||
<input type="radio" name="report_type" value="spam"> Spam / Garbage<br />
|
||||
<input type="radio" name="report_type" value="wrongcat"> Wrong category<br />
|
||||
<input type="radio" name="report_type" value="dup"> Duplicate / Deprecated<br />
|
||||
{{end}}
|
||||
{{with .Captcha}} {{block "captcha" .}}{{end}} {{end}}
|
||||
{{block "captcha" .}}{{end}}
|
||||
<button type="submit" class="btn btn-default">Report!</button>
|
||||
</form> <br />
|
||||
</div>
|
||||
|
|
|
@ -319,10 +319,6 @@
|
|||
"id": "authors_favorite_language",
|
||||
"translation": "És el llenguatge de programació preferit de l'autor."
|
||||
},
|
||||
{
|
||||
"id": "nyaa_pantsu_dont_host_files",
|
||||
"translation": "nyaa.pantsu.cat i sukebei.pantsu.cat no allotgen cap fitxer."
|
||||
},
|
||||
{
|
||||
"id": "upload_magnet",
|
||||
"translation": "Carrega un enllaç magnètic"
|
||||
|
@ -550,5 +546,9 @@
|
|||
{
|
||||
"id": "delete_success",
|
||||
"translation": "S'ha suprimit el compte!"
|
||||
},
|
||||
{
|
||||
"id": "language_name",
|
||||
"translation": "Català"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -331,10 +331,6 @@
|
|||
"id": "authors_favorite_language",
|
||||
"translation": "Weil es die Lieblingsprogrammiersprache des Entwicklers ist."
|
||||
},
|
||||
{
|
||||
"id": "nyaa_pantsu_dont_host_files",
|
||||
"translation": "nyaa.pantsu.cat und sukebei.pantsu.cat stellen keine Dateien bereit."
|
||||
},
|
||||
{
|
||||
"id": "upload_magnet",
|
||||
"translation": "Magnet hochladen"
|
||||
|
@ -602,5 +598,9 @@
|
|||
{
|
||||
"id": "completed",
|
||||
"translation": "Komplett"
|
||||
},
|
||||
{
|
||||
"id": "language_name",
|
||||
"translation": "Deutsch"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -225,7 +225,7 @@
|
|||
},
|
||||
{
|
||||
"id": "notice_keep_seeding",
|
||||
"translation": "NOTICE: KEEP SEEDING AND ENABLE DHT YOU RETARD"
|
||||
"translation": "NOTICE: KEEP SEEDING AND ENABLE DHT YOU GIT"
|
||||
},
|
||||
{
|
||||
"id": "official_nyaapocalipse_faq",
|
||||
|
@ -273,7 +273,7 @@
|
|||
},
|
||||
{
|
||||
"id": "answer_is_sukebei_db_lost",
|
||||
"translation": "Sukebei, however might be in worse shape. Currently we only have sukebei databases up to 2016, but a newer database might be available for use."
|
||||
"translation": "Sukebei is safe too, and almost nothing is lost either."
|
||||
},
|
||||
{
|
||||
"id": "how_are_we_recovering",
|
||||
|
@ -317,7 +317,7 @@
|
|||
},
|
||||
{
|
||||
"id": "answer_how_can_i_help",
|
||||
"translation": "If you have website development expertise, you can join the #nyaapantsu IRC channel on irc.rizon.net. If you have any current databases, especially for sukebei, <b>UPLOAD THEM</b>."
|
||||
"translation": "If you have website development expertise, you can join the #nyaapantsu IRC channel on irc.rizon.net. If you have any current databases, especially for sukebei, please upload them."
|
||||
},
|
||||
{
|
||||
"id": "your_design_sucks_found_a_bug",
|
||||
|
@ -331,10 +331,6 @@
|
|||
"id": "authors_favorite_language",
|
||||
"translation": "It's the author's favorite programming language."
|
||||
},
|
||||
{
|
||||
"id": "nyaa_pantsu_dont_host_files",
|
||||
"translation": "nyaa.pantsu.cat and sukebei.pantsu.cat do not host any files."
|
||||
},
|
||||
{
|
||||
"id": "upload_magnet",
|
||||
"translation": "Upload magnet"
|
||||
|
@ -355,6 +351,10 @@
|
|||
"id": "all_categories",
|
||||
"translation": "All categories"
|
||||
},
|
||||
{
|
||||
"id": "select_a_torrent_category",
|
||||
"translation": "Select a Torrent Category"
|
||||
},
|
||||
{
|
||||
"id": "anime",
|
||||
"translation": "Anime"
|
||||
|
@ -618,5 +618,13 @@
|
|||
{
|
||||
"id": "completed",
|
||||
"translation": "Completed"
|
||||
},
|
||||
{
|
||||
"id": "change_language",
|
||||
"translation": "Change Language"
|
||||
},
|
||||
{
|
||||
"id": "language_name",
|
||||
"translation": "English"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -315,10 +315,6 @@
|
|||
"id": "authors_favorite_language",
|
||||
"translation": "Es el lenguaje de programación favorito del autor."
|
||||
},
|
||||
{
|
||||
"id": "nyaa_pantsu_dont_host_files",
|
||||
"translation": "nyaa.pantsu.cat y sukebei.pantsu.cat no hospedan archivos."
|
||||
},
|
||||
{
|
||||
"id": "upload_magnet",
|
||||
"translation": "Subir Magnet"
|
||||
|
@ -546,5 +542,9 @@
|
|||
{
|
||||
"id": "delete_success",
|
||||
"translation": "Se ha eliminado exitosamente la cuenta!"
|
||||
},
|
||||
{
|
||||
"id": "language_name",
|
||||
"translation": "Español"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -331,10 +331,6 @@
|
|||
"id": "authors_favorite_language",
|
||||
"translation": "C'est le langage de programmation favori du développeur."
|
||||
},
|
||||
{
|
||||
"id": "nyaa_pantsu_dont_host_files",
|
||||
"translation": "nyaa.pantsu.cat and sukebei.pantsu.cat n'hébergent aucun fichier."
|
||||
},
|
||||
{
|
||||
"id": "upload_magnet",
|
||||
"translation": "Uploader un magnet"
|
||||
|
@ -345,7 +341,7 @@
|
|||
},
|
||||
{
|
||||
"id": "uploading_file_prefills_fields",
|
||||
"translation": "Il est recommandé d'uploader un fichier torrent afin de pré-remplir certains champs."
|
||||
"translation": "Il est recommandé d'uploader un fichier torrent afin de préremplir certains champs."
|
||||
},
|
||||
{
|
||||
"id": "magnet_link",
|
||||
|
@ -622,5 +618,9 @@
|
|||
{
|
||||
"id": "completed",
|
||||
"translation": "Terminé"
|
||||
},
|
||||
{
|
||||
"id": "language_name",
|
||||
"translation": "Français"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -319,10 +319,6 @@
|
|||
"id": "authors_favorite_language",
|
||||
"translation": "Ez a dev kedvenc programozási nyelve."
|
||||
},
|
||||
{
|
||||
"id": "nyaa_pantsu_dont_host_files",
|
||||
"translation": "nyaa.pantsu.cat és sukebei.pantsu.cat nem hosztol fájlokat."
|
||||
},
|
||||
{
|
||||
"id": "upload_magnet",
|
||||
"translation": "Magnet Feltöltése"
|
||||
|
@ -550,5 +546,9 @@
|
|||
{
|
||||
"id": "delete_success",
|
||||
"translation": "A fiók törlésre került."
|
||||
},
|
||||
{
|
||||
"id": "language_name",
|
||||
"translation": "Magyar"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -331,10 +331,6 @@
|
|||
"id": "authors_favorite_language",
|
||||
"translation": "È il linguaggio di programmazione preferito dell'autore."
|
||||
},
|
||||
{
|
||||
"id": "nyaa_pantsu_dont_host_files",
|
||||
"translation": "nyaa.pantsu.cat e sukebei.pantsu.cat non contengono/hostano nessun file al loro interno."
|
||||
},
|
||||
{
|
||||
"id": "upload_magnet",
|
||||
"translation": "Carica magnet"
|
||||
|
@ -618,5 +614,9 @@
|
|||
{
|
||||
"id": "completed",
|
||||
"translation": "Completato"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "language_name",
|
||||
"translation": "Italiano"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
},
|
||||
{
|
||||
"id":"signup_box_title",
|
||||
"translation": "登録しましょう。<small>ずっと無料です</small>"
|
||||
"translation": "登録 <small>ずっと無料です</small>"
|
||||
},
|
||||
{
|
||||
"id":"username",
|
||||
|
@ -81,7 +81,7 @@
|
|||
},
|
||||
{
|
||||
"id":"sign_in_box_title",
|
||||
"translation": "ログインします"
|
||||
"translation": "ログイン"
|
||||
},
|
||||
{
|
||||
"id":"sign_in_title",
|
||||
|
@ -125,7 +125,7 @@
|
|||
},
|
||||
{
|
||||
"id":"see_more_torrents_from",
|
||||
"translation": "%s の Torrent をもっと見る"
|
||||
"translation": "%s の Torrent をもっと表示"
|
||||
},
|
||||
{
|
||||
"id":"category",
|
||||
|
@ -181,7 +181,7 @@
|
|||
},
|
||||
{
|
||||
"id": "404_not_found",
|
||||
"translation": "404 見つかりません"
|
||||
"translation": "404 ページが見つかりません"
|
||||
},
|
||||
{
|
||||
"id": "no_torrents_uploaded",
|
||||
|
@ -197,7 +197,7 @@
|
|||
},
|
||||
{
|
||||
"id": "member",
|
||||
"translation": "メンバー"
|
||||
"translation": "会員"
|
||||
},
|
||||
{
|
||||
"id": "sign_in",
|
||||
|
@ -217,11 +217,11 @@
|
|||
},
|
||||
{
|
||||
"id": "official_nyaapocalipse_faq",
|
||||
"translation": "nyaa.se の閉鎖についてよくある質問"
|
||||
"translation": "公式 nyaa <ruby>黙示録<rp>(</rp><rt>アポカリプス</rt><rp>)</rp></ruby> のよくある質問"
|
||||
},
|
||||
{
|
||||
"id": "links_replacement_mirror",
|
||||
"translation": "nyaa.se のミラーリンク"
|
||||
"translation": "代替 / ミラーリンク"
|
||||
},
|
||||
{
|
||||
"id": "what_happened",
|
||||
|
@ -233,7 +233,7 @@
|
|||
},
|
||||
{
|
||||
"id": "its_not_a_ddos",
|
||||
"translation": "以前のような DDoS 攻撃ではなく、ドメインが利用停止になった。"
|
||||
"translation": "いつものような DDoS 攻撃ではなく、利用停止になった。"
|
||||
},
|
||||
{
|
||||
"id": "future_not_looking_good",
|
||||
|
@ -269,7 +269,7 @@
|
|||
},
|
||||
{
|
||||
"id": "answer_how_are_we_recovering",
|
||||
"translation": "上述のデータベースは nyaa.pantsu.cat と sukebei.pantsu.cat にホストされています。検索機能はすでにあり、nyaa.se にあったほぼすべての機能が近いうちに利用可能になるでしょう。また、Seeder / Leecher 統計はスクレイピングによって収集可能ですが、今は他に優先すべきことがあるため後回しにされます。"
|
||||
"translation": "上述のデータベースは nyaa.pantsu.cat と sukebei.pantsu.cat にホストされています。検索機能はすでにあり、近いうちに nyaa.se にあったほぼすべての機能が利用可能になるでしょう。また、Seeder / Leecher 統計はスクレイピングによって収集可能ですが、今は他に優先すべきことがあるため後回しにされます。"
|
||||
},
|
||||
{
|
||||
"id": "are_the_trackers_working",
|
||||
|
@ -277,15 +277,15 @@
|
|||
},
|
||||
{
|
||||
"id": "answer_are_the_trackers_working",
|
||||
"translation": "トラッカーがダウンしたとしても、シーダーはまだ DHT ネットワークに接続しているはずです。ファイルが DHT ネットワーク上にリスティングされてさえいれば、いつも通り利用できます。"
|
||||
"translation": "トラッカーがダウンしたとしても、Seeder はまだ DHT ネットワークに接続しているはずです。ファイルが DHT ネットワーク上にリスティングされてさえいれば、いつも通り利用できます。"
|
||||
},
|
||||
{
|
||||
"id": "how_do_i_download_the_torrents",
|
||||
"translation": "どうやって Torrent ファイルをダウンロードすればいいの?"
|
||||
"translation": "どうやって Torrent をダウンロードすればいいの?"
|
||||
},
|
||||
{
|
||||
"id": "answer_how_do_i_download_the_torrents",
|
||||
"translation": "<b>magnet リンク</b> をご利用ください。magnet リンクは BitTorrent クライアントが DHT ネットワーク上のファイルを見つけるのに利用されます。もちろんいつも通りダウンロードすることができます。"
|
||||
"translation": "<b>magnet リンク</b> をご利用ください。magnet リンクは BitTorrent クライアントが DHT ネットワーク上のファイルを検出するのに利用されます。もちろんいつも通りダウンロードすることができます。"
|
||||
},
|
||||
{
|
||||
"id": "magnet_link_should_look_like",
|
||||
|
@ -319,10 +319,6 @@
|
|||
"id": "authors_favorite_language",
|
||||
"translation": "作者の一番好きなプログラミング言語だからです。"
|
||||
},
|
||||
{
|
||||
"id": "nyaa_pantsu_dont_host_files",
|
||||
"translation": "nyaa.pantsu.cat と sukebei.pantsu.cat はいかなるファイルもホストしていません。"
|
||||
},
|
||||
{
|
||||
"id": "upload_magnet",
|
||||
"translation": "magnet リンクのアップロード"
|
||||
|
@ -333,7 +329,7 @@
|
|||
},
|
||||
{
|
||||
"id": "uploading_file_prefills_fields",
|
||||
"translation": "Torrent ファイルをアップロードしている間にいくつかの項目を入力できます。そうすることをおすすめします。"
|
||||
"translation": "Torrent ファイルをアップロードする前にいくつかの項目を入力できます。そうすることをお勧めします。"
|
||||
},
|
||||
{
|
||||
"id": "magnet_link",
|
||||
|
@ -525,7 +521,7 @@
|
|||
},
|
||||
{
|
||||
"id": "trusted_member",
|
||||
"translation": "信頼されたメンバー"
|
||||
"translation": "信頼された会員"
|
||||
},
|
||||
{
|
||||
"id": "moderator",
|
||||
|
@ -594,5 +590,20 @@
|
|||
{
|
||||
"id":"date_format",
|
||||
"translation": "2006/01/02 15:04"
|
||||
},
|
||||
{
|
||||
"id": "language_name",
|
||||
"translation": "日本語"
|
||||
},
|
||||
"id": "seeders",
|
||||
"translation": "Seeder 数"
|
||||
},
|
||||
{
|
||||
"id": "leechers",
|
||||
"translation": "Leecher 数"
|
||||
},
|
||||
{
|
||||
"id": "completed",
|
||||
"translation": "完了数"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -319,10 +319,6 @@
|
|||
"id": "authors_favorite_language",
|
||||
"translation": "제일 좋아하는 언어입니다. >__- 찡긋"
|
||||
},
|
||||
{
|
||||
"id": "nyaa_pantsu_dont_host_files",
|
||||
"translation": "nyaa.pantsu.cat 와 sukebei.pantsu.cat 는 그 어떤 파일도 호스팅하고 있지 않습니다"
|
||||
},
|
||||
{
|
||||
"id": "upload_magnet",
|
||||
"translation": "마그넷 업로드"
|
||||
|
@ -498,5 +494,9 @@
|
|||
{
|
||||
"id": "submit",
|
||||
"translation": "확인"
|
||||
},
|
||||
{
|
||||
"id": "language_name",
|
||||
"translation": "한국어"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -331,10 +331,6 @@
|
|||
"id": "authors_favorite_language",
|
||||
"translation": "Favorittspråket til karen som dro sulamitten i gang."
|
||||
},
|
||||
{
|
||||
"id": "nyaa_pantsu_dont_host_files",
|
||||
"translation": "nyaa.pantsu.cat og sukebei.pantsu.cat har ingen av disse filene på sine servere, og deler ingen filer fra serverene ellers."
|
||||
},
|
||||
{
|
||||
"id": "upload_magnet",
|
||||
"translation": "Last opp magnet"
|
||||
|
@ -582,5 +578,9 @@
|
|||
{
|
||||
"id": "email_changed",
|
||||
"translation": "Epost-addressen har blitt endret! Neste steg er å bekrefte den (sjekk innboksen)."
|
||||
},
|
||||
{
|
||||
"id": "language_name",
|
||||
"translation": "Norsk bokmål"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -319,10 +319,6 @@
|
|||
"id": "authors_favorite_language",
|
||||
"translation": "Het is de favoriete programmeertaal van de bedenker."
|
||||
},
|
||||
{
|
||||
"id": "nyaa_pantsu_dont_host_files",
|
||||
"translation": "nyaa.pantsu.cat en sukebei.pantsu.cat hosten geen bestanden."
|
||||
},
|
||||
{
|
||||
"id": "upload_magnet",
|
||||
"translation": "Magnet uploaden"
|
||||
|
@ -550,5 +546,9 @@
|
|||
{
|
||||
"id": "delete_success",
|
||||
"translation": "Het account is met succes verwijderd!"
|
||||
},
|
||||
{
|
||||
"id": "language_name",
|
||||
"translation": "Nederlands"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -105,11 +105,11 @@
|
|||
},
|
||||
{
|
||||
"id":"signup_verification_noemail",
|
||||
"translation": "Cadastro bem-sucedido, você pode agora usar a sua conta."
|
||||
"translation": "Cadastro bem-sucedido, você já pode usar a sua conta."
|
||||
},
|
||||
{
|
||||
"id":"settings",
|
||||
"translation": "Configurações da conta"
|
||||
"translation": "Configurações de conta"
|
||||
},
|
||||
{
|
||||
"id":"torrents",
|
||||
|
@ -331,10 +331,6 @@
|
|||
"id": "authors_favorite_language",
|
||||
"translation": "É a linguagem de programação favorita do autor."
|
||||
},
|
||||
{
|
||||
"id": "nyaa_pantsu_dont_host_files",
|
||||
"translation": "nyaa.pantsu.cat e sukebei.pantsu.cat não hospedam nenhum arquivo."
|
||||
},
|
||||
{
|
||||
"id": "upload_magnet",
|
||||
"translation": "Enviar link magnético"
|
||||
|
@ -582,5 +578,13 @@
|
|||
{
|
||||
"id": "profile_edit_page",
|
||||
"translation": "Editar o perfil de %s"
|
||||
},
|
||||
{
|
||||
"id": "change_language",
|
||||
"translation": "Mudar Idioma"
|
||||
},
|
||||
{
|
||||
"id": "language_name",
|
||||
"translation": "Português (Brasil)"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -209,15 +209,15 @@
|
|||
},
|
||||
{
|
||||
"id": "member",
|
||||
"translation": "Пользователь"
|
||||
"translation": "Членство"
|
||||
},
|
||||
{
|
||||
"id": "sign_in",
|
||||
"translation": "Войти"
|
||||
"translation": "Вход"
|
||||
},
|
||||
{
|
||||
"id": "sign_up",
|
||||
"translation": "Зарегистрироваться"
|
||||
"translation": "Регистрация"
|
||||
},
|
||||
{
|
||||
"id": "no_results_found",
|
||||
|
@ -269,7 +269,7 @@
|
|||
},
|
||||
{
|
||||
"id": "answer_is_nyaa_db_lost",
|
||||
"translation": "У нас есть база данных торрентов на nyaa до <s>5 апреля</s> 1 мая. Это означает, что почти ничего не потеряно."
|
||||
"translation": "У нас есть база данных торрентов nyaa по <s>5 апреля</s> 1 мая. Это означает, что почти ничего не потеряно."
|
||||
},
|
||||
{
|
||||
"id": "answer_is_sukebei_db_lost",
|
||||
|
@ -289,7 +289,7 @@
|
|||
},
|
||||
{
|
||||
"id": "answer_are_the_trackers_working",
|
||||
"translation": "Даже если трекеры не работают, раздающие все еще подключены к децентрализованной сети DHT. Пока файл находится в сети DHT, все должено работать как обычно."
|
||||
"translation": "Даже если трекеры не работают, сиды все еще подключены к децентрализованной сети DHT. Пока файл находится в сети DHT, все должено работать как обычно."
|
||||
},
|
||||
{
|
||||
"id": "how_do_i_download_the_torrents",
|
||||
|
@ -331,10 +331,6 @@
|
|||
"id": "authors_favorite_language",
|
||||
"translation": "Это любимый язык программирования автора."
|
||||
},
|
||||
{
|
||||
"id": "nyaa_pantsu_dont_host_files",
|
||||
"translation": "nyaa.pantsu.cat и sukebei.pantsu.cat не содержат файлы."
|
||||
},
|
||||
{
|
||||
"id": "upload_magnet",
|
||||
"translation": "Загрузить магнит"
|
||||
|
@ -452,8 +448,8 @@
|
|||
"translation": "Описание торрента"
|
||||
},
|
||||
{
|
||||
"id": "limited_html_set_is_allowed_use",
|
||||
"translation": "В описании допускается ограниченный набор HTML, поэтому обязательно используйте"
|
||||
"id": "description_markdown_notice",
|
||||
"translation": "В описании может быть использован Markdown синтаксис."
|
||||
},
|
||||
{
|
||||
"id": "show_all",
|
||||
|
@ -461,11 +457,11 @@
|
|||
},
|
||||
{
|
||||
"id": "filter_remakes",
|
||||
"translation": "Фильтрация повторов"
|
||||
"translation": "Ремейки"
|
||||
},
|
||||
{
|
||||
"id": "trusted",
|
||||
"translation": "Доверенные"
|
||||
"translation": "Надёжные"
|
||||
},
|
||||
{
|
||||
"id": "id",
|
||||
|
@ -537,7 +533,7 @@
|
|||
},
|
||||
{
|
||||
"id": "trusted_member",
|
||||
"translation": "Надежный пользователь"
|
||||
"translation": "Надёжный пользователь"
|
||||
},
|
||||
{
|
||||
"id": "moderator",
|
||||
|
@ -577,7 +573,7 @@
|
|||
},
|
||||
{
|
||||
"id": "mark_as_remake",
|
||||
"translation": "Отметить как повтор"
|
||||
"translation": "Отметить как ремейк"
|
||||
},
|
||||
{
|
||||
"id": "email_changed",
|
||||
|
@ -597,10 +593,30 @@
|
|||
},
|
||||
{
|
||||
"id": "torrent_status_remake",
|
||||
"translation": "Повтор"
|
||||
"translation": "Ремейк"
|
||||
},
|
||||
{
|
||||
"id": "profile_edit_page",
|
||||
"translation": "Изменение профиля %s's"
|
||||
},
|
||||
{
|
||||
"id":"date_format",
|
||||
"translation": "2006-01-02 15:04"
|
||||
},
|
||||
{
|
||||
"id": "seeders",
|
||||
"translation": "Сиды"
|
||||
},
|
||||
{
|
||||
"id": "leechers",
|
||||
"translation": "Личи"
|
||||
},
|
||||
{
|
||||
"id": "completed",
|
||||
"translation": "Скачавшие"
|
||||
},
|
||||
{
|
||||
"id": "language_name",
|
||||
"translation": "Русский"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -331,10 +331,6 @@
|
|||
"id": "authors_favorite_language",
|
||||
"translation": "Det är skaparens favoritspråk."
|
||||
},
|
||||
{
|
||||
"id": "nyaa_pantsu_dont_host_files",
|
||||
"translation": "nyaa.pantsu.cat och sukebei.pantsu.cat tillhandahåller inga filer på sina servrar."
|
||||
},
|
||||
{
|
||||
"id": "upload_magnet",
|
||||
"translation": "Ladda upp magnet"
|
||||
|
@ -582,5 +578,9 @@
|
|||
{
|
||||
"id": "email_changed",
|
||||
"translation": "Din mailadress har bytts! Du måste bekräfta adressen genom att klicka på länken som skickats till %s."
|
||||
},
|
||||
{
|
||||
"id": "language_name",
|
||||
"translation": "Svenska"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -331,10 +331,6 @@
|
|||
"id": "authors_favorite_language",
|
||||
"translation": "เพราะเป็นภาษาถนัดของผู้พัฒนา"
|
||||
},
|
||||
{
|
||||
"id": "nyaa_pantsu_dont_host_files",
|
||||
"translation": "nyaa.pantsu.cat และ sukebei.pantsu.cat จะไม่เก็บไฟล์ใดๆ ไว้ในระบบทั้งสิ้น"
|
||||
},
|
||||
{
|
||||
"id": "upload_magnet",
|
||||
"translation": "อัพโหลด Magnet"
|
||||
|
@ -618,5 +614,9 @@
|
|||
{
|
||||
"id": "completed",
|
||||
"translation": "โหลดเสร็จแล้ว"
|
||||
},
|
||||
{
|
||||
"id": "language_name",
|
||||
"translation": "ไทย"
|
||||
}
|
||||
]
|
||||
|
|
566
translations/zh-cn.all.json
Fichier normal
566
translations/zh-cn.all.json
Fichier normal
|
@ -0,0 +1,566 @@
|
|||
[
|
||||
{
|
||||
"id": "link",
|
||||
"translation": "链接"
|
||||
},
|
||||
{
|
||||
"id": "verify_email_title",
|
||||
"translation": "请验证您的电子邮箱喵~"
|
||||
},
|
||||
{
|
||||
"id": "verify_email_content",
|
||||
"translation": "请点击下面的链接验证您的电子邮箱喵~"
|
||||
},
|
||||
{
|
||||
"id": "reset_password_title",
|
||||
"translation": "密码重置"
|
||||
},
|
||||
{
|
||||
"id": "reset_password_content",
|
||||
"translation": "请点击下面的链接重置密码喵~"
|
||||
},
|
||||
{
|
||||
"id":"register_title",
|
||||
"translation": "注册账号"
|
||||
},
|
||||
{
|
||||
"id":"signup_box_title",
|
||||
"translation": "请注册 <small>我们永久免费</small>"
|
||||
},
|
||||
{
|
||||
"id":"username",
|
||||
"translation": "用户名"
|
||||
},
|
||||
{
|
||||
"id":"email_address_or_username",
|
||||
"translation": "电子邮箱或用户名"
|
||||
},
|
||||
{
|
||||
"id":"email_address",
|
||||
"translation": "电子邮箱"
|
||||
},
|
||||
{
|
||||
"id":"password",
|
||||
"translation": "密码"
|
||||
},
|
||||
{
|
||||
"id":"confirm_password",
|
||||
"translation": "确认密码"
|
||||
},
|
||||
{
|
||||
"id":"i_agree",
|
||||
"translation": "我同意"
|
||||
},
|
||||
{
|
||||
"id":"terms_conditions_confirm",
|
||||
"translation": "按下 <strong class=\"label label-primary\">注册账号</strong> 即表示您同意本网站的 <a href=\"#\" data-toggle=\"modal\" data-target=\"#t_and_c_m\">条款与条件</a> 以及浏览器Cookie喵~"
|
||||
},
|
||||
{
|
||||
"id":"signin",
|
||||
"translation": "登录"
|
||||
},
|
||||
{
|
||||
"id":"register",
|
||||
"translation": "注册"
|
||||
},
|
||||
{
|
||||
"id":"terms_conditions",
|
||||
"translation": "条款与条件"
|
||||
},
|
||||
{
|
||||
"id":"terms_conditions_full",
|
||||
"translation": "一些扯淡"
|
||||
},
|
||||
{
|
||||
"id":"remember_me",
|
||||
"translation": "记住我"
|
||||
},
|
||||
{
|
||||
"id":"forgot_password",
|
||||
"translation": "忘记密码?"
|
||||
},
|
||||
{
|
||||
"id":"sign_in_box_title",
|
||||
"translation": "请登录"
|
||||
},
|
||||
{
|
||||
"id":"sign_in_title",
|
||||
"translation": "登录"
|
||||
},
|
||||
{
|
||||
"id":"register_success_title",
|
||||
"translation": "注册完成"
|
||||
},
|
||||
{
|
||||
"id":"sign_up_success",
|
||||
"translation": "恭喜您注册成功喵!"
|
||||
},
|
||||
{
|
||||
"id":"verify_success",
|
||||
"translation": "<i style=\"color:limegreen\" class=\"glyphicon glyphicon-ok-circle\"></i>您的账号已可以使用了喵!"
|
||||
},
|
||||
{
|
||||
"id":"signup_verification_email",
|
||||
"translation": "请检查您的电子邮箱(或垃圾邮件)找到账号确认邮件喵~"
|
||||
},
|
||||
{
|
||||
"id":"signup_verification_noemail",
|
||||
"translation": "注册成功喵,账号已经可以使用了!"
|
||||
},
|
||||
{
|
||||
"id":"settings",
|
||||
"translation": "账号设置"
|
||||
},
|
||||
{
|
||||
"id":"torrents",
|
||||
"translation": "种子"
|
||||
},
|
||||
{
|
||||
"id":"follow",
|
||||
"translation": "关注"
|
||||
},
|
||||
{
|
||||
"id":"unfollow",
|
||||
"translation": "取消关注"
|
||||
},
|
||||
{
|
||||
"id":"user_followed_msg",
|
||||
"translation": "您已经关注了 %s 喵!"
|
||||
},
|
||||
{
|
||||
"id":"user_unfollowed_msg",
|
||||
"translation": "您已经取消了对 %s 的关注喵!"
|
||||
},
|
||||
{
|
||||
"id":"profile_page",
|
||||
"translation": "%s 用户资料"
|
||||
},
|
||||
{
|
||||
"id":"see_more_torrents_from",
|
||||
"translation": "更多种子来自 %s "
|
||||
},
|
||||
{
|
||||
"id":"category",
|
||||
"translation": "分类"
|
||||
},
|
||||
{
|
||||
"id": "name",
|
||||
"translation": "名称"
|
||||
},
|
||||
{
|
||||
"id": "date",
|
||||
"translation": "日期"
|
||||
},
|
||||
{
|
||||
"id": "size",
|
||||
"translation": "文件大小"
|
||||
},
|
||||
{
|
||||
"id": "links",
|
||||
"translation": "链接"
|
||||
},
|
||||
{
|
||||
"id": "home",
|
||||
"translation": "主页"
|
||||
},
|
||||
{
|
||||
"id": "error_404",
|
||||
"translation": "纳尼? 是 404 喵!"
|
||||
},
|
||||
{
|
||||
"id": "toggle_navigation",
|
||||
"translation": "切换索引"
|
||||
},
|
||||
{
|
||||
"id": "upload",
|
||||
"translation": "上传"
|
||||
},
|
||||
{
|
||||
"id": "faq",
|
||||
"translation": "FAQ"
|
||||
},
|
||||
{
|
||||
"id": "fap",
|
||||
"translation": "神秘花园"
|
||||
},
|
||||
{
|
||||
"id": "advanced_search",
|
||||
"translation": "高级搜索"
|
||||
},
|
||||
{
|
||||
"id": "nothing_here",
|
||||
"translation": "这里什么都没有喵~"
|
||||
},
|
||||
{
|
||||
"id": "404_not_found",
|
||||
"translation": "404 不存在的喵~"
|
||||
},
|
||||
{
|
||||
"id": "no_torrents_uploaded",
|
||||
"translation": "没有被上传的种子喵!"
|
||||
},
|
||||
{
|
||||
"id": "profile",
|
||||
"translation": "用户资料"
|
||||
},
|
||||
{
|
||||
"id": "sign_out",
|
||||
"translation": "注销"
|
||||
},
|
||||
{
|
||||
"id": "member",
|
||||
"translation": "会员"
|
||||
},
|
||||
{
|
||||
"id": "sign_in",
|
||||
"translation": "登录"
|
||||
},
|
||||
{
|
||||
"id": "sign_up",
|
||||
"translation": "注册"
|
||||
},
|
||||
{
|
||||
"id": "no_results_found",
|
||||
"translation": "没有找到内容喵~"
|
||||
},
|
||||
{
|
||||
"id": "notice_keep_seeding",
|
||||
"translation": "别偷懒!开启 DHT 保持做种喵!"
|
||||
},
|
||||
{
|
||||
"id": "official_nyaapocalipse_faq",
|
||||
"translation": "NYAA末日的官方FAQ"
|
||||
},
|
||||
{
|
||||
"id": "links_replacement_mirror",
|
||||
"translation": "备用链接/镜像"
|
||||
},
|
||||
{
|
||||
"id": "what_happened",
|
||||
"translation": "什么鬼?"
|
||||
},
|
||||
{
|
||||
"id": "nyaa_se_went_offline",
|
||||
"translation": "nyaa.se 以及相关站点(比如 nyaatorrents.info)不幸于 2017年5月1日 离开了我们"
|
||||
},
|
||||
{
|
||||
"id": "its_not_a_ddos",
|
||||
"translation": "站点完全被停用,并不是以往的 DDoS 攻击"
|
||||
},
|
||||
{
|
||||
"id": "future_not_looking_good",
|
||||
"translation": "nyaa 的未来非常的严峻 她真的走了"
|
||||
},
|
||||
{
|
||||
"id": "recovery_effort",
|
||||
"translation": "而资料的抢救也正在进行中"
|
||||
},
|
||||
{
|
||||
"id": "is_everything_lost",
|
||||
"translation": "难道所有的资料都随她而去了吗?"
|
||||
},
|
||||
{
|
||||
"id": "in_short_no",
|
||||
"translation": "不!并没有"
|
||||
},
|
||||
{
|
||||
"id": "are_some_things_lost",
|
||||
"translation": "那她带走了什么吗?"
|
||||
},
|
||||
{
|
||||
"id": "answer_is_nyaa_db_lost",
|
||||
"translation": "我们备份了 nyaa 直到 <s>4月5日</s> 5月1日 的种子库 ,几乎没有任何的损失"
|
||||
},
|
||||
{
|
||||
"id": "answer_is_sukebei_db_lost",
|
||||
"translation": "然而 sukebei 就没那么幸运了。种子库只备份到 2016年,但也许能找到更新的版本"
|
||||
},
|
||||
{
|
||||
"id": "how_are_we_recovering",
|
||||
"translation": "我们要如何恢复她?"
|
||||
},
|
||||
{
|
||||
"id": "answer_how_are_we_recovering",
|
||||
"translation": "前面所说的资料都存放在 nyaa.pantsu.cat 和 sukebei.pantsu.cat 的服务器上。 我们也会尽力恢复旧 nyaa 站的全部功能。 而在未来的某一天,种子和链接数的统计资料也可能通过抓取其他 tracker 來恢复!不过先要把眼前的工作完成再说。"
|
||||
},
|
||||
{
|
||||
"id": "are_the_trackers_working",
|
||||
"translation": "这些种子都还活着吗?"
|
||||
},
|
||||
{
|
||||
"id": "answer_are_the_trackers_working",
|
||||
"translation": "就算 tracker 都挂了,做种的大佬们都还通过 DHT 网络保持着联系。 只要那些资料都还在 DHT 网络上,就几乎没有影响。"
|
||||
},
|
||||
{
|
||||
"id": "how_do_i_download_the_torrents",
|
||||
"translation": "所以要怎么抓种?"
|
||||
},
|
||||
{
|
||||
"id": "answer_how_do_i_download_the_torrents",
|
||||
"translation": "用<b>磁力链接(神秘代码)</b>吧! 磁力链接能让你的 BT 客戶端在 DHT 网络寻找种子並且下载。"
|
||||
},
|
||||
{
|
||||
"id": "magnet_link_should_look_like",
|
||||
"translation": "相信各位绅士早已耳熟能详,这就是神秘代码:"
|
||||
},
|
||||
{
|
||||
"id": "which_trackers_do_you_recommend",
|
||||
"translation": "有推荐的 tracker 吗?"
|
||||
},
|
||||
{
|
||||
"id": "answer_which_trackers_do_you_recommend",
|
||||
"translation": "如果您上传种子被拒绝,原因是 tracker 不符合要求,那有可能需要加上这几个 tracker:"
|
||||
},
|
||||
{
|
||||
"id": "how_can_i_help",
|
||||
"translation": "有什么我能帮忙的吗?"
|
||||
},
|
||||
{
|
||||
"id": "answer_how_can_i_help",
|
||||
"translation": "如果您是个技术宅,請加入 #nyaapantsu 的 IRC,位於 irc.rizon.net 。如果您有最新的资料库,特別是 sukebei 的,<b>请上传它們!</b>"
|
||||
},
|
||||
{
|
||||
"id": "your_design_sucks_found_a_bug",
|
||||
"translation": "瞧你们设计的破网站 / 我找到 bug 了!"
|
||||
},
|
||||
{
|
||||
"id": "why_written_in_go",
|
||||
"translation": "为啥要用 GO 语言?没有别的选择了吗?"
|
||||
},
|
||||
{
|
||||
"id": "authors_favorite_language",
|
||||
"translation": "这是作者的信仰!"
|
||||
},
|
||||
{
|
||||
"id": "nyaa_pantsu_dont_host_files",
|
||||
"translation": "nyaa.pantsu.cat 以及 sukebei.pantsu.cat 沒有保存任何资料喵~"
|
||||
},
|
||||
{
|
||||
"id": "upload_magnet",
|
||||
"translation": "上传磁力链接"
|
||||
},
|
||||
{
|
||||
"id": "torrent_file",
|
||||
"translation": "种子文件"
|
||||
},
|
||||
{
|
||||
"id": "uploading_file_prefills_fields",
|
||||
"translation": "上传时允许预填写(pre-filling)一些内容,这是推荐选项。"
|
||||
},
|
||||
{
|
||||
"id": "magnet_link",
|
||||
"translation": "磁力链接地址"
|
||||
},
|
||||
{
|
||||
"id": "all_categories",
|
||||
"translation": "所有分类"
|
||||
},
|
||||
{
|
||||
"id": "anime",
|
||||
"translation": "动画"
|
||||
},
|
||||
{
|
||||
"id": "anime_amv",
|
||||
"translation": "动画 - 动画MV"
|
||||
},
|
||||
{
|
||||
"id": "anime_english_translated",
|
||||
"translation": "动画 - 已翻译为英文"
|
||||
},
|
||||
{
|
||||
"id": "anime_non_english_translated",
|
||||
"translation": "动画 - 未翻译为英文"
|
||||
},
|
||||
{
|
||||
"id": "anime_raw",
|
||||
"translation": "动画 - 生肉"
|
||||
},
|
||||
{
|
||||
"id": "audio",
|
||||
"translation": "音乐"
|
||||
},
|
||||
{
|
||||
"id": "audio_lossless",
|
||||
"translation": "音乐 - 无损"
|
||||
},
|
||||
{
|
||||
"id": "audio_lossy",
|
||||
"translation": "音游 - 有损"
|
||||
},
|
||||
{
|
||||
"id": "literature",
|
||||
"translation": "文学"
|
||||
},
|
||||
{
|
||||
"id": "literature_english_translated",
|
||||
"translation": "文学 - 已翻译为英文"
|
||||
},
|
||||
{
|
||||
"id": "literature_raw",
|
||||
"translation": "文学 - 生肉"
|
||||
},
|
||||
{
|
||||
"id": "literature_non_english_translated",
|
||||
"translation": "文学 - 未翻译为英文"
|
||||
},
|
||||
{
|
||||
"id": "live_action",
|
||||
"translation": "真人电影"
|
||||
},
|
||||
{
|
||||
"id": "live_action_english_translated",
|
||||
"translation": "真人电影 - 已翻译为英文"
|
||||
},
|
||||
{
|
||||
"id": "live_action_idol_pv",
|
||||
"translation": "真人电影 - 偶像/PV"
|
||||
},
|
||||
{
|
||||
"id": "live_action_non_english_translated",
|
||||
"translation": "真人电影 - 未翻译为英文"
|
||||
},
|
||||
{
|
||||
"id": "live_action_raw",
|
||||
"translation": "真人电影 - 生肉"
|
||||
},
|
||||
{
|
||||
"id": "pictures",
|
||||
"translation": "图片"
|
||||
},
|
||||
{
|
||||
"id": "pictures_graphics",
|
||||
"translation": "图片 - 图像"
|
||||
},
|
||||
{
|
||||
"id": "pictures_photos",
|
||||
"translation": "图片 - 照片"
|
||||
},
|
||||
{
|
||||
"id": "software",
|
||||
"translation": "软件"
|
||||
},
|
||||
{
|
||||
"id": "software_applications",
|
||||
"translation": "软件 - 应用程序"
|
||||
},
|
||||
{
|
||||
"id": "software_games",
|
||||
"translation": "软件 - 游戏"
|
||||
},
|
||||
{
|
||||
"id": "torrent_description",
|
||||
"translation": "种子描述"
|
||||
},
|
||||
{
|
||||
"id": "description_markdown_notice",
|
||||
"translation": "描述可使用 Markdown"
|
||||
},
|
||||
{
|
||||
"id": "show_all",
|
||||
"translation": "显示所有"
|
||||
},
|
||||
{
|
||||
"id": "filter_remakes",
|
||||
"translation": "过滤 Remakes"
|
||||
},
|
||||
{
|
||||
"id": "trusted",
|
||||
"translation": "信任的"
|
||||
},
|
||||
{
|
||||
"id": "id",
|
||||
"translation": "ID"
|
||||
},
|
||||
{
|
||||
"id": "downloads",
|
||||
"translation": "下载量"
|
||||
},
|
||||
{
|
||||
"id": "descending",
|
||||
"translation": "降序"
|
||||
},
|
||||
{
|
||||
"id": "ascending",
|
||||
"translation": "升序"
|
||||
},
|
||||
{
|
||||
"id": "search",
|
||||
"translation": "搜索"
|
||||
},
|
||||
{
|
||||
"id": "hash",
|
||||
"translation": "哈希"
|
||||
},
|
||||
{
|
||||
"id": "description",
|
||||
"translation": "描述"
|
||||
},
|
||||
{
|
||||
"id": "comments",
|
||||
"translation": "评论"
|
||||
},
|
||||
{
|
||||
"id": "submit_a_comment_as_username",
|
||||
"translation": "以用户名 %s 评论"
|
||||
},
|
||||
{
|
||||
"id": "submit_a_comment_as_anonymous",
|
||||
"translation": "匿名评论"
|
||||
},
|
||||
{
|
||||
"id": "submit",
|
||||
"translation": "提交"
|
||||
},
|
||||
{
|
||||
"id": "personal_info",
|
||||
"translation": "个人资料"
|
||||
},
|
||||
{
|
||||
"id": "language",
|
||||
"translation": "语言"
|
||||
},
|
||||
{
|
||||
"id": "current_password",
|
||||
"translation": "当前密码"
|
||||
},
|
||||
{
|
||||
"id": "role",
|
||||
"translation": "角色"
|
||||
},
|
||||
{
|
||||
"id": "banned",
|
||||
"translation": "账号已被封禁"
|
||||
},
|
||||
{
|
||||
"id": "default",
|
||||
"translation": "默认值"
|
||||
},
|
||||
{
|
||||
"id": "trusted_member",
|
||||
"translation": "受信任会员"
|
||||
},
|
||||
{
|
||||
"id": "moderator",
|
||||
"translation": "管理员"
|
||||
},
|
||||
{
|
||||
"id": "save_changes",
|
||||
"translation": "保存更改"
|
||||
},
|
||||
{
|
||||
"id": "profile_updated",
|
||||
"translation": "您的个人资料已保存喵!"
|
||||
},
|
||||
{
|
||||
"id": "delete_account",
|
||||
"translation": "删除账号"
|
||||
},
|
||||
{
|
||||
"id": "delete_account_confirm",
|
||||
"translation": "您真的要删除您的账号喵?这无法挽回!"
|
||||
},
|
||||
{
|
||||
"id": "delete_success",
|
||||
"translation": "您的账号已被删除!"
|
||||
}
|
||||
]
|
|
@ -331,10 +331,6 @@
|
|||
"id": "authors_favorite_language",
|
||||
"translation": "這是作者的愛啊啊啊啊"
|
||||
},
|
||||
{
|
||||
"id": "nyaa_pantsu_dont_host_files",
|
||||
"translation": "nyaa.pantsu.cat 以及 sukebei.pantsu.cat 沒有存放任何檔案喔∼"
|
||||
},
|
||||
{
|
||||
"id": "upload_magnet",
|
||||
"translation": "上傳磁力連結"
|
||||
|
|
|
@ -4,25 +4,26 @@ import (
|
|||
"fmt"
|
||||
"github.com/ewhal/nyaa/service/user"
|
||||
"github.com/nicksnyder/go-i18n/i18n"
|
||||
"github.com/nicksnyder/go-i18n/i18n/language"
|
||||
"html/template"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// When go-i18n finds a language with >0 translations, it uses it as the Tfunc
|
||||
// However, if said language has a missing translation, it won't fallback to the "main" language
|
||||
func TfuncWithFallback(language string, languages ...string) (i18n.TranslateFunc, error) {
|
||||
func TfuncAndLanguageWithFallback(language string, languages ...string) (i18n.TranslateFunc, *language.Language, error) {
|
||||
// Use the last language on the args as the fallback one.
|
||||
fallbackLanguage := language
|
||||
if languages != nil {
|
||||
fallbackLanguage = languages[len(languages)-1]
|
||||
}
|
||||
|
||||
T, err1 := i18n.Tfunc(language, languages...)
|
||||
fallbackT, err2 := i18n.Tfunc(fallbackLanguage)
|
||||
T, Tlang, err1 := i18n.TfuncAndLanguage(language, languages...)
|
||||
fallbackT, fallbackTlang, err2 := i18n.TfuncAndLanguage(fallbackLanguage)
|
||||
|
||||
if err1 != nil && err2 != nil {
|
||||
// fallbackT is still a valid function even with the error, it returns translationID.
|
||||
return fallbackT, err2
|
||||
return fallbackT, fallbackTlang, err2
|
||||
}
|
||||
|
||||
return func(translationID string, args ...interface{}) string {
|
||||
|
@ -31,7 +32,7 @@ func TfuncWithFallback(language string, languages ...string) (i18n.TranslateFunc
|
|||
}
|
||||
|
||||
return fallbackT(translationID, args...)
|
||||
}, nil
|
||||
}, Tlang, nil
|
||||
}
|
||||
|
||||
func GetAvailableLanguages() (languages map[string]string) {
|
||||
|
@ -50,8 +51,7 @@ func GetAvailableLanguages() (languages map[string]string) {
|
|||
return
|
||||
}
|
||||
|
||||
func SetTranslation(tmpl *template.Template, language string, languages ...string) i18n.TranslateFunc {
|
||||
T, _ := TfuncWithFallback(language, languages...)
|
||||
func setTranslation(tmpl *template.Template, T i18n.TranslateFunc) {
|
||||
tmpl.Funcs(map[string]interface{}{
|
||||
"T": func(str string, args ...interface{}) template.HTML {
|
||||
return template.HTML(fmt.Sprintf(T(str), args...))
|
||||
|
@ -60,10 +60,9 @@ func SetTranslation(tmpl *template.Template, language string, languages ...strin
|
|||
return fmt.Sprintf(T(str), args...)
|
||||
},
|
||||
})
|
||||
return T
|
||||
}
|
||||
|
||||
func SetTranslationFromRequest(tmpl *template.Template, r *http.Request, defaultLanguage string) i18n.TranslateFunc {
|
||||
func GetTfuncAndLanguageFromRequest(r *http.Request, defaultLanguage string) (T i18n.TranslateFunc, Tlang *language.Language) {
|
||||
userLanguage := ""
|
||||
user, _, err := userService.RetrieveCurrentUser(r)
|
||||
if err == nil {
|
||||
|
@ -78,6 +77,14 @@ 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)
|
||||
T, Tlang, _ = TfuncAndLanguageWithFallback(userLanguage, cookieLanguage, headerLanguage, defaultLanguage)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
func SetTranslationFromRequest(tmpl *template.Template, r *http.Request, defaultLanguage string) i18n.TranslateFunc {
|
||||
r.Header.Add("Vary", "Accept-Encoding")
|
||||
T, _ := GetTfuncAndLanguageFromRequest(r, defaultLanguage)
|
||||
setTranslation(tmpl, T)
|
||||
return T
|
||||
}
|
||||
|
|
|
@ -18,17 +18,31 @@ import (
|
|||
)
|
||||
|
||||
var searchOperator string
|
||||
var useTSQuery bool
|
||||
|
||||
func Configure(conf *config.SearchConfig) (err error) {
|
||||
// SQLite has case-insensitive LIKE, but no ILIKE
|
||||
if db.ORM.Dialect().GetName() == "sqlite3" {
|
||||
searchOperator = "LIKE ?"
|
||||
} else {
|
||||
useTSQuery = false
|
||||
// Postgres needs ILIKE for case-insensitivity
|
||||
if db.ORM.Dialect().GetName() == "postgres" {
|
||||
searchOperator = "ILIKE ?"
|
||||
//useTSQuery = true
|
||||
// !!DISABLED!! because this makes search a lot stricter
|
||||
// (only matches at word borders)
|
||||
} else {
|
||||
searchOperator = "LIKE ?"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func stringIsAscii(input string) bool {
|
||||
for _, char := range input {
|
||||
if char > 127 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -164,7 +178,7 @@ func searchByQuery(r *http.Request, pagenum int, countAll bool) (
|
|||
}
|
||||
|
||||
searchQuerySplit := strings.Fields(search.Query)
|
||||
for i, word := range searchQuerySplit {
|
||||
for _, word := range searchQuerySplit {
|
||||
firstRune, _ := utf8.DecodeRuneInString(word)
|
||||
if len(word) == 1 && unicode.IsPunct(firstRune) {
|
||||
// some queries have a single punctuation character
|
||||
|
@ -175,9 +189,14 @@ func searchByQuery(r *http.Request, pagenum int, countAll bool) (
|
|||
continue
|
||||
}
|
||||
|
||||
// TODO: make this faster ?
|
||||
conditions = append(conditions, "torrent_name "+searchOperator)
|
||||
parameters.Params = append(parameters.Params, "%"+searchQuerySplit[i]+"%")
|
||||
if useTSQuery && stringIsAscii(word) {
|
||||
conditions = append(conditions, "torrent_name @@ plainto_tsquery(?)")
|
||||
parameters.Params = append(parameters.Params, word)
|
||||
} else {
|
||||
// TODO: possible to make this faster?
|
||||
conditions = append(conditions, "torrent_name "+searchOperator)
|
||||
parameters.Params = append(parameters.Params, "%"+word+"%")
|
||||
}
|
||||
}
|
||||
|
||||
parameters.Conditions = strings.Join(conditions[:], " AND ")
|
||||
|
@ -194,4 +213,4 @@ func searchByQuery(r *http.Request, pagenum int, countAll bool) (
|
|||
return
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
Référencer dans un nouveau ticket