194 lines
4.3 KiB
Go
194 lines
4.3 KiB
Go
package watchorder
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/PuerkitoBio/goquery"
|
|
)
|
|
|
|
const defaultUserAgent = "anime-relations-scraper/1.0 (+https://github.com/mkelvers/anime-relations)"
|
|
|
|
var idPattern = regexp.MustCompile(`/id/(\d+)`)
|
|
|
|
var ErrInvalidWatchOrderURL = errors.New("invalid watch order url")
|
|
var ErrWatchOrderMarkupNotFound = errors.New("watch order markup not found")
|
|
|
|
type WatchOrderEntry struct {
|
|
ID int `json:"id"`
|
|
Type string `json:"type"`
|
|
Title string `json:"title"`
|
|
TitleAlt string `json:"title_alt,omitempty"`
|
|
}
|
|
|
|
type WatchOrderResult struct {
|
|
ID int `json:"id"`
|
|
WatchOrder []WatchOrderEntry `json:"watch_order"`
|
|
}
|
|
|
|
type watchOrderRow struct {
|
|
id int
|
|
typeID int
|
|
title string
|
|
alternativeTitle string
|
|
}
|
|
|
|
func parseRootID(url string) (int, error) {
|
|
match := idPattern.FindStringSubmatch(url)
|
|
if len(match) != 2 {
|
|
return 0, ErrInvalidWatchOrderURL
|
|
}
|
|
|
|
id, err := strconv.Atoi(match[1])
|
|
if err != nil {
|
|
return 0, ErrInvalidWatchOrderURL
|
|
}
|
|
|
|
return id, nil
|
|
}
|
|
|
|
func fetchDocument(ctx context.Context, httpClient *http.Client, url string) (*goquery.Document, error) {
|
|
client := httpClient
|
|
if client == nil {
|
|
client = http.DefaultClient
|
|
}
|
|
|
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
|
}
|
|
|
|
request.Header.Set("User-Agent", defaultUserAgent)
|
|
|
|
response, err := client.Do(request)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("request failed: %w", err)
|
|
}
|
|
defer response.Body.Close()
|
|
|
|
if response.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("unexpected status code: %d", response.StatusCode)
|
|
}
|
|
|
|
document, err := goquery.NewDocumentFromReader(response.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse html: %w", err)
|
|
}
|
|
|
|
return document, nil
|
|
}
|
|
|
|
func extractTypeLabelsByID(doc *goquery.Document) map[int]string {
|
|
typeLabels := make(map[int]string)
|
|
|
|
doc.Find("#wo_type_filter label").Each(func(_ int, selection *goquery.Selection) {
|
|
input := selection.Find("input[type='checkbox']")
|
|
rawID, exists := input.Attr("value")
|
|
if !exists {
|
|
return
|
|
}
|
|
|
|
typeID, err := strconv.Atoi(strings.TrimSpace(rawID))
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
label := strings.TrimSpace(selection.Text())
|
|
if label == "" {
|
|
return
|
|
}
|
|
|
|
typeLabels[typeID] = label
|
|
})
|
|
|
|
return typeLabels
|
|
}
|
|
|
|
func parseAttrInt(selection *goquery.Selection, attrName string) (int, bool) {
|
|
rawValue, exists := selection.Attr(attrName)
|
|
if !exists {
|
|
return 0, false
|
|
}
|
|
|
|
value, err := strconv.Atoi(strings.TrimSpace(rawValue))
|
|
if err != nil {
|
|
return 0, false
|
|
}
|
|
|
|
return value, true
|
|
}
|
|
|
|
func extractRows(doc *goquery.Document) []watchOrderRow {
|
|
rows := make([]watchOrderRow, 0)
|
|
|
|
doc.Find("tr[data-id]").Each(func(_ int, selection *goquery.Selection) {
|
|
id, ok := parseAttrInt(selection, "data-id")
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
typeID, ok := parseAttrInt(selection, "data-type")
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
title := strings.TrimSpace(selection.Find(".wo_title").First().Text())
|
|
alternativeTitle := strings.TrimSpace(selection.Find(".uk-text-small").First().Text())
|
|
|
|
rows = append(rows, watchOrderRow{
|
|
id: id,
|
|
typeID: typeID,
|
|
title: title,
|
|
alternativeTitle: alternativeTitle,
|
|
})
|
|
})
|
|
|
|
return rows
|
|
}
|
|
|
|
func hasWatchOrderTable(doc *goquery.Document) bool {
|
|
return doc.Find("#wo_list").Length() > 0
|
|
}
|
|
|
|
func FetchWatchOrder(ctx context.Context, httpClient *http.Client, url string) (WatchOrderResult, error) {
|
|
rootID, err := parseRootID(url)
|
|
if err != nil {
|
|
return WatchOrderResult{}, err
|
|
}
|
|
|
|
doc, err := fetchDocument(ctx, httpClient, url)
|
|
if err != nil {
|
|
return WatchOrderResult{}, err
|
|
}
|
|
|
|
if !hasWatchOrderTable(doc) {
|
|
return WatchOrderResult{}, ErrWatchOrderMarkupNotFound
|
|
}
|
|
|
|
rows := extractRows(doc)
|
|
if len(rows) == 0 {
|
|
return WatchOrderResult{ID: rootID, WatchOrder: []WatchOrderEntry{}}, nil
|
|
}
|
|
|
|
typeByID := extractTypeLabelsByID(doc)
|
|
|
|
entries := make([]WatchOrderEntry, 0, len(rows))
|
|
for _, row := range rows {
|
|
typeName := strings.TrimSpace(typeByID[row.typeID])
|
|
|
|
entries = append(entries, WatchOrderEntry{
|
|
ID: row.id,
|
|
Type: typeName,
|
|
Title: row.title,
|
|
TitleAlt: row.alternativeTitle,
|
|
})
|
|
}
|
|
|
|
return WatchOrderResult{ID: rootID, WatchOrder: entries}, nil
|
|
}
|