Files
ncore-hnr/internal/ncore/client.go
Zsolt Alföldi 469e5b0678 init
2026-05-07 00:14:02 +02:00

232 lines
5.9 KiB
Go

package ncore
import (
"fmt"
"net/http"
"net/http/cookiejar"
"net/url"
"strconv"
"strings"
"github.com/PuerkitoBio/goquery"
"ncore-hnr/internal/model"
)
type Client struct {
httpClient *http.Client
loginURL string
hitRunURL string
}
func New(httpClient *http.Client, loginURL string, hitRunURL string) *Client {
if httpClient == nil {
jar, _ := cookiejar.New(nil)
httpClient = &http.Client{Jar: jar}
}
return &Client{httpClient: httpClient, loginURL: loginURL, hitRunURL: hitRunURL}
}
func (c *Client) Login(username string, password string) error {
resp, err := c.httpClient.Get(c.loginURL)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("login page returned %s", resp.Status)
}
doc, err := goquery.NewDocumentFromReader(resp.Body)
if err != nil {
return err
}
form := findLoginForm(doc)
if form.Length() == 0 {
return fmt.Errorf("could not find nCore login form")
}
values := formValues(form)
values.Set(inputName(form, "text", "nev"), username)
values.Set(inputName(form, "password", "pass"), password)
if values.Get("submitted") == "" {
values.Set("submitted", "1")
}
if values.Get("set_lang") == "" {
values.Set("set_lang", "hu")
}
values.Set("ne_leptessen_ki", "1")
action, ok := form.Attr("action")
if !ok || strings.TrimSpace(action) == "" {
action = c.loginURL
}
target, err := url.Parse(c.loginURL)
if err != nil {
return err
}
actionURL, err := target.Parse(action)
if err != nil {
return err
}
postResp, err := c.httpClient.PostForm(actionURL.String(), values)
if err != nil {
return err
}
defer postResp.Body.Close()
if postResp.StatusCode < 200 || postResp.StatusCode >= 300 {
return fmt.Errorf("login returned %s", postResp.Status)
}
loginDoc, err := goquery.NewDocumentFromReader(postResp.Body)
if err != nil {
return err
}
if findLoginForm(loginDoc).Length() > 0 {
return fmt.Errorf("login failed; check credentials or browser-only checks")
}
return nil
}
func (c *Client) FetchHitRun() (model.HitRunPage, error) {
reqURL, err := url.Parse(c.hitRunURL)
if err != nil {
return model.HitRunPage{}, err
}
query := reqURL.Query()
query.Set("showall", "false")
reqURL.RawQuery = query.Encode()
resp, err := c.httpClient.Get(reqURL.String())
if err != nil {
return model.HitRunPage{}, err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return model.HitRunPage{}, fmt.Errorf("hitnrun page returned %s", resp.Status)
}
doc, err := goquery.NewDocumentFromReader(resp.Body)
if err != nil {
return model.HitRunPage{}, err
}
if findLoginForm(doc).Length() > 0 {
return model.HitRunPage{}, fmt.Errorf("nCore session is not authenticated")
}
return parseHitRunPage(doc), nil
}
func parseHitRunPage(doc *goquery.Document) model.HitRunPage {
page := model.HitRunPage{Stats: map[string]string{}}
alert := cleanText(doc.Find("#hnrAlert .fobox_tartalom").Clone().ChildrenFiltered("script").Remove().End())
page.Alert = alert
doc.Find(".dt").Each(func(_ int, label *goquery.Selection) {
value := label.NextFiltered(".dd")
key := strings.TrimSuffix(cleanText(label), ":")
val := cleanText(value)
if key != "" && val != "" {
page.Stats[key] = val
}
})
doc.Find(".hnr_torrents > div").Each(func(_ int, row *goquery.Selection) {
link := row.Find(".hnr_tname a").First()
href, _ := link.Attr("href")
title, _ := link.Attr("title")
if title == "" {
title = cleanText(row.Find(".hnr_tname"))
}
page.Torrents = append(page.Torrents, model.Torrent{
ID: torrentID(href),
Name: title,
URL: absoluteURL(href),
Start: cleanText(row.Find(".hnr_tstart")),
Updated: cleanText(row.Find(".hnr_tlastactive")),
Status: cleanText(row.Find(".hnr_tseed")),
Uploaded: cleanText(row.Find(".hnr_tup")),
Downloaded: cleanText(row.Find(".hnr_tdown")),
Remaining: cleanText(row.Find(".hnr_ttimespent")),
Ratio: cleanText(row.Find(".hnr_tratio")),
HnRMarked: row.Find(".hnr_tstart .stopped").Length() > 0,
})
})
return page
}
func findLoginForm(doc *goquery.Document) *goquery.Selection {
return doc.Find("form").FilterFunction(func(_ int, form *goquery.Selection) bool {
return form.Find("input[type='password']").Length() > 0
}).First()
}
func formValues(form *goquery.Selection) url.Values {
values := url.Values{}
form.Find("input").Each(func(_ int, input *goquery.Selection) {
name, ok := input.Attr("name")
if !ok || name == "" {
return
}
inputType := strings.ToLower(attr(input, "type", "text"))
if inputType == "submit" || inputType == "button" || inputType == "image" || inputType == "file" || inputType == "text" || inputType == "password" {
return
}
if (inputType == "checkbox" || inputType == "radio") && attr(input, "checked", "") == "" {
return
}
values.Set(name, attr(input, "value", ""))
})
return values
}
func inputName(form *goquery.Selection, inputType string, preferred string) string {
if form.Find("input[name='"+preferred+"']").Length() > 0 {
return preferred
}
if name, ok := form.Find("input[type='" + inputType + "']").First().Attr("name"); ok && name != "" {
return name
}
return preferred
}
func torrentID(href string) int64 {
parsed, err := url.Parse(href)
if err != nil {
return 0
}
id, err := strconv.ParseInt(parsed.Query().Get("id"), 10, 64)
if err != nil {
return 0
}
return id
}
func absoluteURL(href string) string {
if href == "" {
return ""
}
if strings.HasPrefix(href, "http://") || strings.HasPrefix(href, "https://") {
return href
}
return "https://ncore.pro/" + strings.TrimPrefix(href, "/")
}
func cleanText(selection *goquery.Selection) string {
return strings.Join(strings.Fields(selection.Text()), " ")
}
func attr(selection *goquery.Selection, name string, fallback string) string {
value, ok := selection.Attr(name)
if !ok {
return fallback
}
return value
}