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 }