inital commit

This commit is contained in:
2026-05-25 22:27:18 +02:00
commit 965f9d6ad4
91 changed files with 28963 additions and 0 deletions

98
submission.go Normal file
View File

@@ -0,0 +1,98 @@
package fa
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"time"
"github.com/PuerkitoBio/goquery"
"git.anthrove.art/public/go-fa-api/internal/urls"
)
// Submission is a fully resolved FA submission as seen on /view/{id}/.
type Submission struct {
ID SubmissionID
Title string
Author UserRef
PostedAt time.Time
Rating Rating
Category Category
Type Type
Species Species
Gender Gender
Description string // raw HTML; sanitise before rendering to a browser
DescriptionText string // plaintext convenience
Tags []string
FileURL string // absolute CDN URL; pass to Download
ThumbURL string
Width int // 0 if unknown / non-image
Height int
Stats SubmissionStats
Folders []FolderRef
Prev SubmissionID // 0 if this is the oldest in the gallery
Next SubmissionID // 0 if this is the newest
// Favorited reports whether the authenticated viewer has favorited this
// submission. It is true only when the page was fetched with valid
// cookies and FA rendered the "Fav" (/unfav/) link. An anonymous fetch
// always yields false.
Favorited bool
}
// GetSubmission fetches the submission with the given numeric ID.
// Returns [ErrNotFound] if FA renders a "submission not found" system message,
// [ErrUnauthorized] for restricted-visibility submissions when called
// without valid cookies, or a wrapped parse error if the markup has shifted.
func (c *Client) GetSubmission(ctx context.Context, id SubmissionID) (*Submission, error) {
if id <= 0 {
return nil, fmt.Errorf("fa: GetSubmission: id must be > 0")
}
var out *Submission
err := c.fetch(ctx, urls.Submission(int64(id)), func(doc *goquery.Document) error {
s, err := parseSubmission(id, doc)
if err != nil {
return err
}
out = s
return nil
})
if err != nil {
return nil, err
}
return out, nil
}
// Download streams the submission's main file from the CDN into w. The same
// rate limiter that paces /view/ fetches paces CDN fetches, so an in-flight
// gallery iteration will yield correctly when Download is interleaved.
//
// Returns the number of bytes written. Errors from the writer are wrapped
// as-is; HTTP errors come back as [*HTTPError].
func (c *Client) Download(ctx context.Context, sub *Submission, w io.Writer) (int64, error) {
if sub == nil {
return 0, errors.New("fa: Download: nil submission")
}
if sub.FileURL == "" {
return 0, errors.New("fa: Download: submission has no FileURL")
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, sub.FileURL, nil)
if err != nil {
return 0, err
}
// CDN fetches share the same rate-limited transport as page fetches —
// see RoundTrip in transport.go where the limiter gates every request.
resp, err := c.http.Do(req)
if err != nil {
return 0, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
_, _ = io.Copy(io.Discard, resp.Body)
return 0, &HTTPError{StatusCode: resp.StatusCode, URL: sub.FileURL}
}
return io.Copy(w, resp.Body)
}