69 lines
2.4 KiB
Go
69 lines
2.4 KiB
Go
package fa
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
|
|
"github.com/PuerkitoBio/goquery"
|
|
)
|
|
|
|
// listingJSONEntry is one row of FA's embedded js-submissionData blob —
|
|
// a JSON dictionary every listing page emits with stable metadata for
|
|
// each visible submission. The blob is keyed by submission ID (string)
|
|
// and survives most HTML reshuffles because it's consumed by FA's own
|
|
// front-end JS.
|
|
//
|
|
// Not every field appears on every page; description is sometimes truncated
|
|
// to ~500 chars with a trailing "…". We use this purely as a hinting
|
|
// source the HTML figure scrape remains authoritative for fields the
|
|
// JSON doesn't carry (ID, rating, thumbnail).
|
|
type listingJSONEntry struct {
|
|
Title string `json:"title"`
|
|
Description string `json:"description"` // BBCode source (may be truncated)
|
|
Username string `json:"username"` // display name with original casing
|
|
Lower string `json:"lower"` // URL-safe lowercase login
|
|
AvatarMtime string `json:"avatar_mtime"`
|
|
}
|
|
|
|
// listingJSONMap is the parsed js-submissionData blob: SubmissionID → entry.
|
|
type listingJSONMap map[SubmissionID]listingJSONEntry
|
|
|
|
// readListingJSON pulls and parses the <script id="js-submissionData">
|
|
// blob from doc. Returns nil if the tag is absent or the JSON is
|
|
// malformed the caller then falls back to HTML-only scraping. The
|
|
// JSON is keyed by string in the source; we deserialize directly into a
|
|
// SubmissionID map by exploiting the typed int unmarshal path.
|
|
func readListingJSON(doc *goquery.Document) listingJSONMap {
|
|
raw := strings.TrimSpace(doc.Find("script#js-submissionData").First().Text())
|
|
if raw == "" {
|
|
return nil
|
|
}
|
|
// Decode into string-keyed map first because Go's json package only
|
|
// accepts string-convertible keys from JSON object keys.
|
|
var stringKeyed map[string]listingJSONEntry
|
|
if err := json.Unmarshal([]byte(raw), &stringKeyed); err != nil {
|
|
return nil
|
|
}
|
|
out := make(listingJSONMap, len(stringKeyed))
|
|
for k, v := range stringKeyed {
|
|
id, err := parseID[SubmissionID](k)
|
|
if err != nil || id == 0 {
|
|
continue
|
|
}
|
|
out[id] = v
|
|
}
|
|
if len(out) == 0 {
|
|
return nil
|
|
}
|
|
return out
|
|
}
|
|
|
|
// avatarURLFromMtime builds FA's avatar CDN URL from the mtime + lowercase
|
|
// login. Returns "" when either piece is missing.
|
|
func avatarURLFromMtime(lower, mtime string) string {
|
|
if lower == "" || mtime == "" {
|
|
return ""
|
|
}
|
|
return "https://a.furaffinity.net/" + mtime + "/" + lower + ".gif"
|
|
}
|