9 Commits

Author SHA1 Message Date
8f4767966a feat(listing): add per-page methods with HasNext flag
GalleryPage / ScrapsPage / FavoritesPage return a ListingPage struct
carrying the page items, the 1-based page number, and a HasNext flag
that mirrors FA's "next page" link. This lets external scrapers drive
their own pagination loop (checkpoint resume, parallel workers,
custom throttling) without re-implementing the page-walking code.

The existing iter.Seq2-shaped methods now share the same per-page
primitive internally so behaviour stays in lock-step.
2026-06-02 22:28:49 +02:00
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
bc2d27f702 chore: add fixture-refresh helper script
scripts/refresh-user-fixture.sh wraps the TestRefreshFixtures
invocation: validates required env vars, warns on missing CF
credentials, runs the refresh, and re-runs the user parser tests
against the new fixture.
2026-06-02 21:30:28 +02:00
2d6e73a800 test(actions): make TestFindFavLinks resilient to capture state
Asserts exactly one of fav/unfav is set with a well-formed URL,
instead of hardcoding the +Fav direction. The previous test broke
whenever the capturing account favourited the target submission.
Also points the test at submission 12345678 (the documented default
FA_TEST_SUB_ID) so it matches what TestRefreshFixtures captures by
default.
2026-06-02 21:29:38 +02:00
79e8a35732 test: refresh fixtures with full target coverage
Picks up testdata/html files that were previously skipped by
TestRefreshFixtures because their optional env vars were unset:
gallery_page_last, search_results, note_view, plus a fresh
submission.html / gallery_page1 captured against the right targets.
2026-06-02 21:29:31 +02:00
5cb196940d chore: remove SDK_ISSUES.md
The umbrella audit (#4) had no open SDK issues remaining; the file is
no longer load-bearing.
2026-06-02 21:23:59 +02:00
25800bc753 test: refresh authenticated HTML fixtures
Re-captures testdata/html/*.html against the live site with valid
session cookies; the previous user.html was the logged-out interstitial,
which broke TestParseUser_RealFixture entirely. Bumps the expected
Stats.Views in that test to match the new fixture.
2026-06-02 21:23:52 +02:00
20fcad7fbb feat(submission): parse FA's prefixed system tags into CategorizedTags
FA renders its species/character/artist/type system tags as tag-block
anchors with a data-tag-name carrying a single-letter prefix
(s_/c_/a_-u_/t_) and a sibling tag-invalid span instead of a /search/
link. The existing keyword pass skips them, so they were lost.

Adds a Submission.CategorizedTags field exposing the four buckets with
the prefix stripped, plus an examples/categorized_tags runnable demo.
2026-06-02 21:15:30 +02:00
02479212bc chore: fix .gitignore filename typo
Renames .gitingore to .gitignore so the rules actually apply; .idea/
was leaking into the working tree as a result.
2026-06-02 21:15:04 +02:00
28 changed files with 19489 additions and 2461 deletions

View File

View File

@@ -1,268 +0,0 @@
# SDK issues FA Droid
Bugs whose root cause is in the **FA SDK** (`go-fa-api`, at
`/var/home/soxx/git/go-fa-api`, `replace`d in `go.mod`) not in this app.
The SDK is a **separate module, maintained and fixed by a separate AI**
that does not see this app's code or conversation context. This file is the
handoff: every entry is a self-contained bug report meant to be copy-pasted
to that AI as-is.
App-side bugs (frontend / go-service) live in `ISSUES.md`; fixed issues in
`SOLVED.md`. Features with UI but no backend are in `STUBS.md`; Wails/Android
stack traps in `PITFALLS.md`.
**Status legend:** `[ ]` open · `[~]` diagnosed handoff brief drafted ·
`[x]` fixed in the SDK (then moved to `SOLVED.md`)
**Numbering:** issue numbers are shared across `ISSUES.md`, `SOLVED.md` and
this file draw the next free number from the same sequence.
---
## SDK handoff brief required fields
Every entry MUST carry a handoff brief detailed enough to be copy-pasted to
the SDK AI as a standalone bug report. Do not write "sdk issue?" and stop —
that is not actionable for someone without this repo open.
A handoff brief must answer all seven of:
1. **SDK entry point** the exact exported function involved
(e.g. `Client.Fav(ctx, SubmissionID)` in `actions.go`). Name the file.
2. **How the app calls it** which `internal/services/*.go` wrapper
invokes it, and with what arguments.
3. **Observed behaviour** what the SDK call returns: an error (quote it
verbatim), a wrong value, or a silent no-op.
4. **Expected behaviour** what FurAffinity should do server-side and
what the SDK should return.
5. **Reproduction** concrete inputs (submission ID, username) and the
steps that trigger it.
6. **Suspected layer** HTML parsing (FA changed its markup?), request
shape (wrong form fields / missing CSRF / wrong URL), auth/cookies, or
rate-limiting. Point at the likely file: parsers are `*_parser.go`,
form posts are in `actions.go` / `*_post.go` / `*_send.go`.
7. **What is NOT the SDK** explicitly rule out the app layer so the SDK
AI does not chase a frontend bug. State what you verified.
If you cannot yet fill all seven fields, the issue stays `[~]` and the brief
lists exactly which diagnostics are still needed never hand over a
half-brief as if it were complete.
---
## Issues
### #4 [~] Audit the FA SDK for bugs
- **Where:** `go-fa-api` (sibling module, `replace`d in `go.mod`).
- **Task:** Standing umbrella review the SDK surface used by
`internal/services/` for incorrect parsing, missing endpoints, or wrong
request shapes.
- **Cause tag:** `sdk`
- **Progress:** The first audit pass produced #1, #5, #9, #10 and #14 **all
now fixed and verified** (#1/#5/#9/#10 in app v0.0.7, #14 in v0.0.10; see
`SOLVED.md`). #15 was then fixed too (SDK gave `WithResolvedAvatars` a
count limit; app v0.0.14). #17 (priority rate limiting) was implemented too
— verified in app v0.0.16. **No open SDK issues currently.** Keep this
entry as the umbrella for future audit passes; file each concrete finding
as its own numbered issue.
_No open SDK issues. Fixed ones are recorded in `SOLVED.md`._
---
## Add new SDK issues below
<!-- #N title (N = next free number, shared with ISSUES.md)
- Where:
- Symptom:
- Expected:
- Cause tag: sdk
- SDK handoff brief:
1. Entry point:
2. How the app calls it:
3. Observed behaviour:
4. Expected behaviour:
5. Reproduction:
6. Suspected layer:
7. What is NOT the SDK:
-->
### #18 [x] Rate limiter: upgrade from 2-level to N-level priority
**Implemented in the SDK update** `options.go` now exposes `WithPriority`
and a multi-level `Priority` type; `WithPrioritizedRateLimiting` honours it.
FA Droid follow-up (assign tiers to reads/writes/preload/crawl) is tracked as
app-side work, not an SDK issue.
- **Component:** `go-fa-api` `ratelimit.go`, `options.go`.
- **Today:** the limiter has exactly two priorities. `WithBackgroundPriority(ctx)`
marks a request background; everything else is foreground. Background
requests wait until no foreground request is queued. Enabled via
`WithPrioritizedRateLimiting(true)`.
- **Need:** a consumer (FA Droid) wants *three+* tiers, not two:
1. user-interactive (the page on screen, user write actions),
2. speculative neighbor preload (likely-next submissions),
3. bulk background crawl (inbox/watchlist warming).
With only two levels, neighbor preload must either compete with the live
page (tier 1) or interleave with the bulk crawl (tier 2) neither is right.
- **Proposed API:** keep `WithBackgroundPriority` working (maps to the lowest
level) and add `WithPriority(ctx, Priority)` where `Priority` is an ordered
enum, e.g. `PriorityInteractive`, `PriorityNormal` (default),
`PriorityLow`, `PriorityBackground`. The limiter serves waiting goroutines
highest-priority-first; the token emission rate is unchanged.
- **Compatibility:** default (no marker) must remain `PriorityNormal`;
`WithBackgroundPriority` must remain equivalent to `PriorityBackground`.
- **Why it matters to the app:** FA Droid will then assign tier 1 to
current-page reads + the write-action queue worker, tier `PriorityLow` to
neighbor preload, and `PriorityBackground` to the inbox crawler.
### #21 [x] GetSubmission doesn't expose the viewer's favorite state
- **Where:** `go-fa-api` `submission.go` (`Submission` struct + `parseSubmission`).
- **Symptom:** A submission the logged-in user has favorited shows the
favorite heart as *empty* on the app's submission detail page. There is no
way to know a submission's favorite state from a `GetSubmission` result.
- **Expected:** `GetSubmission` should report whether the authenticated viewer
has favorited the submission, so the UI can render the correct heart state.
- **Cause tag:** `sdk`
- **SDK handoff brief:**
1. **Entry point:** `Client.GetSubmission(ctx, SubmissionID)` in
`submission.go`, which builds the result via `parseSubmission(id, doc)`.
The returned `Submission` struct (`submission.go:17-38`) has fields
ID, Title, Author, PostedAt, Rating, Category, Type, Species, Gender,
Description, DescriptionText, Tags, FileURL, ThumbURL, Width, Height,
Stats, Folders, Prev, Next and **nothing indicating favorite state**.
2. **How the app calls it:** `SubmissionService.getCached` in
`internal/services/submission.go` calls `GetSubmission`, then
`dto.FromSubmission` (`internal/dto/types.go`) copies the struct to the
wire DTO. The frontend (`SubmissionView.svelte`) does
`favorited = !!sub.favorited`.
3. **Observed behaviour:** `GetSubmission` returns a `*Submission` with no
favorite-state field. It is not a wrong value or an error the datum is
simply absent from the SDK's public type.
4. **Expected behaviour:** When `/view/{id}/` is fetched with valid `a`/`b`
cookies, FA renders either a `+Fav` (`/fav/{id}/...`) or a `Fav`
(`/unfav/{id}/...`) anchor exactly one, matching the viewer's current
state. The SDK should surface this on `Submission`, e.g. a new
`Favorited bool` field (final name your call), set true when the page
shows the `/unfav/` link. On an anonymous (no-cookie) fetch neither link
is present → `Favorited` false, which is correct.
5. **Reproduction:** With cookies set, favorite submission X on FA, then
call `GetSubmission(ctx, X)` the result cannot express that X is
favorited.
6. **Suspected layer:** HTML parsing `parseSubmission`. The SDK *already*
has the exact parser: `findFavLinks(doc, subID) (favURL, unfavURL string)`
in `actions.go` (used by `toggleFavorite` for its idempotency check).
`unfavURL != ""` means "currently favorited." `parseSubmission` just needs
to run that check and set the new field. No new scraping logic required.
7. **What is NOT the SDK:** The app side is verified ready and correct.
`SubmissionView.svelte` already reads `sub.favorited` and types it
(`favorited?: boolean`); `getCached` correctly busts and re-fetches the
submission cache after a fav write (via `WriteService` / the action
queue). The value never reaches the app only because the SDK `Submission`
struct has no field to carry it `dto.FromSubmission` has nothing to map.
- **Related (please also check):** the same gap likely exists for *watch*
state `findWatchLinks` exists in `actions.go`, but the `User` struct may
not expose whether the viewer watches that user. Worth fixing in the same
pass.
- **When it lands (FA Droid follow-up):** add `Favorited bool` to
`dto.Submission` (`json:"favorited"`), map it in `dto.FromSubmission`, and
regenerate bindings. The frontend already consumes `sub.favorited`, so no UI
change is needed.
- **Status:** done SDK update added `Submission.Favorited`; app wired it in
v0.0.22.
### #23 [ ] SubmissionInbox yields only the first page (~72 items)
- **Where:** `go-fa-api` `inbox.go` (`SubmissionInbox` + `parseSubmissionInboxPage`).
- **Symptom:** The new-submission inbox shows only ~72 items even when the
account has thousands pending. A user with ~4718 inbox submissions sees one
page.
- **Expected:** `SubmissionInbox` should walk every cursor page until FA stops
rendering a "Next 72" link.
- **Cause tag:** `sdk`
- **SDK handoff brief:**
1. **Entry point:** `Client.SubmissionInbox(ctx, ListOptions)` in `inbox.go`,
whose iterator follows `parseSubmissionInboxPage`'s returned `nextURL`.
2. **How the app calls it:** `InboxService.StreamSubmissions`
(`internal/services/inbox.go`) runs one goroutine that does
`for sub, err := range client.SubmissionInbox(ctx, fa.ListOptions{})`
`ListOptions{}` means `MaxPages: 0` (unbounded), so the app does not
cap the crawl.
3. **Observed behaviour:** the `range` completes after ~72 items the
iterator yields one page then ends. Verified on-device: the app's crawl
channel (fed one item per `yield`) closes after exactly one ~72-item
chunk, so `StreamSubmissions` reports `HasMore: false` immediately after
page 1.
4. **Expected behaviour:** FA's `/msg/submissions/` paginates via a
"Next 72" link encoding a from-id cursor; the iterator should follow it
across all ~66 pages for a 4718-item inbox.
5. **Reproduction:** logged-in client with a large submission inbox;
`count := 0; for range client.SubmissionInbox(ctx, ListOptions{}) { count++ }`
yields ~72, not the true total.
6. **Suspected layer:** HTML parsing `parseSubmissionInboxPage`. Its next-
cursor selector is `div.messagecenter-navigation a.button.more`; if FA
changed that markup the parser returns `nextURL == ""` and the iterator
stops after page 1. Check the selector against current `/msg/submissions/`
HTML (and the cursor-URL construction).
7. **What is NOT the SDK:** app side verified. `StreamSubmissions` ranges the
iterator fully on a single goroutine and streams everything it yields; it
passes `ListOptions{}` (no `MaxPages` cap). On-device logging shows the
crawl ends after one chunk the iterator simply stops yielding.
### #24 [x] Request logger drops `context.Context` (breaks trace propagation)
**Fixed in the SDK** `transport.go`'s `logRequest` now emits its record via
`logger.InfoContext(req.Context(), "fa.request", …)` instead of `logger.Info`,
so a context-aware `slog.Handler` recovers the caller's active span and the
HTTP span nests under the RPC span. A regression test,
`TestTransport_LogRequest_PropagatesRequestContext` in `transport_test.go`,
installs a context-capturing handler and asserts a sentinel value threaded
through `req.Context()` reaches the slog record guarding against a silent
revert to `Info`.
- **Where:** `go-fa-api` the HTTP transport's request logging (`transport.go`,
the `logRequest` helper / wherever `slog` records an outgoing request).
- **Type:** Enhancement, not a bug the SDK works correctly; this is a
one-line change the app needs for distributed tracing.
- **Symptom:** The app (FA Droid, WI-10) added OpenTelemetry spans. An app RPC
opens an `rpc` span and threads its `context.Context` into the SDK call. The
SDK's per-request `slog` line is currently emitted with `logger.Info(...)`,
which carries **no context** so the app's `slog` handler cannot recover the
active span, and each HTTP span becomes an unparented root instead of a child
of the RPC span.
- **Expected:** the request log record should carry the request's context so a
context-aware `slog.Handler` can read the active span from it.
- **The fix (one line):** change the request-logging call from
`logger.Info("fa.request", …)` to
`logger.InfoContext(req.Context(), "fa.request", …)` (use the `*http.Request`'s
own `Context()`). `slog` already supports `InfoContext`; no API change, no new
dependency.
- **Cause tag:** `sdk`
- **SDK handoff brief:**
1. **SDK entry point:** the HTTP transport `RoundTrip` / request path in
`transport.go` specifically the `slog` call that logs each outgoing FA
request (`logRequest`, or inline). All SDK client methods route through it.
2. **How the app calls it:** every `internal/services/*.go` wrapper calls a
`Client.*` method with a `context.Context` that now carries an OTel span;
that ctx reaches the `*http.Request` (`req.Context()`).
3. **Observed behaviour:** the request `slog` record is created without a
context, so `Handler.Handle` receives `context.Background()`.
4. **Expected behaviour:** the record carries `req.Context()`, so a
context-aware handler can extract the active span.
5. **Reproduction:** with WI-10's `diag` slog handler installed, every HTTP
span has `parentSpanId == ""` even when the call was made inside an RPC
span. After the `InfoContext` change, the HTTP span nests under the RPC
span on the same trace id.
6. **Suspected layer:** request shape / logging purely the logging call,
no parsing or auth involved.
7. **What is NOT the SDK behaviour-wise:** no functional SDK behaviour
changes request execution, parsing, retries, rate-limiting are all
untouched. This only changes which `slog` method is used so context flows.
- **Note:** WI-10 applied this change to the *local* `replace`d working copy of
`go-fa-api` so FA Droid v0.0.33 has working rpc→http span linkage. It is
**not committed in the SDK repo**. Until it is upstreamed, a clean SDK
checkout will degrade gracefully HTTP spans become valid but unparented
roots (no crash, no data loss, only the rpc↔http link is lost).

View File

@@ -364,16 +364,25 @@ func TestFindFavLinks_RealFixture(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("read doc: %v", err) t.Fatalf("read doc: %v", err)
} }
// submission.html was captured for submission 65052636 (a "+Fav" page). // submission.html was captured for submission 12345678. FA renders either
favURL, unfavURL := findFavLinks(doc, 65052636) // a +Fav or a -Fav anchor depending on the capturing account's current
if favURL == "" { // state never both, never neither (when authenticated). The test stays
t.Error("findFavLinks: favURL empty +Fav anchor not found in real markup") // direction-agnostic so it doesn't break when the capturing account
} // favourites/unfavourites this submission.
if !strings.Contains(favURL, "/fav/65052636/") || !strings.Contains(favURL, "key=") { favURL, unfavURL := findFavLinks(doc, 12345678)
t.Errorf("findFavLinks: favURL = %q; want a /fav/65052636/?key=... URL", favURL) switch {
} case favURL == "" && unfavURL == "":
if unfavURL != "" { t.Error("findFavLinks: both URLs empty fav/unfav anchor not found in real markup")
t.Errorf("findFavLinks: unfavURL = %q; want empty on a not-yet-faved page", unfavURL) case favURL != "" && unfavURL != "":
t.Errorf("findFavLinks: both URLs set (fav=%q unfav=%q); expected exactly one", favURL, unfavURL)
case favURL != "":
if !strings.Contains(favURL, "/fav/12345678/") || !strings.Contains(favURL, "key=") {
t.Errorf("findFavLinks: favURL = %q; want a /fav/12345678/?key=... URL", favURL)
}
case unfavURL != "":
if !strings.Contains(unfavURL, "/unfav/12345678/") || !strings.Contains(unfavURL, "key=") {
t.Errorf("findFavLinks: unfavURL = %q; want a /unfav/12345678/?key=... URL", unfavURL)
}
} }
} }

View File

@@ -0,0 +1,90 @@
// categorized_tags demonstrates how the SDK groups FA's prefixed system
// tags into the four CategorizedTags buckets: Species (s_), Characters (c_),
// Artists (a_/u_), and Types (t_). Each bucket is printed on its own so the
// example covers every aspect of the feature.
//
// Runs anonymously by default. Set FA_A / FA_B (and ideally CF_CLEARANCE +
// FA_UA) to authenticate when the target submission requires it.
//
// go run ./examples/categorized_tags 12345678
package main
import (
"context"
"fmt"
"log"
"os"
"strconv"
"strings"
fa "git.anthrove.art/public/go-fa-api"
)
func main() {
if len(os.Args) < 2 {
log.Fatalf("usage: %s <submission-id>", os.Args[0])
}
id, err := strconv.ParseInt(os.Args[1], 10, 64)
if err != nil {
log.Fatalf("invalid submission id: %v", err)
}
opts := []fa.Option{fa.WithUserAgent("go-fa-api-example/0.1")}
if a, b := os.Getenv("FA_A"), os.Getenv("FA_B"); a != "" && b != "" {
log.Printf("using FA_A/FA_B cookies for authenticated request")
opts = []fa.Option{
fa.WithCookies(fa.Cookies{A: a, B: b}),
fa.WithCloudflare(fa.CFCookies{Clearance: os.Getenv("CF_CLEARANCE")}),
fa.WithUserAgent(envOr("FA_UA", "go-fa-api-example/0.1")),
}
}
client := fa.New(opts...)
sub, err := client.GetSubmission(context.Background(), fa.SubmissionID(id))
if err != nil {
log.Fatalf("GetSubmission: %v", err)
}
fmt.Printf("%s\nby %s\n\n", sub.Title, sub.Author.DisplayName)
fmt.Println("=== Plain keyword tags (no prefix) ===")
if len(sub.Tags) == 0 {
fmt.Println(" (none)")
} else {
fmt.Println(" " + strings.Join(sub.Tags, ", "))
}
ct := sub.CategorizedTags
fmt.Println()
fmt.Println("=== Species (s_) ===")
printBucket(ct.Species, "s_")
fmt.Println()
fmt.Println("=== Characters (c_) ===")
printBucket(ct.Characters, "c_")
fmt.Println()
fmt.Println("=== Artists (a_ / u_) ===")
printBucket(ct.Artists, "a_")
fmt.Println()
fmt.Println("=== Types (t_) ===")
printBucket(ct.Types, "t_")
}
func printBucket(items []string, prefix string) {
if len(items) == 0 {
fmt.Println(" (none)")
return
}
for _, v := range items {
fmt.Printf(" %s%s\n", prefix, v)
}
}
func envOr(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}

View File

@@ -12,7 +12,8 @@ import (
// Gallery iterates the submissions in a user's main gallery, newest first. // Gallery iterates the submissions in a user's main gallery, newest first.
// //
// Each yielded *Submission carries only the fields visible on the listing // Each yielded *Submission carries only the fields visible on the listing
// page: ID, Title, Author (for favorites), ThumbURL, and Rating. Call // page: ID, Title, Author (for favorites), ThumbURL, Rating, and the Tags
// / CategorizedTags parsed from the figure's data-tags attribute. Call
// [Client.GetSubmission] with the ID to load the full record. // [Client.GetSubmission] with the ID to load the full record.
func (c *Client) Gallery(ctx context.Context, name string, opts ListOptions, reqOpts ...Option) iter.Seq2[*Submission, error] { func (c *Client) Gallery(ctx context.Context, name string, opts ListOptions, reqOpts ...Option) iter.Seq2[*Submission, error] {
return c.listGallerySection(ctx, name, urls.Gallery, opts, reqOpts) return c.listGallerySection(ctx, name, urls.Gallery, opts, reqOpts)
@@ -30,6 +31,50 @@ func (c *Client) Favorites(ctx context.Context, name string, opts ListOptions, r
return c.listGallerySection(ctx, name, urls.Favorites, opts, reqOpts) return c.listGallerySection(ctx, name, urls.Favorites, opts, reqOpts)
} }
// GalleryPage fetches a single page of /gallery/{name}/ and returns the
// items along with whether more pages exist. Pages are 1-based; pass 0 or
// 1 for the first page. Use this when driving pagination manually
// (resuming from a checkpoint, distributing pages across workers); use
// [Client.Gallery] when you just want every item in order.
func (c *Client) GalleryPage(ctx context.Context, name string, page int, reqOpts ...Option) (*ListingPage, error) {
return c.fetchListingPage(ctx, name, page, urls.Gallery, reqOpts)
}
// ScrapsPage is the single-page counterpart to [Client.Scraps]. See
// [Client.GalleryPage] for usage notes.
func (c *Client) ScrapsPage(ctx context.Context, name string, page int, reqOpts ...Option) (*ListingPage, error) {
return c.fetchListingPage(ctx, name, page, urls.Scraps, reqOpts)
}
// FavoritesPage is the single-page counterpart to [Client.Favorites]. See
// [Client.GalleryPage] for usage notes.
func (c *Client) FavoritesPage(ctx context.Context, name string, page int, reqOpts ...Option) (*ListingPage, error) {
return c.fetchListingPage(ctx, name, page, urls.Favorites, reqOpts)
}
// fetchListingPage is the shared per-page primitive used by
// GalleryPage / ScrapsPage / FavoritesPage and the iterator engine.
func (c *Client) fetchListingPage(
ctx context.Context,
name string,
page int,
urlFn func(string, int) string,
reqOpts []Option,
) (*ListingPage, error) {
if page < 1 {
page = 1
}
out := &ListingPage{Page: page}
err := c.fetch(ctx, urlFn(name, page), func(doc *goquery.Document) error {
out.Items, out.HasNext = parseGalleryPage(doc, c.cfg.jsonListings)
return nil
}, reqOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// listGallerySection is the shared engine for Gallery / Scraps / Favorites. // listGallerySection is the shared engine for Gallery / Scraps / Favorites.
// urlFn picks the section-specific URL builder; the rest of the pagination // urlFn picks the section-specific URL builder; the rest of the pagination
// machinery is identical across all three sections. // machinery is identical across all three sections.
@@ -47,28 +92,21 @@ func (c *Client) listGallerySection(
if opts.reachedLimit(pagesFetched) { if opts.reachedLimit(pagesFetched) {
return return
} }
var ( lp, err := c.fetchListingPage(ctx, name, page, urlFn, reqOpts)
items []*Submission
hasNext bool
)
err := c.fetch(ctx, urlFn(name, page), func(doc *goquery.Document) error {
items, hasNext = parseGalleryPage(doc, c.cfg.jsonListings)
return nil
}, reqOpts...)
if err != nil { if err != nil {
yield(nil, err) yield(nil, err)
return return
} }
pagesFetched++ pagesFetched++
if len(items) == 0 { if len(lp.Items) == 0 {
return return
} }
for _, s := range items { for _, s := range lp.Items {
if !yield(s, nil) { if !yield(s, nil) {
return return
} }
} }
if !hasNext { if !lp.HasNext {
return return
} }
page++ page++

149
gallery_page_test.go Normal file
View File

@@ -0,0 +1,149 @@
package fa
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"sync/atomic"
"testing"
)
// fakeGalleryPage builds a minimal gallery-page response with two figures.
// hasNext controls whether the "Next" anchor is included so detectNextPage
// flips.
func fakeGalleryPage(startID int, hasNext bool) string {
var b strings.Builder
b.WriteString(`<html><body>`)
for i := 0; i < 2; i++ {
id := startID + i
fmt.Fprintf(&b, `
<figure id="sid-%d" class="t-image r-general">
<a href="/view/%d/" title="Sub %d">
<img data-tags="u_someartist c_artwork_digital t_all s_wolf wolf" src="//d.example/t/%d.png"/>
</a>
<figcaption>
<p>Sub %d</p>
<a href="/user/someartist/">someartist</a>
</figcaption>
</figure>`, id, id, id, id, id)
}
if hasNext {
b.WriteString(`<a class="button standard" href="/gallery/u/2/">Next</a>`)
}
b.WriteString(`</body></html>`)
return b.String()
}
func TestGalleryPage_HasNextPropagates(t *testing.T) {
var requests atomic.Int32
mux := http.NewServeMux()
mux.HandleFunc("/gallery/u/", func(w http.ResponseWriter, _ *http.Request) {
requests.Add(1)
_, _ = w.Write([]byte(fakeGalleryPage(1000, true)))
})
mux.HandleFunc("/gallery/u/2/", func(w http.ResponseWriter, _ *http.Request) {
requests.Add(1)
_, _ = w.Write([]byte(fakeGalleryPage(2000, false)))
})
srv := httptest.NewServer(mux)
defer srv.Close()
client := newE2EClient(t, srv)
first, err := client.GalleryPage(context.Background(), "u", 1)
if err != nil {
t.Fatalf("GalleryPage(1): %v", err)
}
if first.Page != 1 {
t.Errorf("first.Page = %d; want 1", first.Page)
}
if !first.HasNext {
t.Error("first.HasNext = false; want true")
}
if len(first.Items) != 2 {
t.Fatalf("first.Items len = %d; want 2", len(first.Items))
}
if first.Items[0].ID != 1000 {
t.Errorf("first.Items[0].ID = %d; want 1000", first.Items[0].ID)
}
// data-tags routed through to the page method too.
if len(first.Items[0].Tags) == 0 || len(first.Items[0].CategorizedTags.Species) == 0 {
t.Errorf("first.Items[0]: tags not populated from data-tags: %+v", first.Items[0])
}
last, err := client.GalleryPage(context.Background(), "u", 2)
if err != nil {
t.Fatalf("GalleryPage(2): %v", err)
}
if last.HasNext {
t.Error("last.HasNext = true; want false (last page)")
}
if last.Page != 2 {
t.Errorf("last.Page = %d; want 2", last.Page)
}
if requests.Load() != 2 {
t.Errorf("requests = %d; want 2", requests.Load())
}
}
func TestGalleryPage_ZeroPageDefaultsToOne(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/gallery/u/", func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte(fakeGalleryPage(1, false)))
})
srv := httptest.NewServer(mux)
defer srv.Close()
client := newE2EClient(t, srv)
page, err := client.GalleryPage(context.Background(), "u", 0)
if err != nil {
t.Fatalf("GalleryPage(0): %v", err)
}
if page.Page != 1 {
t.Errorf("page.Page = %d; want 1 (zero should normalise)", page.Page)
}
}
func TestScrapsPage_HitsScrapsRoute(t *testing.T) {
var gotPath string
mux := http.NewServeMux()
mux.HandleFunc("/scraps/u/", func(w http.ResponseWriter, r *http.Request) {
gotPath = r.URL.Path
_, _ = w.Write([]byte(fakeGalleryPage(1, false)))
})
srv := httptest.NewServer(mux)
defer srv.Close()
client := newE2EClient(t, srv)
if _, err := client.ScrapsPage(context.Background(), "u", 1); err != nil {
t.Fatalf("ScrapsPage: %v", err)
}
if gotPath != "/scraps/u/" {
t.Errorf("gotPath = %q; want /scraps/u/", gotPath)
}
}
func TestFavoritesPage_HitsFavoritesRoute(t *testing.T) {
var gotPath string
mux := http.NewServeMux()
mux.HandleFunc("/favorites/u/", func(w http.ResponseWriter, r *http.Request) {
gotPath = r.URL.Path
_, _ = w.Write([]byte(fakeGalleryPage(1, true)))
})
srv := httptest.NewServer(mux)
defer srv.Close()
client := newE2EClient(t, srv)
p, err := client.FavoritesPage(context.Background(), "u", 1)
if err != nil {
t.Fatalf("FavoritesPage: %v", err)
}
if gotPath != "/favorites/u/" {
t.Errorf("gotPath = %q; want /favorites/u/", gotPath)
}
if !p.HasNext {
t.Error("p.HasNext = false; want true")
}
}

View File

@@ -85,6 +85,15 @@ func parseGalleryFigure(sel *goquery.Selection, jsonData listingJSONMap) *Submis
} }
} }
// data-tags on the figure's <img> carries both the unprefixed keyword
// list and the prefixed system tags (s_/c_/a_/u_/t_). Splitting it lets
// callers classify listing items without an extra /view/ fetch.
if img := sel.Find("img[data-tags]").First(); img.Length() > 0 {
if raw, ok := img.Attr("data-tags"); ok {
applyListingDataTags(s, raw)
}
}
// JSON enrichment preferred sources for the fields it carries. // JSON enrichment preferred sources for the fields it carries.
if jsonData != nil { if jsonData != nil {
if entry, ok := jsonData[id]; ok { if entry, ok := jsonData[id]; ok {
@@ -105,3 +114,35 @@ func parseGalleryFigure(sel *goquery.Selection, jsonData listingJSONMap) *Submis
return s return s
} }
// applyListingDataTags splits the whitespace-separated data-tags attribute
// FA emits on listing-page <img> elements and routes each token to either
// CategorizedTags (when the token has a known single-letter prefix
// s_/c_/a_/u_/t_) or Tags (everything else).
//
// The prefix mapping mirrors the /view/ parser in submission_parser.go so a
// listing-path Submission carries the same categorisation a /view/-path one
// would, modulo tokens FA can't represent in this flat attribute (multi-word
// tags, the a_ vs u_ distinction).
func applyListingDataTags(s *Submission, raw string) {
for _, tok := range strings.Fields(raw) {
if len(tok) >= 3 && tok[1] == '_' {
name := tok[2:]
switch tok[0] {
case 's':
s.CategorizedTags.Species = append(s.CategorizedTags.Species, name)
continue
case 'c':
s.CategorizedTags.Characters = append(s.CategorizedTags.Characters, name)
continue
case 'a', 'u':
s.CategorizedTags.Artists = append(s.CategorizedTags.Artists, name)
continue
case 't':
s.CategorizedTags.Types = append(s.CategorizedTags.Types, name)
continue
}
}
s.Tags = append(s.Tags, tok)
}
}

View File

@@ -62,6 +62,99 @@ func TestParseGalleryPage_Synthetic(t *testing.T) {
} }
} }
func TestParseGalleryFigure_DataTags(t *testing.T) {
const html = `<html><body>
<figure id="sid-2001" class="t-image r-general">
<a href="/view/2001/" title="Mixed Tags">
<img data-tags="u_someartist c_artwork_digital t_all s_wolf wolf solo digital landscape" src="//d.example/thumb/2001.png"/>
</a>
</figure>
<figure id="sid-2002" class="t-image r-general">
<a href="/view/2002/" title="No Tags">
<img src="//d.example/thumb/2002.png"/>
</a>
</figure>
<figure id="sid-2003" class="t-image r-general">
<a href="/view/2003/" title="Only Keywords">
<img data-tags="wolf solo" src="//d.example/thumb/2003.png"/>
</a>
</figure>
</body></html>`
doc, err := goquery.NewDocumentFromReader(strings.NewReader(html))
if err != nil {
t.Fatalf("setup: %v", err)
}
items, _ := parseGalleryPage(doc, false)
if len(items) != 3 {
t.Fatalf("items = %d; want 3", len(items))
}
// Mixed prefixed + unprefixed.
mixed := items[0]
wantTags := []string{"wolf", "solo", "digital", "landscape"}
if !equalStrings(mixed.Tags, wantTags) {
t.Errorf("items[0].Tags = %v; want %v", mixed.Tags, wantTags)
}
if !equalStrings(mixed.CategorizedTags.Species, []string{"wolf"}) {
t.Errorf("items[0].Species = %v", mixed.CategorizedTags.Species)
}
if !equalStrings(mixed.CategorizedTags.Characters, []string{"artwork_digital"}) {
t.Errorf("items[0].Characters = %v", mixed.CategorizedTags.Characters)
}
if !equalStrings(mixed.CategorizedTags.Types, []string{"all"}) {
t.Errorf("items[0].Types = %v", mixed.CategorizedTags.Types)
}
if !equalStrings(mixed.CategorizedTags.Artists, []string{"someartist"}) {
t.Errorf("items[0].Artists = %v", mixed.CategorizedTags.Artists)
}
// Missing data-tags: both slices stay nil.
if items[1].Tags != nil {
t.Errorf("items[1].Tags = %v; want nil", items[1].Tags)
}
if items[1].CategorizedTags.Species != nil ||
items[1].CategorizedTags.Characters != nil ||
items[1].CategorizedTags.Artists != nil ||
items[1].CategorizedTags.Types != nil {
t.Errorf("items[1].CategorizedTags = %+v; want zero", items[1].CategorizedTags)
}
// Unprefixed-only: everything lands in Tags.
if !equalStrings(items[2].Tags, []string{"wolf", "solo"}) {
t.Errorf("items[2].Tags = %v", items[2].Tags)
}
if items[2].CategorizedTags.Species != nil {
t.Errorf("items[2].Species = %v; want nil", items[2].CategorizedTags.Species)
}
}
func TestParseGalleryPage_RealFixtureTags(t *testing.T) {
raw := loadFixture(t, "gallery_page1.html")
doc, err := goquery.NewDocumentFromReader(bytes.NewReader(raw))
if err != nil {
t.Fatalf("read doc: %v", err)
}
items, _ := parseGalleryPage(doc, false)
if len(items) == 0 {
t.Fatal("real fixture: no items parsed")
}
var withTags, withSpecies int
for _, it := range items {
if len(it.Tags) > 0 {
withTags++
}
if len(it.CategorizedTags.Species) > 0 {
withSpecies++
}
}
if withTags == 0 {
t.Error("no items got Tags populated from data-tags")
}
if withSpecies == 0 {
t.Error("no items got CategorizedTags.Species populated from data-tags")
}
}
func TestParseGalleryPage_RealFixture(t *testing.T) { func TestParseGalleryPage_RealFixture(t *testing.T) {
raw := loadFixture(t, "gallery_page1.html") raw := loadFixture(t, "gallery_page1.html")
doc, err := goquery.NewDocumentFromReader(bytes.NewReader(raw)) doc, err := goquery.NewDocumentFromReader(bytes.NewReader(raw))

View File

@@ -6,6 +6,24 @@ import (
"github.com/PuerkitoBio/goquery" "github.com/PuerkitoBio/goquery"
) )
// ListingPage is one page of a listing endpoint (Gallery / Scraps /
// Favorites). It carries everything an external caller needs to drive
// pagination by hand: the items, the 1-based page number that produced
// them, and whether FA exposed a "next page" link.
//
// External scrapers that want to manage their own loop (resume from a
// checkpoint, run pages in parallel, throttle differently) should call
// the per-page methods ([Client.GalleryPage], [Client.ScrapsPage],
// [Client.FavoritesPage]) and stop when HasNext is false. Callers that
// just want every item in order should keep using the iter.Seq2-shaped
// methods ([Client.Gallery] et al.), which use the same primitive
// internally.
type ListingPage struct {
Items []*Submission
HasNext bool
Page int // 1-based page number this result corresponds to
}
// ListOptions configures the pagination of a simple iterator method like // ListOptions configures the pagination of a simple iterator method like
// [Client.Gallery] or [Client.Notes]. Filtered iterators ([Client.Search], // [Client.Gallery] or [Client.Notes]. Filtered iterators ([Client.Search],
// [Client.Browse]) use their own option structs that fold the same fields // [Client.Browse]) use their own option structs that fold the same fields

43
scripts/refresh-user-fixture.sh Executable file
View File

@@ -0,0 +1,43 @@
#!/usr/bin/env bash
# Refresh testdata/html/user.html (and any other fixtures whose env vars are
# set) by re-running TestRefreshFixtures against live FA with your cookies.
#
# Required:
# FA_A, FA_B session cookies
# FA_TEST_USER your FA username (lowercase, e.g. soxx-thefennec)
#
# Strongly recommended (otherwise Cloudflare will likely block the request):
# CF_CLEARANCE cf_clearance cookie from the same browser session
# FA_UA the exact User-Agent that produced CF_CLEARANCE
#
# Usage:
# FA_A=... FA_B=... CF_CLEARANCE=... FA_UA="..." FA_TEST_USER=soxx-thefennec \
# ./scripts/refresh-user-fixture.sh
#
# Or export the vars in your shell first, then just run the script.
set -euo pipefail
cd "$(dirname "$0")/.."
missing=()
for var in FA_A FA_B FA_TEST_USER; do
if [[ -z "${!var:-}" ]]; then
missing+=("$var")
fi
done
if (( ${#missing[@]} )); then
echo "error: missing required env vars: ${missing[*]}" >&2
exit 1
fi
if [[ -z "${CF_CLEARANCE:-}" || -z "${FA_UA:-}" ]]; then
echo "warning: CF_CLEARANCE or FA_UA not set; the request will likely hit a Cloudflare challenge" >&2
fi
echo "refreshing fixtures for user=$FA_TEST_USER"
go test -tags=fixtures -run TestRefreshFixtures -v ./...
echo
echo "re-running user parser tests against the new fixture"
go test -run TestParseUser ./...

View File

@@ -13,6 +13,15 @@ import (
"git.anthrove.art/public/go-fa-api/internal/urls" "git.anthrove.art/public/go-fa-api/internal/urls"
) )
// CategorizedTags groups FA's prefixed system tags by category. Names are
// stored without their prefix (e.g. "s_hybrid_species" → Species "hybrid_species").
type CategorizedTags struct {
Species []string
Characters []string
Artists []string
Types []string
}
// Submission is a fully resolved FA submission as seen on /view/{id}/. // Submission is a fully resolved FA submission as seen on /view/{id}/.
type Submission struct { type Submission struct {
ID SubmissionID ID SubmissionID
@@ -26,7 +35,20 @@ type Submission struct {
Gender Gender Gender Gender
Description string // raw HTML; sanitise before rendering to a browser Description string // raw HTML; sanitise before rendering to a browser
DescriptionText string // plaintext convenience DescriptionText string // plaintext convenience
Tags []string // Tags holds the user-supplied keyword tags. On /view/-path Submissions
// these come from div.submission-tags anchors. On listing-path
// Submissions (Gallery/Scraps/Favorites/Browse/Search/SubmissionInbox)
// they come from the figure's data-tags attribute, which carries the
// same keywords FA renders on /view/ for that submission.
Tags []string
// CategorizedTags groups FA's prefixed system tags by category.
// On /view/-path Submissions FA emits these as tag-block entries inside
// div.submission-tags with prefixes s_ (species), c_ (character),
// a_/u_ (artist), and t_ (type). On listing-path Submissions the same
// prefixed tokens are parsed out of the figure's data-tags attribute;
// the a_ vs u_ distinction is lost there because FA collapses both into
// u_ in that flat list.
CategorizedTags CategorizedTags
FileURL string // absolute CDN URL; pass to Download FileURL string // absolute CDN URL; pass to Download
ThumbURL string ThumbURL string
Width int // 0 if unknown / non-image Width int // 0 if unknown / non-image

View File

@@ -156,6 +156,32 @@ func parseSubmission(id SubmissionID, doc *goquery.Document) (*Submission, error
} }
}) })
// Prefixed system tags FA renders these as tag-block anchors with a
// data-tag-name attribute carrying a leading single-letter prefix:
// s_ species, c_ character, a_/u_ artist, t_ type.
// They are paired with a sibling <span class="tag-invalid"> and have no
// /search/ href, so they are skipped by the keyword pass above.
doc.Find("div.submission-tags a.tag-block[data-tag-name]").Each(func(_ int, a *goquery.Selection) {
raw := strings.TrimSpace(trimAttr(a, "data-tag-name"))
if len(raw) < 3 || raw[1] != '_' {
return
}
name := raw[2:]
if name == "" {
return
}
switch raw[0] {
case 's':
s.CategorizedTags.Species = append(s.CategorizedTags.Species, name)
case 'c':
s.CategorizedTags.Characters = append(s.CategorizedTags.Characters, name)
case 'a', 'u':
s.CategorizedTags.Artists = append(s.CategorizedTags.Artists, name)
case 't':
s.CategorizedTags.Types = append(s.CategorizedTags.Types, name)
}
})
// File URL FA renders a "Download" button in #submission-options that // File URL FA renders a "Download" button in #submission-options that
// links to the canonical file for *every* submission type. For visual // links to the canonical file for *every* submission type. For visual
// art it equals the #submissionImg source; for stories and music it's // art it equals the #submissionImg source; for stories and music it's

View File

@@ -54,6 +54,10 @@ const syntheticSubmissionHTML = `<html><body>
<div> <div>
<span class="tags"><span><a href="javascript:void(0);" class="tag-block"></a><a href="/search/@keywords wolf">wolf</a></span></span> <span class="tags"><span><a href="javascript:void(0);" class="tag-block"></a><a href="/search/@keywords wolf">wolf</a></span></span>
<span class="tags"><span><a href="javascript:void(0);" class="tag-block"></a><a href="/search/@keywords art">art</a></span></span> <span class="tags"><span><a href="javascript:void(0);" class="tag-block"></a><a href="/search/@keywords art">art</a></span></span>
<span class="tags"><span><a href="javascript:void(0);" data-tag-name="s_wolf" class="tag-block"></a><span class="tag-invalid">s_wolf</span></span></span>
<span class="tags"><span><a href="javascript:void(0);" data-tag-name="c_artwork_digital" class="tag-block"></a><span class="tag-invalid">c_artwork_digital</span></span></span>
<span class="tags"><span><a href="javascript:void(0);" data-tag-name="t_general_furry_art" class="tag-block"></a><span class="tag-invalid">t_general_furry_art</span></span></span>
<span class="tags"><span><a href="javascript:void(0);" data-tag-name="u_somefurry" class="tag-block"></a><span class="tag-invalid">u_somefurry</span></span></span>
</div> </div>
</div> </div>
@@ -110,6 +114,34 @@ func TestParseSubmission_Synthetic(t *testing.T) {
if !strings.Contains(sub.Description, "world") { if !strings.Contains(sub.Description, "world") {
t.Errorf("Description missing expected content: %q", sub.Description) t.Errorf("Description missing expected content: %q", sub.Description)
} }
catChecks := []struct {
name string
got []string
want []string
}{
{"Species", sub.CategorizedTags.Species, []string{"wolf"}},
{"Characters", sub.CategorizedTags.Characters, []string{"artwork_digital"}},
{"Types", sub.CategorizedTags.Types, []string{"general_furry_art"}},
{"Artists", sub.CategorizedTags.Artists, []string{"somefurry"}},
}
for _, c := range catChecks {
if !equalStrings(c.got, c.want) {
t.Errorf("CategorizedTags.%s = %v; want %v", c.name, c.got, c.want)
}
}
}
func equalStrings(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
} }
// TestParseSubmission_FavoritedState verifies parseSubmission reports the // TestParseSubmission_FavoritedState verifies parseSubmission reports the

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -67,7 +67,12 @@
<!-- EU request: yes --> <!-- EU request: yes -->
<body class="c-bodyColor" <body class="c-bodyColor"
id="pageid-redirect" data-static-path="/themes/beta" id="pageid-messagecenter-pms-view" data-static-path="/themes/beta"
data-user-blocklist=""
data-user-logged-in="1"
data-tag-blocklist="music"
data-tag-blocklist-hide-tagless="0"
data-tag-blocklist-nonce="a0484bd071eb18ddc52c45696f83e98306600844e096df29c41b770b7ef8da61"
> >
<script type="text/javascript"> <script type="text/javascript">
@@ -104,6 +109,11 @@
<div class="mobile-nav-content-container"> <div class="mobile-nav-content-container">
<div class="aligncenter"> <div class="aligncenter">
<a href="/user/soxx-thefennec/"><img class="loggedin_user_avatar avatar" alt="SoXX-TheFennec" src="//a.furaffinity.net/1515442832/soxx-thefennec.gif"/></a>
<h2 style="margin-bottom:0"><a href="/user/soxx-thefennec/">SoXX-TheFennec</a></h2>
<a href="/user/soxx-thefennec/">Userpage</a> |
<a href="/msg/pms/">Notes</a> |
<a href="/controls/journal/">Journals</a> |
<a href="/plus/"><img class="menu-mini-icon" src="/themes/beta/img/the-golden-pawb.png"> FA+</a> | <a href="/plus/"><img class="menu-mini-icon" src="/themes/beta/img/the-golden-pawb.png"> FA+</a> |
<a href="https://shop.furaffinity.net" target="_blank"><img class="menu-mini-icon" src="/themes/beta/img/icons/merch_store_icon.png"> Shop</a> <a href="https://shop.furaffinity.net" target="_blank"><img class="menu-mini-icon" src="/themes/beta/img/icons/merch_store_icon.png"> Shop</a>
<br /> <br />
@@ -111,6 +121,7 @@
<hr> <hr>
<h2><a href="/browse/">Browse</a></h2> <h2><a href="/browse/">Browse</a></h2>
<h2><a href="/search/">Search</a></h2> <h2><a href="/search/">Search</a></h2>
<h2><a href="/submit/">Upload</a></h2>
<div class="nav-ac-container"> <div class="nav-ac-container">
<label for="mobile-menu-submenu-0"><h2 style="margin-top:0;padding-top:0">Support &#x25BC;</h2></label> <label for="mobile-menu-submenu-0"><h2 style="margin-top:0;padding-top:0">Support &#x25BC;</h2></label>
@@ -142,22 +153,65 @@
<h3>SUPPORT</h3> <h3>SUPPORT</h3>
<a href="/help/#contact">Contact Us</a><br /> <a href="/help/#contact">Contact Us</a><br />
<a href="/controls/troubletickets/">REPORT A PROBLEM</a><br />
<a href="https://status.furaffinity.net/">Site Status</a> <a href="https://status.furaffinity.net/">Site Status</a>
</article> </article>
</div> </div>
<div class="mobile-sfw-toggle">
<h2>SFW Mode</h2>
<div class="sfw-toggle type-slider slider-button-wrapper">
<input type="checkbox" id="sfw-toggle-mobile" class="slider-toggle" />
<label class="slider-viewport" for="sfw-toggle-mobile" title="Quick toggle to show or hide Mature and Adult submissions">
<div class="slider">
<div class="slider-button">&nbsp;</div>
<div class="slider-content left"><span>SFW</span></div>
<div class="slider-content right"><span>NSFW</span></div>
</div>
</label>
</div>
</div>
<div class="nav-ac-container">
<label for="mobile-menu-submenu-1"><h2 style="margin-top:0;padding-top:0">Settings &#x25BC;</h2></label>
<input id="mobile-menu-submenu-1" name="accordion-1" type="checkbox" />
<article class="nav-ac-content nav-ac-content-dropdown">
<h3>ACCOUNT INFORMATION</h3>
<a href="/controls/settings/">Account Settings</a><br>
<a href="/controls/site-settings/">Global Site Settings</a><br>
<a href="/controls/user-settings/">User Settings</a>
<h3>CUSTOMIZE USER PROFILE</h3>
<a href="/controls/profile/">Profile Info</a><br>
<a href="/controls/profilebanner/">Profile Banner</a><br>
<a href="/controls/contacts/">Contacts and Social Media</a><br>
<a href="/controls/avatar/">Avatar Management</a>
<h3>MANAGE MY CONTENT</h3>
<a href="/controls/submissions/">Submissions</a><br>
<a href="/controls/folders/submissions/">Folders</a><br>
<a href="/controls/journal/">Journals</a><br>
<a href="/controls/favorites/">Favorites</a><br>
<a href="/controls/buddylist/">Watches</a><br>
<a href="/controls/shouts/">Shouts</a><br>
<a href="/controls/badges/">Badges</a><br>
<a href="/controls/user-icons/">User Icons</a>
<h3>SECURITY</h3>
<a href="/controls/sessions/logins/">Active Sessions</a><br>
<a href="/controls/sessions/logs/">Activity Log</a><br>
<a href="/controls/sessions/labels/">Browser Labels</a>
</article>
</div>
<hr>
<hr> <hr>
<h2><div class="inline hideonmobile hideontablet"> <h2><form class="post-btn logout-link" method="post" action="/logout/"><button type="submit">Log Out</button><input type="hidden" name="key" value="0b178006ed9c0137089032690e59b1799185aef779fcc884630f2cc5a100bf57"/></form>
<a href="/login"><strong>Log In</strong></a> or <a href="/register"><strong>Create an Account</strong></a> <script type="text/javascript">
</div> _fajs.push(['init_logout_button', '.logout-link button']);
</script>
<div class="inline hideondesktop">
<a href="/login">Log In</a><br>
<a href="/register">Create an Account</a>
</div>
</h2> </h2>
@@ -170,6 +224,11 @@
<div class="mobile-notification-bar">
<a class="notification-container inline" href="/msg/submissions/" title="5,161 Submission Notifications">5161S</a>
<a class="notification-container inline" href="/msg/others/#journals" title="75 Journal Notifications">75J</a>
</div>
@@ -229,6 +288,7 @@
<h3>Support</h3> <h3>Support</h3>
<a href="/help/#contact">Contact Us</a> <a href="/help/#contact">Contact Us</a>
<a href="/controls/troubletickets/">Report a Problem</a>
<a href="https://status.furaffinity.net/">Site Status</a> <a href="https://status.furaffinity.net/">Site Status</a>
</div> </div>
</div> </div>
@@ -254,18 +314,98 @@
<li class="no-sub"> <li class="message-bar-desktop">
<span class="top-heading"><div class="inline hideonmobile hideontablet"> <a class="notification-container inline" href="/msg/submissions/" title="5,161 Submission Notifications">5161S</a>
<a href="/login"><strong>Log In</strong></a> or <a href="/register"><strong>Create an Account</strong></a> <a class="notification-container inline" href="/msg/others/#journals" title="75 Journal Notifications">75J</a>
</div>
<div class="inline hideondesktop">
<a href="/login">Log In</a><br>
<a href="/register">Create an Account</a>
</div>
</span>
</li> </li>
</ul>
<li>
<div class="floatleft hideonmobile">
<a href="/user/soxx-thefennec"><img class="loggedin_user_avatar menubar-icon-resize avatar" style="cursor:pointer" alt="SoXX-TheFennec" src="//a.furaffinity.net/1515442832/soxx-thefennec.gif"/></a>
</div>
</li>
<li class="submenu-trigger">
<div class="floatleft hideonmobile">
<svg class="avatar-submenu-trigger banner-svg" xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24"><path d="M4 6h16v2H4zm0 5h16v2H4zm0 5h16v2H4z"></path></svg>
</div>
<a id="my-username" class="top-heading hideondesktop" href="#"><span class="hideondesktop">My FA ( </span>SoXX-TheFennec<span class="hideondesktop"> )</span></a>
<div class="dropdown dropdown-right">
<div class="dd-inner">
<div class="column">
<h3>Account</h3>
<a href="/user/soxx-thefennec/">My Userpage</a>
<a href="/msg/pms/">Check My Notes</a>
<a href="/controls/journal/">Create a Journal</a>
<a href="/commissions/soxx-thefennec/">My Commission Info</a>
<h3>Support Fur Affinity</h3>
<a href="/plus/">Subscribe to FA+ </a>
<a href="https://shop.furaffinity.net/" target="_blank">Merch Store</a>
<h3>Trouble Tickets</h3>
<a href="/controls/troubletickets/">Report a Problem</a>
<div class="mobile-sfw-toggle">
<h3 class="padding-top:10px">Toggle SFW</h3>
<div class="sfw-toggle type-slider slider-button-wrapper" style="position:relative;top:5px">
<input type="checkbox" id="sfw-toggle-mobile" class="slider-toggle" />
<label class="slider-viewport" for="sfw-toggle-mobile" title="Quick toggle to show or hide Mature and Adult submissions">
<div class="slider">
<div class="slider-button">&nbsp;</div>
<div class="slider-content left"><span>SFW</span></div>
<div class="slider-content right"><span>NSFW</span></div>
</div>
</label>
</div>
</div>
<hr>
<form class="post-btn logout-link" method="post" action="/logout/"><button type="submit">Log Out</button><input type="hidden" name="key" value="0b178006ed9c0137089032690e59b1799185aef779fcc884630f2cc5a100bf57"/></form>
<script type="text/javascript">
_fajs.push(['init_logout_button', '.logout-link button']);
</script>
</div>
</div>
</div>
</li>
<li class="submenu-trigger">
<a class="top-heading" href="#"><svg class="banner-svg" xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24" style="transform: ;msFilter:;"><path d="M12 16c2.206 0 4-1.794 4-4s-1.794-4-4-4-4 1.794-4 4 1.794 4 4 4zm0-6c1.084 0 2 .916 2 2s-.916 2-2 2-2-.916-2-2 .916-2 2-2z"></path><path d="m2.845 16.136 1 1.73c.531.917 1.809 1.261 2.73.73l.529-.306A8.1 8.1 0 0 0 9 19.402V20c0 1.103.897 2 2 2h2c1.103 0 2-.897 2-2v-.598a8.132 8.132 0 0 0 1.896-1.111l.529.306c.923.53 2.198.188 2.731-.731l.999-1.729a2.001 2.001 0 0 0-.731-2.732l-.505-.292a7.718 7.718 0 0 0 0-2.224l.505-.292a2.002 2.002 0 0 0 .731-2.732l-.999-1.729c-.531-.92-1.808-1.265-2.731-.732l-.529.306A8.1 8.1 0 0 0 15 4.598V4c0-1.103-.897-2-2-2h-2c-1.103 0-2 .897-2 2v.598a8.132 8.132 0 0 0-1.896 1.111l-.529-.306c-.924-.531-2.2-.187-2.731.732l-.999 1.729a2.001 2.001 0 0 0 .731 2.732l.505.292a7.683 7.683 0 0 0 0 2.223l-.505.292a2.003 2.003 0 0 0-.731 2.733zm3.326-2.758A5.703 5.703 0 0 1 6 12c0-.462.058-.926.17-1.378a.999.999 0 0 0-.47-1.108l-1.123-.65.998-1.729 1.145.662a.997.997 0 0 0 1.188-.142 6.071 6.071 0 0 1 2.384-1.399A1 1 0 0 0 11 5.3V4h2v1.3a1 1 0 0 0 .708.956 6.083 6.083 0 0 1 2.384 1.399.999.999 0 0 0 1.188.142l1.144-.661 1 1.729-1.124.649a1 1 0 0 0-.47 1.108c.112.452.17.916.17 1.378 0 .461-.058.925-.171 1.378a1 1 0 0 0 .471 1.108l1.123.649-.998 1.729-1.145-.661a.996.996 0 0 0-1.188.142 6.071 6.071 0 0 1-2.384 1.399A1 1 0 0 0 13 18.7l.002 1.3H11v-1.3a1 1 0 0 0-.708-.956 6.083 6.083 0 0 1-2.384-1.399.992.992 0 0 0-1.188-.141l-1.144.662-1-1.729 1.124-.651a1 1 0 0 0 .471-1.108z"></path></svg></a>
<div class="dropdown dropdown-right">
<div class="dd-inner">
<div class="column">
<h3>Account Information</h3>
<a href="/controls/settings/">Account Settings</a>
<a href="/controls/site-settings/">Global Site Settings</a>
<a href="/controls/user-settings/">User Settings</a>
<h3>Customize User Profile</h3>
<a href="/controls/profile/">Profile Info</a>
<a href="/controls/profilebanner/">Profile Banner</a>
<a href="/controls/contacts/">Contacts & Social Media</a>
<a href="/controls/avatar/">Avatar Management</a>
<h3>Manage My Content</h3>
<a href="/controls/submissions/">Submissions</a>
<a href="/controls/folders/submissions/">Folders</a>
<a href="/controls/journal/">Journals</a>
<a href="/controls/favorites/">Favorites</a>
<a href="/controls/buddylist/">Watches</a>
<a href="/controls/shouts/">Shouts</a>
<a href="/controls/badges/">Badges</a>
<a href="/controls/user-icons/">User Icons</a>
<h3>Security</h3>
<a href="/controls/sessions/logins/">Active Sessions</a>
<a href="/controls/sessions/logs/">Activity Log</a>
<a href="/controls/sessions/labels/">Browser Labels</a>
</div>
</div>
</div>
</li>
</ul>
<script type="text/javascript"> <script type="text/javascript">
_fajs.push(['init_sfw_button', '.sfw-toggle']); _fajs.push(['init_sfw_button', '.sfw-toggle']);
</script> </script>
@@ -291,25 +431,26 @@
</script> </script>
<div class="news-block"> <div class="news-block">
</div>
<div id="news" class="newsBlock" data-date="1779756930">
<strong>News:</strong><span class="hideondesktop hideontablet"><br></span> <a class="journal-news-link" href="/journal/11365688">VGen Challenge 6 Day Reminder + Swag Pickup (<span class="c-contentRating--general" alt="General rating" title="General rating">G</span>)</a>
<span class="jsClose newsBlock__closeBtn" title="Close"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" style="transform: ;msFilter:;" title="Dismiss" ><path d="M20 3H4c-1.103 0-2 .897-2 2v14c0 1.103.897 2 2 2h16c1.103 0 2-.897 2-2V5c0-1.103-.897-2-2-2zM4 19V7h16l.001 12H4z"></path><path d="m15.707 10.707-1.414-1.414L12 11.586 9.707 9.293l-1.414 1.414L10.586 13l-2.293 2.293 1.414 1.414L12 14.414l2.293 2.293 1.414-1.414L13.414 13z"></path></svg></span>
</div>
<script type="text/javascript">
_fajs.push(['init_news_block', 'news']);
</script>
</div>
<div id="main-window" class="footer-mobile-tweak g-wrapper"> <div id="main-window" class="footer-mobile-tweak g-wrapper">
<div id="header"> <div id="header">
<!-- site banner --> <!-- site banner -->
<site-banner > <site-banner >
<map name="banner-map">
<area
shape="rect"
coords="441,144,1042,197"
href="https://link.vgen.co/furaffinity"
onclick="javascript: return(confirm(`You've clicked on a usemap link that will redirect you to\n\nhttps://link.vgen.co/furaffinity\n\nAre you fine with being redirected?`))"
target="_blank" />
</map>
<a href="/"> <a href="/">
<picture> <picture>
<source srcset="//d.furaffinity.net/media/banners/modern/fa-banner-spring-vgen-20260501.webp" type="image/webp"> <source srcset="//d.furaffinity.net/media/banners/modern/fa-banner-spring-furality-20260531.webp" type="image/webp">
<img usemap="#banner-map" src="//d.furaffinity.net/media/banners/modern/fa-banner-spring-vgen-20260501.jpg"> <img usemap="#banner-map" src="//d.furaffinity.net/media/banners/modern/fa-banner-spring-furality-20260531.jpg">
</picture> </picture>
</a> </a>
</site-banner> </site-banner>
@@ -320,23 +461,174 @@
<div id="site-content"> <div id="site-content">
<!-- /header --> <!-- /header -->
<!-- {redirect} -->
<div id="standardpage">
<section class="aligncenter notice-message user-submitted-links"> <div id="message">
<div class="section-body alignleft"> <section>
<h2>System Message</h2> <div class="section-header">
<div class="message-center-note-information">
<div class="message-center-note-information avatar">
<a href="/user/vampexx/"><img class="avatar" src="//a.furaffinity.net/1741507375/vampexx.gif"/></a>
</div>
<div class="message-center-note-information addresses">
<h2>RE: TF YCH Bidding</h2>
Sent by
<div class="c-usernameBlock">
<div class="redirect-message">Please log in!</div> <a class="c-usernameBlock__displayName js-displayName-block" href="/user/vampexx/">
<span class="js-displayName">Vampexx</span>
</a>
<div class="proceed-btn-container"> <a class="c-usernameBlock__userName js-userName-block" href="/user/vampexx/">
<a class="button standard go" href="/login/">Continue &raquo;</a> <span><span class="c-usernameBlock__symbol" title="Member" alt="Member">~</span>vampexx</span>
</a>
</div>
&nbsp; <span class="popup_date" data-title-date="1" data-24-hour="0" data-time="1654111463" title="June 1, 2022 08:24:23 PM">4 years ago</span>
<br>
To
<div class="c-usernameBlock">
<a class="c-usernameBlock__displayName js-displayName-block" href="/user/soxx-thefennec/">
<span class="js-displayName">SoXX-TheFennec</span>
</a>
<a class="c-usernameBlock__userName js-userName-block" href="/user/soxx-thefennec/">
<span><span class="c-usernameBlock__symbol" title="Member" alt="Member">~</span>soxx-thefennec</span>
</a>
</div> </div>
</div>
</div>
<div class="section-body">
<div class="user-submitted-links">
<div class="noteWarningMessage noteWarningMessage--scam user-submitted-links">
<div class="noteWarningMessage__icon">
<img src="/themes/beta/img/icons/Error_l.png">
</div>
<div>
<h4>Do you know this person?</h4>
Verify the username and profile before doing business with them! Scammers often attempt to impersonate well-known artists.
<br />
If you encounter something suspicious, please report it using a <a href="/controls/troubletickets/">Trouble Ticket</a>.
<br />
Also, review our <a href="/fight_spam" target="_blank">Internet Safety and Scamming</a> page to keep yourself informed and safe while using the web!
</div>
</div>
actually, ill put you as 65 but say its Anon, then if they respond you can respond back<br />
—————————<br />
original post by <a href="/user/soxx-thefennec" class="linkusername"><span class="c-usernameBlockSimple username-underlined"><span class="c-usernameBlockSimple__displayName" title="soxx-thefennec">SoXX-TheFennec</span></span></a>:<br />
<br />
oh wow how tf did I miss read that yeah then 65 XD<br />
<br />
—————————<br />
original post by <a href="/user/vampexx" class="linkusername"><span class="c-usernameBlockSimple username-underlined"><span class="c-usernameBlockSimple__displayName" title="vampexx">Vampexx</span></span></a>:<br />
<br />
Hey, wanted to let you know that you biddedon the min bid, not the starting bid, SB is 60 X3 </div>
<div class="section-options">
<div class="inline note-view toggle">
<form id="note-actions" action="/msg/pms/" method="post">
<input type="hidden" name="manage_notes" value="1" />
<input type="hidden" name="items[]" value="131012623" />
<button class="button standard priority_high" type="submit" name="set_prio" value="high">High</button>
<button class="button standard priority_medium" type="submit" name="set_prio" value="medium">Medium</button>
<button class="button standard priority_low" type="submit" name="set_prio" value="low">Low</button>
<button class="button standard priority_none" type="submit" name="set_prio" value="none">None</button>
<button class="button standard move_archive" type="submit" name="move_to" value="archive">Archive</button>
</form>
</div>
</div> </div>
</div> </div>
</section> </section>
</div> </div>
<form method="post" name="note-form" id="note-form" action="/msg/send/">
<input type="hidden" name="key" value="b52ba5d0d5f0071bc47df3bb0645c5926c15e245a17c76f6b108a3915fe378c9" />
<section>
<div class="section-body">
<h2>Reply</h2>
<div>
<span class="noteresponse inline">Recipient:</span>
<input type="text" name="to" value="vampexx" maxLength="150" class="textbox"/>
</div>
<div class="p5t">
<span class="noteresponse inline">Subject:</span>
<input type="text" name="subject" value="RE: RE: TF YCH Bidding" maxLength="150" class="textbox "/>
</div>
<div class="p5t">
<span class="noteresponsespacer" ><i class="bbcodeformat b hand" title="Bold (CTRL+B)" onclick="performInsert(this, '[b]', '[/b]');"></i>
<i class="bbcodeformat i hand" title="Italic (CTRL+I)" onclick="performInsert(this, '[i]', '[/i]');"></i>
<i class="bbcodeformat u hand" title="Underlined (CTRL+U)" onclick="performInsert(this, '[u]', '[/u]');"></i>
&nbsp;&nbsp;&nbsp;
<i class="bbcodeformat align_left hand" title="Align Left" onclick="performInsert(this, '[left]', '[/left]');"></i>
<i class="bbcodeformat align_center hand" title="Align Center" onclick="performInsert(this, '[center]', '[/center]');"></i>
<i class="bbcodeformat align_right hand" title="Align Right" onclick="performInsert(this, '[right]', '[/right]');"></i>
</span>
</div>
<div style="display:table; width:100%; margin-bottom:8px;">
<div class="user-submitted-links" style="display:table-cell;width:auto">
<small style="display: block; margin-bottom: 6px;">Please write your reply at the top of the reply box.</small>
<textarea id="JSMessage_view" name="message" rows="17" class="textarea">
&mdash;&mdash;&mdash;&mdash;&mdash;&mdash;&mdash;&mdash;&mdash;
original post by Vampexx (@vampexx):
actually, ill put you as 65 but say its Anon, then if they respond you can respond back
&mdash;&mdash;&mdash;&mdash;&mdash;&mdash;&mdash;&mdash;&mdash;
original post by @SoXX-TheFennec:
oh wow how tf did I miss read that yeah then 65 XD
</textarea>
<p style="text-align: left; font-size: 0.8em; margin-top: 2px;">Help with <a href="/help/#tags-and-codes">Tags & Codes</a> formatting.</p>
</div>
</div>
<div class="section-options">
<input class="button standard" type="submit" value="Reply"/>
</div>
</div>
</section>
</form>
<script type="text/javascript">
_fajs.push(['init_bbcode_hotkeys', 'JSMessage_view']);
_fajs.push(function(){
// disable submit button on form submit
$('note-form').observe('submit', function(evt) {
// disable the button to prevent multiple clicks and thus multiple requests
var btn = $('note-form').down('input[type="submit"]');
btn.value = 'Sending...';
btn.disabled = true;
btn.addClassName('disabled');
window.setTimeout(function(){
btn.value = 'Reply';
btn.disabled = false;
btn.removeClassName('disabled');
}, 3000);
});
// prevent possible cached button state on page reload
var btn = $('note-form').down('input[type="submit"]');
btn.value = 'Reply';
btn.disabled = false;
});
</script>
</div> </div>
<!-- /<div id="site-content"> --> <!-- /<div id="site-content"> -->
@@ -371,11 +663,11 @@
</div> </div>
<div class="online-stats"> <div class="online-stats">
88574 <strong><span title="Measured in the last 900 seconds">Users online</span></strong> &mdash; 91668 <strong><span title="Measured in the last 900 seconds">Users online</span></strong> &mdash;
3334 <strong>guests</strong>, 4823 <strong>guests</strong>,
8900 <strong>registered</strong> 14001 <strong>registered</strong>
and 76340 <strong>other</strong> and 72844 <strong>other</strong>
<!-- Online Counter Last Update: Sun, 24 May 2026 04:31:01 -0700 --> <!-- Online Counter Last Update: Tue, 02 Jun 2026 12:27:00 -0700 -->
</div> </div>
<small>Limit bot activity to periods with less than 10k registered users online.</small> <small>Limit bot activity to periods with less than 10k registered users online.</small>
@@ -383,8 +675,8 @@
<strong>&copy; 2005-2026 Frost Dragon Art LLC</strong> <strong>&copy; 2005-2026 Frost Dragon Art LLC</strong>
<div class="footnote"> <div class="footnote">
Server Time: May 24, 2026 04:31 AM<br /> Server Time: Jun 2, 2026 12:27 PM<br />
Page generated in 0.009 seconds<br />[ 26.5% PHP, 73.5% SQL ] (9 queries)<br /> Page generated in 0.018 seconds<br />[ 42.4% PHP, 57.6% SQL ] (24 queries)<br />
</div> </div>
</div> </div>
</div> </div>
@@ -415,7 +707,7 @@
<script src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js" crossorigin="anonymous"></script> <script src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js" crossorigin="anonymous"></script>
<script type="text/javascript"> <script type="text/javascript">
var server_timestamp = 1779622312; var server_timestamp = 1780428442;
var client_timestamp = Date.now() / 1000; var client_timestamp = Date.now() / 1000;
var server_timestamp_delta = server_timestamp - client_timestamp; var server_timestamp_delta = server_timestamp - client_timestamp;
var sfw_cookie_name = 'sfw'; var sfw_cookie_name = 'sfw';
@@ -424,7 +716,7 @@
// //
document.addEventListener("DOMContentLoaded", (event) => { document.addEventListener("DOMContentLoaded", (event) => {
// //
const ad_manager = new adManager({"sizeConfig":[{"labels":["desktopWide"],"mediaQuery":"(min-width: 1090px)","sizesSupported":[[728,90],[300,250],[300,168],[300,600],[160,600]]},{"labels":["desktopNarrow"],"mediaQuery":"(min-width: 740px) and (max-width: 1089px)","sizesSupported":[[728,90],[300,250],[300,168]]},{"labels":["mobile"],"mediaQuery":"(min-width: 0px) and (max-width: 739px)","sizesSupported":[[320,50],[300,50],[320,100]]}],"slotConfig":{"header_middle":{"containerSize":{"desktopWide":[728,90],"desktopNarrow":[728,90],"mobile":[320,50]},"providerPriority":["inhouse"]},"above_content":{"containerSize":{"desktopWide":[728,90],"desktopNarrow":[728,90],"mobile":[320,50]},"providerPriority":["inhouse"]},"sidebar":{"containerSize":{"desktopWide":[300,250]},"providerPriority":["inhouse"]},"sidebar_tall":{"containerSize":{"desktopWide":[300,600],"desktopNarrow":[300,600],"mobile":[300,250]},"providerPriority":["inhouse"]},"footer_left":{"containerSize":{"desktopWide":[300,250],"desktopNarrow":[300,250],"mobile":[300,250]},"providerPriority":["inhouse"]},"footer_right":{"containerSize":{"desktopWide":[300,250],"desktopNarrow":[300,250],"mobile":[300,250]},"providerPriority":["inhouse"]},"footer_right_top":{"containerSize":{"desktopWide":[320,50],"desktopNarrow":[320,50],"mobile":[320,50]},"providerPriority":["inhouse"]},"footer_right_bottom":{"containerSize":{"desktopWide":[320,50],"desktopNarrow":[320,50],"mobile":[320,50]},"providerPriority":["inhouse"]},"header_right_left":{"containerSize":{"desktopWide":[320,50]},"providerPriority":["inhouse"]},"header_right_right":{"containerSize":{"desktopWide":[320,50]},"providerPriority":["inhouse"]},"sidebar_top":{"containerSize":{"desktopWide":[320,50]},"providerPriority":["inhouse"]},"sidebar_bottom":{"containerSize":{"desktopWide":[320,50]},"providerPriority":["inhouse"]},"front_page":{"containerSize":{"desktopWide":[728,90],"desktopNarrow":[728,90],"mobile":[320,50]},"providerPriority":["inhouse"]},"c-videoAd":{"containerSize":{"desktopWide":[300,250]},"providerPriority":["inhouse"]}},"providerConfig":{"inhouse":{"domain":"https:\/\/rv.furaffinity.net","dataPath":"\/live\/www\/delivery\/spc.php","dataVariableName":"OA_output"}},"adConfig":{"inhouse":{"header_middle":{"default":{"tagId":40,"tagSize":[728,90]},"sizeOverride":{"mobile":{"tagId":56,"tagSize":[320,50]}}},"above_content":{"default":{"tagId":25,"tagSize":[728,90]},"sizeOverride":{"mobile":{"tagId":53,"tagSize":[320,50]}}},"sidebar":{"default":{"tagId":49,"tagSize":[300,250]}},"sidebar_tall":{"default":{"tagId":49,"tagSize":[300,250]}},"footer_left":{"default":{"tagId":28,"tagSize":[300,250]}},"footer_right":{"default":{"tagId":74,"tagSize":[300,250]}},"footer_right_top":{"default":{"tagId":62,"tagSize":[320,50]}},"footer_right_bottom":{"default":{"tagId":59,"tagSize":[320,50]}},"header_right_left":{"default":{"tagId":65,"tagSize":[320,50]}},"header_right_right":{"default":{"tagId":68,"tagSize":[320,50]}},"sidebar_top":{"default":{"tagId":65,"tagSize":[320,50]}},"sidebar_bottom":{"default":{"tagId":68,"tagSize":[320,50]}},"front_page":{"default":{"tagId":77,"tagSize":[728,90]},"sizeOverride":{"mobile":{"tagId":78,"tagSize":[320,50]}}},"c-videoAd":{"default":{"tagId":71,"tagSize":[320,50]}}}},"extraMetadata":{"adsenseClient":"ca-pub-3495616356562362","forceLoadConfigs":["c-videoAd"]}}, true); const ad_manager = new adManager({"sizeConfig":[{"labels":["desktopWide"],"mediaQuery":"(min-width: 1090px)","sizesSupported":[[728,90],[300,250],[300,168],[300,600],[160,600]]},{"labels":["desktopNarrow"],"mediaQuery":"(min-width: 740px) and (max-width: 1089px)","sizesSupported":[[728,90],[300,250],[300,168]]},{"labels":["mobile"],"mediaQuery":"(min-width: 0px) and (max-width: 739px)","sizesSupported":[[320,50],[300,50],[320,100]]}],"slotConfig":{"header_middle":{"containerSize":{"desktopWide":[728,90],"desktopNarrow":[728,90],"mobile":[320,50]},"providerPriority":["inhouse"]},"above_content":{"containerSize":{"desktopWide":[728,90],"desktopNarrow":[728,90],"mobile":[320,50]},"providerPriority":["inhouse"]},"sidebar":{"containerSize":{"desktopWide":[300,250]},"providerPriority":["inhouse"]},"sidebar_tall":{"containerSize":{"desktopWide":[300,600],"desktopNarrow":[300,600],"mobile":[300,250]},"providerPriority":["inhouse"]},"footer_left":{"containerSize":{"desktopWide":[300,250],"desktopNarrow":[300,250],"mobile":[300,250]},"providerPriority":["inhouse"]},"footer_right":{"containerSize":{"desktopWide":[300,250],"desktopNarrow":[300,250],"mobile":[300,250]},"providerPriority":["inhouse"]},"footer_right_top":{"containerSize":{"desktopWide":[320,50],"desktopNarrow":[320,50],"mobile":[320,50]},"providerPriority":["inhouse"]},"footer_right_bottom":{"containerSize":{"desktopWide":[320,50],"desktopNarrow":[320,50],"mobile":[320,50]},"providerPriority":["inhouse"]},"header_right_left":{"containerSize":{"desktopWide":[320,50]},"providerPriority":["inhouse"]},"header_right_right":{"containerSize":{"desktopWide":[320,50]},"providerPriority":["inhouse"]},"sidebar_top":{"containerSize":{"desktopWide":[320,50]},"providerPriority":["inhouse"]},"sidebar_bottom":{"containerSize":{"desktopWide":[320,50]},"providerPriority":["inhouse"]},"front_page":{"containerSize":{"desktopWide":[728,90],"desktopNarrow":[728,90],"mobile":[320,50]},"providerPriority":["inhouse"]},"c-videoAd":{"containerSize":{"desktopWide":[300,250]},"providerPriority":["inhouse"]}},"providerConfig":{"inhouse":{"domain":"https:\/\/rv.furaffinity.net","dataPath":"\/live\/www\/delivery\/spc.php","dataVariableName":"OA_output"}},"adConfig":{"inhouse":{"header_middle":{"default":{"tagId":42,"tagSize":[728,90]},"sizeOverride":{"mobile":{"tagId":58,"tagSize":[320,50]}}},"above_content":{"default":{"tagId":27,"tagSize":[728,90]},"sizeOverride":{"mobile":{"tagId":55,"tagSize":[320,50]}}},"sidebar":{"default":{"tagId":51,"tagSize":[300,250]}},"sidebar_tall":{"default":{"tagId":51,"tagSize":[300,250]}},"footer_left":{"default":{"tagId":30,"tagSize":[300,250]}},"footer_right":{"default":{"tagId":72,"tagSize":[300,250]}},"footer_right_top":{"default":{"tagId":64,"tagSize":[320,50]}},"footer_right_bottom":{"default":{"tagId":61,"tagSize":[320,50]}},"header_right_left":{"default":{"tagId":67,"tagSize":[320,50]}},"header_right_right":{"default":{"tagId":70,"tagSize":[320,50]}},"sidebar_top":{"default":{"tagId":67,"tagSize":[320,50]}},"sidebar_bottom":{"default":{"tagId":70,"tagSize":[320,50]}},"front_page":{"default":{"tagId":75,"tagSize":[728,90]},"sizeOverride":{"mobile":{"tagId":80,"tagSize":[320,50]}}},"c-videoAd":{"default":{"tagId":71,"tagSize":[320,50]}}}},"extraMetadata":{"adsenseClient":"ca-pub-3495616356562362","forceLoadConfigs":["c-videoAd"]}}, true);
}); });
</script> </script>

View File

@@ -84,6 +84,11 @@
<!-- EU request: yes --> <!-- EU request: yes -->
<body class="c-bodyColor" <body class="c-bodyColor"
id="pageid-gallery" data-static-path="/themes/beta" id="pageid-gallery" data-static-path="/themes/beta"
data-user-blocklist=""
data-user-logged-in="1"
data-tag-blocklist="music"
data-tag-blocklist-hide-tagless="0"
data-tag-blocklist-nonce="73dd0bd0afb4415ee108c6230a8ff451a7d4865518c65a019550211e4f30a92c"
> >
<script type="text/javascript"> <script type="text/javascript">
@@ -120,6 +125,11 @@
<div class="mobile-nav-content-container"> <div class="mobile-nav-content-container">
<div class="aligncenter"> <div class="aligncenter">
<a href="/user/soxx-thefennec/"><img class="loggedin_user_avatar avatar" alt="SoXX-TheFennec" src="//a.furaffinity.net/1515442832/soxx-thefennec.gif"/></a>
<h2 style="margin-bottom:0"><a href="/user/soxx-thefennec/">SoXX-TheFennec</a></h2>
<a href="/user/soxx-thefennec/">Userpage</a> |
<a href="/msg/pms/">Notes</a> |
<a href="/controls/journal/">Journals</a> |
<a href="/plus/"><img class="menu-mini-icon" src="/themes/beta/img/the-golden-pawb.png"> FA+</a> | <a href="/plus/"><img class="menu-mini-icon" src="/themes/beta/img/the-golden-pawb.png"> FA+</a> |
<a href="https://shop.furaffinity.net" target="_blank"><img class="menu-mini-icon" src="/themes/beta/img/icons/merch_store_icon.png"> Shop</a> <a href="https://shop.furaffinity.net" target="_blank"><img class="menu-mini-icon" src="/themes/beta/img/icons/merch_store_icon.png"> Shop</a>
<br /> <br />
@@ -127,6 +137,7 @@
<hr> <hr>
<h2><a href="/browse/">Browse</a></h2> <h2><a href="/browse/">Browse</a></h2>
<h2><a href="/search/">Search</a></h2> <h2><a href="/search/">Search</a></h2>
<h2><a href="/submit/">Upload</a></h2>
<div class="nav-ac-container"> <div class="nav-ac-container">
<label for="mobile-menu-submenu-0"><h2 style="margin-top:0;padding-top:0">Support &#x25BC;</h2></label> <label for="mobile-menu-submenu-0"><h2 style="margin-top:0;padding-top:0">Support &#x25BC;</h2></label>
@@ -158,22 +169,65 @@
<h3>SUPPORT</h3> <h3>SUPPORT</h3>
<a href="/help/#contact">Contact Us</a><br /> <a href="/help/#contact">Contact Us</a><br />
<a href="/controls/troubletickets/">REPORT A PROBLEM</a><br />
<a href="https://status.furaffinity.net/">Site Status</a> <a href="https://status.furaffinity.net/">Site Status</a>
</article> </article>
</div> </div>
<div class="mobile-sfw-toggle">
<h2>SFW Mode</h2>
<div class="sfw-toggle type-slider slider-button-wrapper">
<input type="checkbox" id="sfw-toggle-mobile" class="slider-toggle" />
<label class="slider-viewport" for="sfw-toggle-mobile" title="Quick toggle to show or hide Mature and Adult submissions">
<div class="slider">
<div class="slider-button">&nbsp;</div>
<div class="slider-content left"><span>SFW</span></div>
<div class="slider-content right"><span>NSFW</span></div>
</div>
</label>
</div>
</div>
<div class="nav-ac-container">
<label for="mobile-menu-submenu-1"><h2 style="margin-top:0;padding-top:0">Settings &#x25BC;</h2></label>
<input id="mobile-menu-submenu-1" name="accordion-1" type="checkbox" />
<article class="nav-ac-content nav-ac-content-dropdown">
<h3>ACCOUNT INFORMATION</h3>
<a href="/controls/settings/">Account Settings</a><br>
<a href="/controls/site-settings/">Global Site Settings</a><br>
<a href="/controls/user-settings/">User Settings</a>
<h3>CUSTOMIZE USER PROFILE</h3>
<a href="/controls/profile/">Profile Info</a><br>
<a href="/controls/profilebanner/">Profile Banner</a><br>
<a href="/controls/contacts/">Contacts and Social Media</a><br>
<a href="/controls/avatar/">Avatar Management</a>
<h3>MANAGE MY CONTENT</h3>
<a href="/controls/submissions/">Submissions</a><br>
<a href="/controls/folders/submissions/">Folders</a><br>
<a href="/controls/journal/">Journals</a><br>
<a href="/controls/favorites/">Favorites</a><br>
<a href="/controls/buddylist/">Watches</a><br>
<a href="/controls/shouts/">Shouts</a><br>
<a href="/controls/badges/">Badges</a><br>
<a href="/controls/user-icons/">User Icons</a>
<h3>SECURITY</h3>
<a href="/controls/sessions/logins/">Active Sessions</a><br>
<a href="/controls/sessions/logs/">Activity Log</a><br>
<a href="/controls/sessions/labels/">Browser Labels</a>
</article>
</div>
<hr>
<hr> <hr>
<h2><div class="inline hideonmobile hideontablet"> <h2><form class="post-btn logout-link" method="post" action="/logout/"><button type="submit">Log Out</button><input type="hidden" name="key" value="deedfd5c0c9487a7aada91b469657f08debbabf69b0aab22cb3c20f5cc50a2ab"/></form>
<a href="/login"><strong>Log In</strong></a> or <a href="/register"><strong>Create an Account</strong></a> <script type="text/javascript">
</div> _fajs.push(['init_logout_button', '.logout-link button']);
</script>
<div class="inline hideondesktop">
<a href="/login">Log In</a><br>
<a href="/register">Create an Account</a>
</div>
</h2> </h2>
@@ -186,6 +240,11 @@
<div class="mobile-notification-bar">
<a class="notification-container inline" href="/msg/submissions/" title="5,161 Submission Notifications">5161S</a>
<a class="notification-container inline" href="/msg/others/#journals" title="75 Journal Notifications">75J</a>
</div>
@@ -245,6 +304,7 @@
<h3>Support</h3> <h3>Support</h3>
<a href="/help/#contact">Contact Us</a> <a href="/help/#contact">Contact Us</a>
<a href="/controls/troubletickets/">Report a Problem</a>
<a href="https://status.furaffinity.net/">Site Status</a> <a href="https://status.furaffinity.net/">Site Status</a>
</div> </div>
</div> </div>
@@ -270,18 +330,98 @@
<li class="no-sub"> <li class="message-bar-desktop">
<span class="top-heading"><div class="inline hideonmobile hideontablet"> <a class="notification-container inline" href="/msg/submissions/" title="5,161 Submission Notifications">5161S</a>
<a href="/login"><strong>Log In</strong></a> or <a href="/register"><strong>Create an Account</strong></a> <a class="notification-container inline" href="/msg/others/#journals" title="75 Journal Notifications">75J</a>
</div>
<div class="inline hideondesktop">
<a href="/login">Log In</a><br>
<a href="/register">Create an Account</a>
</div>
</span>
</li> </li>
</ul>
<li>
<div class="floatleft hideonmobile">
<a href="/user/soxx-thefennec"><img class="loggedin_user_avatar menubar-icon-resize avatar" style="cursor:pointer" alt="SoXX-TheFennec" src="//a.furaffinity.net/1515442832/soxx-thefennec.gif"/></a>
</div>
</li>
<li class="submenu-trigger">
<div class="floatleft hideonmobile">
<svg class="avatar-submenu-trigger banner-svg" xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24"><path d="M4 6h16v2H4zm0 5h16v2H4zm0 5h16v2H4z"></path></svg>
</div>
<a id="my-username" class="top-heading hideondesktop" href="#"><span class="hideondesktop">My FA ( </span>SoXX-TheFennec<span class="hideondesktop"> )</span></a>
<div class="dropdown dropdown-right">
<div class="dd-inner">
<div class="column">
<h3>Account</h3>
<a href="/user/soxx-thefennec/">My Userpage</a>
<a href="/msg/pms/">Check My Notes</a>
<a href="/controls/journal/">Create a Journal</a>
<a href="/commissions/soxx-thefennec/">My Commission Info</a>
<h3>Support Fur Affinity</h3>
<a href="/plus/">Subscribe to FA+ </a>
<a href="https://shop.furaffinity.net/" target="_blank">Merch Store</a>
<h3>Trouble Tickets</h3>
<a href="/controls/troubletickets/">Report a Problem</a>
<div class="mobile-sfw-toggle">
<h3 class="padding-top:10px">Toggle SFW</h3>
<div class="sfw-toggle type-slider slider-button-wrapper" style="position:relative;top:5px">
<input type="checkbox" id="sfw-toggle-mobile" class="slider-toggle" />
<label class="slider-viewport" for="sfw-toggle-mobile" title="Quick toggle to show or hide Mature and Adult submissions">
<div class="slider">
<div class="slider-button">&nbsp;</div>
<div class="slider-content left"><span>SFW</span></div>
<div class="slider-content right"><span>NSFW</span></div>
</div>
</label>
</div>
</div>
<hr>
<form class="post-btn logout-link" method="post" action="/logout/"><button type="submit">Log Out</button><input type="hidden" name="key" value="deedfd5c0c9487a7aada91b469657f08debbabf69b0aab22cb3c20f5cc50a2ab"/></form>
<script type="text/javascript">
_fajs.push(['init_logout_button', '.logout-link button']);
</script>
</div>
</div>
</div>
</li>
<li class="submenu-trigger">
<a class="top-heading" href="#"><svg class="banner-svg" xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24" style="transform: ;msFilter:;"><path d="M12 16c2.206 0 4-1.794 4-4s-1.794-4-4-4-4 1.794-4 4 1.794 4 4 4zm0-6c1.084 0 2 .916 2 2s-.916 2-2 2-2-.916-2-2 .916-2 2-2z"></path><path d="m2.845 16.136 1 1.73c.531.917 1.809 1.261 2.73.73l.529-.306A8.1 8.1 0 0 0 9 19.402V20c0 1.103.897 2 2 2h2c1.103 0 2-.897 2-2v-.598a8.132 8.132 0 0 0 1.896-1.111l.529.306c.923.53 2.198.188 2.731-.731l.999-1.729a2.001 2.001 0 0 0-.731-2.732l-.505-.292a7.718 7.718 0 0 0 0-2.224l.505-.292a2.002 2.002 0 0 0 .731-2.732l-.999-1.729c-.531-.92-1.808-1.265-2.731-.732l-.529.306A8.1 8.1 0 0 0 15 4.598V4c0-1.103-.897-2-2-2h-2c-1.103 0-2 .897-2 2v.598a8.132 8.132 0 0 0-1.896 1.111l-.529-.306c-.924-.531-2.2-.187-2.731.732l-.999 1.729a2.001 2.001 0 0 0 .731 2.732l.505.292a7.683 7.683 0 0 0 0 2.223l-.505.292a2.003 2.003 0 0 0-.731 2.733zm3.326-2.758A5.703 5.703 0 0 1 6 12c0-.462.058-.926.17-1.378a.999.999 0 0 0-.47-1.108l-1.123-.65.998-1.729 1.145.662a.997.997 0 0 0 1.188-.142 6.071 6.071 0 0 1 2.384-1.399A1 1 0 0 0 11 5.3V4h2v1.3a1 1 0 0 0 .708.956 6.083 6.083 0 0 1 2.384 1.399.999.999 0 0 0 1.188.142l1.144-.661 1 1.729-1.124.649a1 1 0 0 0-.47 1.108c.112.452.17.916.17 1.378 0 .461-.058.925-.171 1.378a1 1 0 0 0 .471 1.108l1.123.649-.998 1.729-1.145-.661a.996.996 0 0 0-1.188.142 6.071 6.071 0 0 1-2.384 1.399A1 1 0 0 0 13 18.7l.002 1.3H11v-1.3a1 1 0 0 0-.708-.956 6.083 6.083 0 0 1-2.384-1.399.992.992 0 0 0-1.188-.141l-1.144.662-1-1.729 1.124-.651a1 1 0 0 0 .471-1.108z"></path></svg></a>
<div class="dropdown dropdown-right">
<div class="dd-inner">
<div class="column">
<h3>Account Information</h3>
<a href="/controls/settings/">Account Settings</a>
<a href="/controls/site-settings/">Global Site Settings</a>
<a href="/controls/user-settings/">User Settings</a>
<h3>Customize User Profile</h3>
<a href="/controls/profile/">Profile Info</a>
<a href="/controls/profilebanner/">Profile Banner</a>
<a href="/controls/contacts/">Contacts & Social Media</a>
<a href="/controls/avatar/">Avatar Management</a>
<h3>Manage My Content</h3>
<a href="/controls/submissions/">Submissions</a>
<a href="/controls/folders/submissions/">Folders</a>
<a href="/controls/journal/">Journals</a>
<a href="/controls/favorites/">Favorites</a>
<a href="/controls/buddylist/">Watches</a>
<a href="/controls/shouts/">Shouts</a>
<a href="/controls/badges/">Badges</a>
<a href="/controls/user-icons/">User Icons</a>
<h3>Security</h3>
<a href="/controls/sessions/logins/">Active Sessions</a>
<a href="/controls/sessions/logs/">Activity Log</a>
<a href="/controls/sessions/labels/">Browser Labels</a>
</div>
</div>
</div>
</li>
</ul>
<script type="text/javascript"> <script type="text/javascript">
_fajs.push(['init_sfw_button', '.sfw-toggle']); _fajs.push(['init_sfw_button', '.sfw-toggle']);
</script> </script>
@@ -307,7 +447,16 @@
</script> </script>
<div class="news-block"> <div class="news-block">
</div>
<div id="news" class="newsBlock" data-date="1779756930">
<strong>News:</strong><span class="hideondesktop hideontablet"><br></span> <a class="journal-news-link" href="/journal/11365688">VGen Challenge 6 Day Reminder + Swag Pickup (<span class="c-contentRating--general" alt="General rating" title="General rating">G</span>)</a>
<span class="jsClose newsBlock__closeBtn" title="Close"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" style="transform: ;msFilter:;" title="Dismiss" ><path d="M20 3H4c-1.103 0-2 .897-2 2v14c0 1.103.897 2 2 2h16c1.103 0 2-.897 2-2V5c0-1.103-.897-2-2-2zM4 19V7h16l.001 12H4z"></path><path d="m15.707 10.707-1.414-1.414L12 11.586 9.707 9.293l-1.414 1.414L10.586 13l-2.293 2.293 1.414 1.414L12 14.414l2.293 2.293 1.414-1.414L13.414 13z"></path></svg></span>
</div>
<script type="text/javascript">
_fajs.push(['init_news_block', 'news']);
</script>
</div>
<div id="main-window" class="footer-mobile-tweak g-wrapper"> <div id="main-window" class="footer-mobile-tweak g-wrapper">
@@ -357,7 +506,7 @@
<div class="font-small"> <div class="font-small">
<span class="user-title"> <span class="user-title">
Fursuit Maker | <span class="hideonmobile">Registered:</span> <span class="popup_date" data-title-date="0" data-24-hour="0" data-time="1443468107" title="10 years ago" disabled>September 28, 2015 03:21:47 PM</span> </span> Fursuit Maker | <span class="hideonmobile">Registered:</span> <span class="popup_date" data-title-date="0" data-24-hour="0" data-time="1443468107" title="10 years ago" disabled>September 28, 2015 08:21:47 PM</span> </span>
</div> </div>
<userpage-nav-links> <userpage-nav-links>
@@ -377,7 +526,7 @@
<userpage-nav-interface-buttons> <userpage-nav-interface-buttons>
<a class="button standard samewidth go" style="text-transform: capitalize;" id="watch-button" href="/watch/kazucreations/?key=">Watch</a> <a class="button standard samewidth stop" style="text-transform: capitalize;" id="watch-button" href="/unwatch/kazucreations/?key=38c21dd8f8eb22a2497f68d1f7a901fc132a6e13c09d8004dec4627bcf7f36c2">Unwatch</a>
<a class="button standard" href="/newpm/kazucreations/"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" style="transform: ;msFilter:;"><path d="M20 4H4c-1.103 0-2 .897-2 2v12c0 1.103.897 2 2 2h16c1.103 0 2-.897 2-2V6c0-1.103-.897-2-2-2zm0 2v.511l-8 6.223-8-6.222V6h16zM4 18V9.044l7.386 5.745a.994.994 0 0 0 1.228 0L20 9.044 20.002 18H4z"></path></svg></a> <a class="button standard" href="/newpm/kazucreations/"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" style="transform: ;msFilter:;"><path d="M20 4H4c-1.103 0-2 .897-2 2v12c0 1.103.897 2 2 2h16c1.103 0 2-.897 2-2V6c0-1.103-.897-2-2-2zm0 2v.511l-8 6.223-8-6.222V6h16zM4 18V9.044l7.386 5.745a.994.994 0 0 0 1.228 0L20 9.044 20.002 18H4z"></path></svg></a>
</userpage-nav-interface-buttons> </userpage-nav-interface-buttons>
@@ -584,11 +733,11 @@
</div> </div>
<div class="online-stats"> <div class="online-stats">
88574 <strong><span title="Measured in the last 900 seconds">Users online</span></strong> &mdash; 91668 <strong><span title="Measured in the last 900 seconds">Users online</span></strong> &mdash;
3334 <strong>guests</strong>, 4823 <strong>guests</strong>,
8900 <strong>registered</strong> 14001 <strong>registered</strong>
and 76340 <strong>other</strong> and 72844 <strong>other</strong>
<!-- Online Counter Last Update: Sun, 24 May 2026 04:31:01 -0700 --> <!-- Online Counter Last Update: Tue, 02 Jun 2026 12:27:00 -0700 -->
</div> </div>
<small>Limit bot activity to periods with less than 10k registered users online.</small> <small>Limit bot activity to periods with less than 10k registered users online.</small>
@@ -596,8 +745,8 @@
<strong>&copy; 2005-2026 Frost Dragon Art LLC</strong> <strong>&copy; 2005-2026 Frost Dragon Art LLC</strong>
<div class="footnote"> <div class="footnote">
Server Time: May 24, 2026 04:31 AM<br /> Server Time: Jun 2, 2026 12:27 PM<br />
Page generated in 0.015 seconds<br />[ 38.7% PHP, 61.3% SQL ] (21 queries)<br /> Page generated in 0.025 seconds<br />[ 24.9% PHP, 75.1% SQL ] (29 queries)<br />
</div> </div>
</div> </div>
</div> </div>
@@ -628,7 +777,7 @@
<script src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js" crossorigin="anonymous"></script> <script src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js" crossorigin="anonymous"></script>
<script type="text/javascript"> <script type="text/javascript">
var server_timestamp = 1779622302; var server_timestamp = 1780428434;
var client_timestamp = Date.now() / 1000; var client_timestamp = Date.now() / 1000;
var server_timestamp_delta = server_timestamp - client_timestamp; var server_timestamp_delta = server_timestamp - client_timestamp;
var sfw_cookie_name = 'sfw'; var sfw_cookie_name = 'sfw';
@@ -637,7 +786,7 @@
// //
document.addEventListener("DOMContentLoaded", (event) => { document.addEventListener("DOMContentLoaded", (event) => {
// //
const ad_manager = new adManager({"sizeConfig":[{"labels":["desktopWide"],"mediaQuery":"(min-width: 1090px)","sizesSupported":[[728,90],[300,250],[300,168],[300,600],[160,600]]},{"labels":["desktopNarrow"],"mediaQuery":"(min-width: 740px) and (max-width: 1089px)","sizesSupported":[[728,90],[300,250],[300,168]]},{"labels":["mobile"],"mediaQuery":"(min-width: 0px) and (max-width: 739px)","sizesSupported":[[320,50],[300,50],[320,100]]}],"slotConfig":{"header_middle":{"containerSize":{"desktopWide":[728,90],"desktopNarrow":[728,90],"mobile":[320,50]},"providerPriority":["inhouse"]},"above_content":{"containerSize":{"desktopWide":[728,90],"desktopNarrow":[728,90],"mobile":[320,50]},"providerPriority":["inhouse"]},"sidebar":{"containerSize":{"desktopWide":[300,250]},"providerPriority":["inhouse"]},"sidebar_tall":{"containerSize":{"desktopWide":[300,600],"desktopNarrow":[300,600],"mobile":[300,250]},"providerPriority":["inhouse"]},"footer_left":{"containerSize":{"desktopWide":[300,250],"desktopNarrow":[300,250],"mobile":[300,250]},"providerPriority":["inhouse"]},"footer_right":{"containerSize":{"desktopWide":[300,250],"desktopNarrow":[300,250],"mobile":[300,250]},"providerPriority":["inhouse"]},"footer_right_top":{"containerSize":{"desktopWide":[320,50],"desktopNarrow":[320,50],"mobile":[320,50]},"providerPriority":["inhouse"]},"footer_right_bottom":{"containerSize":{"desktopWide":[320,50],"desktopNarrow":[320,50],"mobile":[320,50]},"providerPriority":["inhouse"]},"header_right_left":{"containerSize":{"desktopWide":[320,50]},"providerPriority":["inhouse"]},"header_right_right":{"containerSize":{"desktopWide":[320,50]},"providerPriority":["inhouse"]},"sidebar_top":{"containerSize":{"desktopWide":[320,50]},"providerPriority":["inhouse"]},"sidebar_bottom":{"containerSize":{"desktopWide":[320,50]},"providerPriority":["inhouse"]},"front_page":{"containerSize":{"desktopWide":[728,90],"desktopNarrow":[728,90],"mobile":[320,50]},"providerPriority":["inhouse"]},"c-videoAd":{"containerSize":{"desktopWide":[300,250]},"providerPriority":["inhouse"]}},"providerConfig":{"inhouse":{"domain":"https:\/\/rv.furaffinity.net","dataPath":"\/live\/www\/delivery\/spc.php","dataVariableName":"OA_output"}},"adConfig":{"inhouse":{"header_middle":{"default":{"tagId":40,"tagSize":[728,90]},"sizeOverride":{"mobile":{"tagId":56,"tagSize":[320,50]}}},"above_content":{"default":{"tagId":25,"tagSize":[728,90]},"sizeOverride":{"mobile":{"tagId":53,"tagSize":[320,50]}}},"sidebar":{"default":{"tagId":49,"tagSize":[300,250]}},"sidebar_tall":{"default":{"tagId":49,"tagSize":[300,250]}},"footer_left":{"default":{"tagId":28,"tagSize":[300,250]}},"footer_right":{"default":{"tagId":74,"tagSize":[300,250]}},"footer_right_top":{"default":{"tagId":62,"tagSize":[320,50]}},"footer_right_bottom":{"default":{"tagId":59,"tagSize":[320,50]}},"header_right_left":{"default":{"tagId":65,"tagSize":[320,50]}},"header_right_right":{"default":{"tagId":68,"tagSize":[320,50]}},"sidebar_top":{"default":{"tagId":65,"tagSize":[320,50]}},"sidebar_bottom":{"default":{"tagId":68,"tagSize":[320,50]}},"front_page":{"default":{"tagId":77,"tagSize":[728,90]},"sizeOverride":{"mobile":{"tagId":78,"tagSize":[320,50]}}},"c-videoAd":{"default":{"tagId":71,"tagSize":[320,50]}}}},"extraMetadata":{"adsenseClient":"ca-pub-3495616356562362","forceLoadConfigs":["c-videoAd"]}}, true); const ad_manager = new adManager({"sizeConfig":[{"labels":["desktopWide"],"mediaQuery":"(min-width: 1090px)","sizesSupported":[[728,90],[300,250],[300,168],[300,600],[160,600]]},{"labels":["desktopNarrow"],"mediaQuery":"(min-width: 740px) and (max-width: 1089px)","sizesSupported":[[728,90],[300,250],[300,168]]},{"labels":["mobile"],"mediaQuery":"(min-width: 0px) and (max-width: 739px)","sizesSupported":[[320,50],[300,50],[320,100]]}],"slotConfig":{"header_middle":{"containerSize":{"desktopWide":[728,90],"desktopNarrow":[728,90],"mobile":[320,50]},"providerPriority":["inhouse"]},"above_content":{"containerSize":{"desktopWide":[728,90],"desktopNarrow":[728,90],"mobile":[320,50]},"providerPriority":["inhouse"]},"sidebar":{"containerSize":{"desktopWide":[300,250]},"providerPriority":["inhouse"]},"sidebar_tall":{"containerSize":{"desktopWide":[300,600],"desktopNarrow":[300,600],"mobile":[300,250]},"providerPriority":["inhouse"]},"footer_left":{"containerSize":{"desktopWide":[300,250],"desktopNarrow":[300,250],"mobile":[300,250]},"providerPriority":["inhouse"]},"footer_right":{"containerSize":{"desktopWide":[300,250],"desktopNarrow":[300,250],"mobile":[300,250]},"providerPriority":["inhouse"]},"footer_right_top":{"containerSize":{"desktopWide":[320,50],"desktopNarrow":[320,50],"mobile":[320,50]},"providerPriority":["inhouse"]},"footer_right_bottom":{"containerSize":{"desktopWide":[320,50],"desktopNarrow":[320,50],"mobile":[320,50]},"providerPriority":["inhouse"]},"header_right_left":{"containerSize":{"desktopWide":[320,50]},"providerPriority":["inhouse"]},"header_right_right":{"containerSize":{"desktopWide":[320,50]},"providerPriority":["inhouse"]},"sidebar_top":{"containerSize":{"desktopWide":[320,50]},"providerPriority":["inhouse"]},"sidebar_bottom":{"containerSize":{"desktopWide":[320,50]},"providerPriority":["inhouse"]},"front_page":{"containerSize":{"desktopWide":[728,90],"desktopNarrow":[728,90],"mobile":[320,50]},"providerPriority":["inhouse"]},"c-videoAd":{"containerSize":{"desktopWide":[300,250]},"providerPriority":["inhouse"]}},"providerConfig":{"inhouse":{"domain":"https:\/\/rv.furaffinity.net","dataPath":"\/live\/www\/delivery\/spc.php","dataVariableName":"OA_output"}},"adConfig":{"inhouse":{"header_middle":{"default":{"tagId":42,"tagSize":[728,90]},"sizeOverride":{"mobile":{"tagId":58,"tagSize":[320,50]}}},"above_content":{"default":{"tagId":27,"tagSize":[728,90]},"sizeOverride":{"mobile":{"tagId":55,"tagSize":[320,50]}}},"sidebar":{"default":{"tagId":51,"tagSize":[300,250]}},"sidebar_tall":{"default":{"tagId":51,"tagSize":[300,250]}},"footer_left":{"default":{"tagId":30,"tagSize":[300,250]}},"footer_right":{"default":{"tagId":72,"tagSize":[300,250]}},"footer_right_top":{"default":{"tagId":64,"tagSize":[320,50]}},"footer_right_bottom":{"default":{"tagId":61,"tagSize":[320,50]}},"header_right_left":{"default":{"tagId":67,"tagSize":[320,50]}},"header_right_right":{"default":{"tagId":70,"tagSize":[320,50]}},"sidebar_top":{"default":{"tagId":67,"tagSize":[320,50]}},"sidebar_bottom":{"default":{"tagId":70,"tagSize":[320,50]}},"front_page":{"default":{"tagId":75,"tagSize":[728,90]},"sizeOverride":{"mobile":{"tagId":80,"tagSize":[320,50]}}},"c-videoAd":{"default":{"tagId":71,"tagSize":[320,50]}}}},"extraMetadata":{"adsenseClient":"ca-pub-3495616356562362","forceLoadConfigs":["c-videoAd"]}}, true);
}); });
</script> </script>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1782
testdata/html/user.html vendored

File diff suppressed because one or more lines are too long

View File

@@ -148,8 +148,8 @@ func TestParseUser_RealFixture(t *testing.T) {
if u.Stats.Favorites != 180 { if u.Stats.Favorites != 180 {
t.Errorf("Stats.Favorites = %d; want 180", u.Stats.Favorites) t.Errorf("Stats.Favorites = %d; want 180", u.Stats.Favorites)
} }
if u.Stats.Views != 1176 { if u.Stats.Views != 1184 {
t.Errorf("Stats.Views = %d; want 1176", u.Stats.Views) t.Errorf("Stats.Views = %d; want 1184", u.Stats.Views)
} }
if u.Stats.Comments != 85 { if u.Stats.Comments != 85 {
t.Errorf("Stats.Comments = %d; want 85", u.Stats.Comments) t.Errorf("Stats.Comments = %d; want 85", u.Stats.Comments)