SoXX a2fc1b7e32 feat(listing): populate Tags and CategorizedTags from figure data-tags
FA's beta listing pages emit each submission's tag list on the
figure's <img data-tags="..."> attribute, mixing prefixed system tags
(s_/c_/a_/u_/t_) with the unprefixed keyword list. Reading it during
gallery-page parse lets callers classify favorites/gallery/scraps/
browse/search/inbox items at scrape time, avoiding a /view/{id}
round-trip per submission.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 21:53:56 +02:00
2026-05-25 22:27:18 +02:00
2026-06-02 21:15:04 +02:00
2026-05-26 20:21:55 +02:00
2026-05-26 20:21:55 +02:00
2026-05-26 20:21:55 +02:00
2026-05-25 22:27:18 +02:00
2026-05-25 22:27:18 +02:00
2026-05-26 20:21:55 +02:00
2026-05-26 20:21:55 +02:00
2026-05-25 22:27:18 +02:00
2026-05-25 22:27:18 +02:00
2026-05-25 22:27:18 +02:00
2026-05-25 22:27:18 +02:00
2026-05-25 22:27:18 +02:00
2026-05-25 22:27:18 +02:00
2026-05-25 22:27:18 +02:00
2026-05-25 22:27:18 +02:00
2026-05-26 20:21:55 +02:00
2026-05-25 22:27:18 +02:00
2026-05-25 22:27:18 +02:00
2026-05-25 22:27:18 +02:00
2026-05-25 22:27:18 +02:00
2026-05-26 20:21:55 +02:00
2026-05-25 22:27:18 +02:00
2026-05-25 22:27:18 +02:00
2026-05-26 20:21:55 +02:00
2026-05-25 22:27:18 +02:00
2026-05-25 22:27:18 +02:00
2026-05-25 22:27:18 +02:00
2026-05-25 22:27:18 +02:00
2026-05-25 22:27:18 +02:00
2026-05-26 20:21:55 +02:00
2026-05-26 20:21:55 +02:00
2026-05-25 22:27:18 +02:00
2026-05-26 20:21:55 +02:00
2026-05-26 20:21:55 +02:00
2026-05-25 22:27:18 +02:00
2026-05-25 22:27:18 +02:00
2026-05-25 22:27:18 +02:00
2026-05-25 22:27:18 +02:00
2026-05-25 22:27:18 +02:00
2026-05-25 22:27:18 +02:00
2026-05-25 22:27:18 +02:00
2026-05-26 20:21:55 +02:00
2026-05-26 20:21:55 +02:00
2026-05-25 22:27:18 +02:00
2026-05-25 22:27:18 +02:00
2026-05-26 20:21:55 +02:00
2026-05-25 22:27:18 +02:00
2026-05-25 22:27:18 +02:00
2026-05-25 22:27:18 +02:00
2026-05-25 22:27:18 +02:00
2026-05-25 22:27:18 +02:00
2026-05-26 20:21:55 +02:00
2026-05-25 22:27:18 +02:00
2026-05-26 20:21:55 +02:00

go-fa-api

Go 1.25+ SDK for FurAffinity. FA has no JSON API, so this library scrapes the beta theme via Colly internally and exposes a strongly typed surface.

Features

Area What you get
Read GetSubmission · GetUser · GetJournal · Download
Iterators (iter.Seq2) Gallery · Scraps · Favorites · UserJournals · SubmissionComments · JournalComments
Discovery Browse(BrowseOptions) · Search(query, SearchOptions) full filter struct (rating/type/order/range/etc.)
Inbox SubmissionInbox (watched-users feed) · Notes · GetNote · Notifications (journals/watches/comments/favs/shouts)
Write Fav/Unfav · Watch/Unwatch · PostSubmissionComment · PostJournalComment · SendNote
Auth WithCookies(a, b) · WithCloudflare(cf_clearance) · WithUserAgent · WithSFW(SFWOn/Off/Auto)
Networking Built-in 1 req/s rate limiter · Cloudflare challenge detection · 429 Retry-After · 5xx exponential backoff · context.Context end-to-end
Experimental WithExperimentalJSONListings(true) listing pages merge from FA's embedded js-submissionData JSON first, HTML fallback if it's absent. More resilient to markup drift; off by default.

Pre-1.0: expect breaking changes. Beta theme only; classic theme is not parsed.

Install

go get git.anthrove.art/public/go-fa-api

Quickstart

package main

import (
    "context"
    "fmt"
    "log"
    "os"

    fa "git.anthrove.art/public/go-fa-api"
)

func main() {
    client := fa.New(
        fa.WithCookies(fa.Cookies{A: os.Getenv("FA_A"), B: os.Getenv("FA_B")}),
        fa.WithCloudflare(fa.CFCookies{Clearance: os.Getenv("CF_CLEARANCE")}),
        fa.WithUserAgent(os.Getenv("FA_UA")),
    )

    sub, err := client.GetSubmission(context.Background(), 12345678)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("%s by %s\n", sub.Title, sub.Author.DisplayName)

    for sub, err := range client.Gallery(context.Background(), "someuser", fa.ListOptions{MaxPages: 3}) {
        if err != nil {
            log.Fatal(err)
        }
        fmt.Println(sub.ID, sub.Title)
    }
}

Runnable examples in examples/: basic, gallery_dump, download, browse, search, inbox, notes, notifications.

Auth

FA's auth is cookie-based; Cloudflare gates submissions behind a clearance cookie bound to your browser's User-Agent. The SDK doesn't log in for you copy the values once:

  1. Log in at https://www.furaffinity.net in your browser.
  2. DevTools → ApplicationCookies → copy a, b, and cf_clearance.
  3. DevTools → Network → any FA request → copy the User-Agent header exactly. CF binds cf_clearance to that UA; mismatch = challenge.
  4. Pass them in via fa.WithCookies / fa.WithCloudflare / fa.WithUserAgent.

If FA challenges you later, your cf_clearance expired refresh it and the SDK will return fa.ErrCloudflareChallenge so you can detect it.

Account preference must be set to the beta theme (Site Preferences → Style → Beta).

Rate limiting

Every request (page fetches and downloads) passes through one token bucket. Default is 1 req/s, lowest safe for FA. Lives inside the http.RoundTripper so callers can't bypass it.

fa.New(fa.WithRateLimit(2*time.Second, 1)) // explicit
fa.New(fa.WithRequestsPerSecond(0.5))      // shorthand

Iterators

Paginated endpoints return iter.Seq2[*T, error]:

for sub, err := range client.Gallery(ctx, "someuser", fa.ListOptions{}) {
    if err != nil { return err }
    fmt.Println(sub.ID, sub.Title)
}
  • Lazy pages fetched on demand; break stops further fetches.
  • Terminal errors first error yields (nil, err) and stops the iterator.
  • One option-shape per iterator class: simple paginated iterators (Gallery / Scraps / Favorites / UserJournals / SubmissionInbox / Notes) take fa.ListOptions{StartPage, MaxPages}; filtered iterators (Search, Browse) take their own SearchOptions / BrowseOptions struct that folds the same pagination fields in.

Errors

Test with errors.Is:

Sentinel When What to do
ErrNotFound FA "not found" / deleted submission surface to user
ErrUnauthorized endpoint needs login or Login Required page refresh a/b
ErrCloudflareChallenge CF interposed a challenge refresh cf_clearance
ErrRateLimited 429 after retry budget exhausted slow down
ErrSystemMessage uncategorised FA system message inspect *SystemMessageError
ErrParse HTML didn't match selectors likely FA shifted markup open an issue

Test fixtures

Parsers are selector-driven; FA's HTML drifts. TestRefreshFixtures (build tag fixtures) hits live FA with your cookies and snapshots each curated page into testdata/html/. Parser *_RealFixture tests t.Skip cleanly when their file is absent, so the basic suite works without any cookies.

# Configure once (or edit scripts/test.sh):
export FA_A=... FA_B=... CF_CLEARANCE=... FA_UA='Mozilla/5.0 ...'
export FA_TEST_USER=yourlogin FA_TEST_SUB_ID=... FA_TEST_JOURNAL_ID=...
# Optional refinements: FA_TEST_SUB_STORY_ID, FA_TEST_USER_WITH_SHOUTS,
# FA_TEST_GALLERY_USER, FA_TEST_GALLERY_LAST_PAGE, FA_TEST_NOTE_ID,
# FA_TEST_SEARCH_QUERY each gates a separate fixture; unset = skip.

go test -tags=fixtures -run TestRefreshFixtures -v ./...   # or ./scripts/test.sh
go test ./...                                              # now *_RealFixture tests activate
Description
No description provided
Readme GPL-3.0 974 KiB
Languages
Go 99.5%
Shell 0.5%