inital commit
This commit is contained in:
55
examples/basic/main.go
Normal file
55
examples/basic/main.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// basic demonstrates use of the SDK: fetching a single submission and
|
||||
// printing a few fields.
|
||||
//
|
||||
// The example runs anonymously by default. If the FA_A / FA_B (and ideally
|
||||
// CF_CLEARANCE + FA_UA) environment variables are set, it authenticates
|
||||
// with them required for any submission FA gates behind login, mature
|
||||
// content guard, or Cloudflare challenges.
|
||||
//
|
||||
// go run ./examples/basic 12345678
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
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\nrating: %s\ntags: %v\nfile: %s\n",
|
||||
sub.Title, sub.Author.DisplayName, sub.Rating, sub.Tags, sub.FileURL)
|
||||
}
|
||||
|
||||
func envOr(key, fallback string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
54
examples/browse/main.go
Normal file
54
examples/browse/main.go
Normal file
@@ -0,0 +1,54 @@
|
||||
// browse prints the global front-page feed at /browse/ FA's "what's new
|
||||
// across the site" stream. Uses anon access by default; honours FA_A /
|
||||
// FA_B / CF_CLEARANCE / FA_UA from env when set (recommended, since FA
|
||||
// gates parts of the feed behind login + Cloudflare).
|
||||
//
|
||||
// go run ./examples/browse # 1 page, ~72 items
|
||||
// go run ./examples/browse 3 # 3 pages
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
fa "git.anthrove.art/public/go-fa-api"
|
||||
)
|
||||
|
||||
func main() {
|
||||
maxPages := 1
|
||||
if len(os.Args) >= 2 {
|
||||
if n, err := strconv.Atoi(os.Args[1]); err == nil && n > 0 {
|
||||
maxPages = n
|
||||
}
|
||||
}
|
||||
|
||||
opts := []fa.Option{fa.WithUserAgent("go-fa-api-example/0.1")}
|
||||
if a, b := os.Getenv("FA_A"), os.Getenv("FA_B"); a != "" && b != "" {
|
||||
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...)
|
||||
|
||||
count := 0
|
||||
for sub, err := range client.Browse(context.Background(), fa.BrowseOptions{MaxPages: maxPages}) {
|
||||
if err != nil {
|
||||
log.Fatalf("Browse: %v", err)
|
||||
}
|
||||
count++
|
||||
fmt.Printf("[%d] %s %s by %s\n", sub.ID, sub.Title, sub.Rating, sub.Author.DisplayName)
|
||||
}
|
||||
fmt.Printf("\n%d submissions across %d page(s)\n", count, maxPages)
|
||||
}
|
||||
|
||||
func envOr(key, fallback string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
49
examples/download/main.go
Normal file
49
examples/download/main.go
Normal file
@@ -0,0 +1,49 @@
|
||||
// download fetches a single submission and streams its main file to disk.
|
||||
// Demonstrates that downloads share the SDK's rate limiter and cookie jar
|
||||
// with metadata fetches.
|
||||
//
|
||||
// go run ./examples/download 12345678 out.jpg
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
fa "git.anthrove.art/public/go-fa-api"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 3 {
|
||||
log.Fatalf("usage: %s <submission-id> <out-path>", os.Args[0])
|
||||
}
|
||||
id, err := strconv.ParseInt(os.Args[1], 10, 64)
|
||||
if err != nil {
|
||||
log.Fatalf("invalid id: %v", err)
|
||||
}
|
||||
|
||||
client := fa.New(
|
||||
fa.WithCookies(fa.Cookies{A: os.Getenv("FA_A"), B: os.Getenv("FA_B")}),
|
||||
fa.WithCloudflare(fa.CFCookies{Clearance: os.Getenv("CF_CLEARANCE")}),
|
||||
fa.WithUserAgent(os.Getenv("FA_UA")),
|
||||
)
|
||||
|
||||
ctx := context.Background()
|
||||
sub, err := client.GetSubmission(ctx, fa.SubmissionID(id))
|
||||
if err != nil {
|
||||
log.Fatalf("GetSubmission: %v", err)
|
||||
}
|
||||
|
||||
out, err := os.Create(os.Args[2])
|
||||
if err != nil {
|
||||
log.Fatalf("create: %v", err)
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
n, err := client.Download(ctx, sub, out)
|
||||
if err != nil {
|
||||
log.Fatalf("download: %v", err)
|
||||
}
|
||||
log.Printf("wrote %d bytes to %s", n, os.Args[2])
|
||||
}
|
||||
54
examples/gallery_dump/main.go
Normal file
54
examples/gallery_dump/main.go
Normal file
@@ -0,0 +1,54 @@
|
||||
// gallery_dump iterates a user's gallery (authenticated) and prints the
|
||||
// title and ID of every submission encountered, honouring the SDK's default
|
||||
// 1 req/sec rate limit.
|
||||
//
|
||||
// Required environment variables:
|
||||
//
|
||||
// FA_A — the `a` session cookie
|
||||
// FA_B — the `b` session cookie
|
||||
// CF_CLEARANCE — the cf_clearance cookie from the same browser session
|
||||
// FA_UA — the User-Agent string that produced CF_CLEARANCE
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// go run ./examples/gallery_dump <username> [maxPages]
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
fa "git.anthrove.art/public/go-fa-api"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
log.Fatalf("usage: %s <username> [maxPages]", os.Args[0])
|
||||
}
|
||||
user := os.Args[1]
|
||||
maxPages := 0
|
||||
if len(os.Args) >= 3 {
|
||||
if n, err := strconv.Atoi(os.Args[2]); err == nil {
|
||||
maxPages = n
|
||||
}
|
||||
}
|
||||
|
||||
client := fa.New(
|
||||
fa.WithCookies(fa.Cookies{A: os.Getenv("FA_A"), B: os.Getenv("FA_B")}),
|
||||
fa.WithCloudflare(fa.CFCookies{Clearance: os.Getenv("CF_CLEARANCE")}),
|
||||
fa.WithUserAgent(os.Getenv("FA_UA")),
|
||||
)
|
||||
|
||||
count := 0
|
||||
for sub, err := range client.Gallery(context.Background(), user, fa.ListOptions{MaxPages: maxPages}) {
|
||||
if err != nil {
|
||||
log.Fatalf("iter: %v", err)
|
||||
}
|
||||
count++
|
||||
fmt.Printf("[%d] %s %s\n", sub.ID, sub.Title, sub.Rating)
|
||||
}
|
||||
fmt.Printf("\n%d submissions\n", count)
|
||||
}
|
||||
76
examples/inbox/main.go
Normal file
76
examples/inbox/main.go
Normal file
@@ -0,0 +1,76 @@
|
||||
// inbox prints the logged-in user's new submissions the "what's new
|
||||
// from people you watch" feed at https://www.furaffinity.net/msg/submissions/.
|
||||
//
|
||||
// Requires FA_A and FA_B (session cookies). CF_CLEARANCE + FA_UA are
|
||||
// strongly recommended without them FurAffinity's Cloudflare layer will
|
||||
// usually serve a challenge page instead of the inbox.
|
||||
//
|
||||
// go run ./examples/inbox # first page (~72 items)
|
||||
// go run ./examples/inbox 3 # up to 3 cursor pages (~216 items)
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
fa "git.anthrove.art/public/go-fa-api"
|
||||
)
|
||||
|
||||
func main() {
|
||||
maxPages := 1
|
||||
if len(os.Args) >= 2 {
|
||||
if n, err := strconv.Atoi(os.Args[1]); err == nil && n > 0 {
|
||||
maxPages = n
|
||||
}
|
||||
}
|
||||
|
||||
a, b := os.Getenv("FA_A"), os.Getenv("FA_B")
|
||||
if a == "" || b == "" {
|
||||
log.Fatal("FA_A and FA_B must be set the submission inbox requires login")
|
||||
}
|
||||
//if os.Getenv("CF_CLEARANCE") == "" || os.Getenv("FA_UA") == "" {
|
||||
// log.Println("warning: CF_CLEARANCE / FA_UA not set expect a Cloudflare challenge")
|
||||
//}
|
||||
|
||||
client := fa.New(
|
||||
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")),
|
||||
// JSON-first listing parse: more resilient to FA markup tweaks,
|
||||
// and populates each item's author avatar URL.
|
||||
fa.WithExperimentalJSONListings(false),
|
||||
)
|
||||
|
||||
count := 0
|
||||
for sub, err := range client.SubmissionInbox(context.Background(), fa.ListOptions{MaxPages: maxPages}) {
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, fa.ErrCloudflareChallenge):
|
||||
log.Fatal("Cloudflare challenge refresh CF_CLEARANCE + FA_UA from your browser and retry")
|
||||
case errors.Is(err, fa.ErrUnauthorized):
|
||||
log.Fatal("unauthorized FA_A / FA_B are missing or expired")
|
||||
default:
|
||||
log.Fatalf("SubmissionInbox: %v", err)
|
||||
}
|
||||
}
|
||||
count++
|
||||
when := "—"
|
||||
if !sub.PostedAt.IsZero() {
|
||||
when = sub.PostedAt.Format("2006-01-02 15:04")
|
||||
}
|
||||
fmt.Printf("[%d] %-50.50s %-8s by %s (%s)\n",
|
||||
sub.ID, sub.Title, sub.Rating, sub.Author.DisplayName, when)
|
||||
}
|
||||
fmt.Printf("\n%d new submission(s) across up to %d page(s)\n", count, maxPages)
|
||||
}
|
||||
|
||||
func envOr(key, fallback string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
73
examples/notes/main.go
Normal file
73
examples/notes/main.go
Normal file
@@ -0,0 +1,73 @@
|
||||
// notes dumps the /msg/pms/ inbox listing and, if an argument is given,
|
||||
// prints the full body of that note id. Requires FA_A / FA_B in env.
|
||||
//
|
||||
// go run ./examples/notes # list inbox
|
||||
// go run ./examples/notes 131012623 # read one note
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
fa "git.anthrove.art/public/go-fa-api"
|
||||
)
|
||||
|
||||
func main() {
|
||||
a, b := os.Getenv("FA_A"), os.Getenv("FA_B")
|
||||
if a == "" || b == "" {
|
||||
log.Fatal("FA_A and FA_B must be set notes require login")
|
||||
}
|
||||
client := fa.New(
|
||||
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")),
|
||||
)
|
||||
ctx := context.Background()
|
||||
|
||||
if len(os.Args) >= 2 {
|
||||
id, err := strconv.ParseInt(os.Args[1], 10, 64)
|
||||
if err != nil {
|
||||
log.Fatalf("invalid note id: %v", err)
|
||||
}
|
||||
n, err := client.GetNote(ctx, fa.NoteID(id))
|
||||
if err != nil {
|
||||
log.Fatalf("GetNote: %v", err)
|
||||
}
|
||||
fmt.Printf("Subject: %s\nFrom: %s (@%s)\nTo: %s (@%s)\nSent: %s\n\n%s\n",
|
||||
n.Subject,
|
||||
n.From.DisplayName, n.From.Name,
|
||||
n.To.DisplayName, n.To.Name,
|
||||
n.SentAt.Format("2006-01-02 15:04"),
|
||||
n.BodyText)
|
||||
return
|
||||
}
|
||||
|
||||
count := 0
|
||||
for np, err := range client.Notes(ctx, fa.ListOptions{MaxPages: 1}) {
|
||||
if err != nil {
|
||||
log.Fatalf("Notes: %v", err)
|
||||
}
|
||||
count++
|
||||
unread := " "
|
||||
if np.Unread {
|
||||
unread = "*"
|
||||
}
|
||||
from := np.Sender.DisplayName
|
||||
if from == "" {
|
||||
from = np.Sender.Name
|
||||
}
|
||||
fmt.Printf("[%s] [%d] %s from %s (%s)\n",
|
||||
unread, np.ID, np.Subject, from, np.SentAt.Format("2006-01-02 15:04"))
|
||||
}
|
||||
fmt.Printf("\n%d notes on first page\n", count)
|
||||
}
|
||||
|
||||
func envOr(key, fallback string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
70
examples/notifications/main.go
Normal file
70
examples/notifications/main.go
Normal file
@@ -0,0 +1,70 @@
|
||||
// notifications dumps the full /msg/others/ page watch / journal /
|
||||
// comment / fav / shout notifications, single fetch, all categories.
|
||||
// Requires FA_A / FA_B in env (and ideally CF_CLEARANCE + FA_UA).
|
||||
//
|
||||
// go run ./examples/notifications
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
fa "git.anthrove.art/public/go-fa-api"
|
||||
)
|
||||
|
||||
func main() {
|
||||
a, b := os.Getenv("FA_A"), os.Getenv("FA_B")
|
||||
if a == "" || b == "" {
|
||||
log.Fatal("FA_A and FA_B must be set notifications require login")
|
||||
}
|
||||
client := fa.New(
|
||||
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")),
|
||||
)
|
||||
|
||||
n, err := client.Notifications(context.Background())
|
||||
if err != nil {
|
||||
log.Fatalf("Notifications: %v", err)
|
||||
}
|
||||
|
||||
section("Journals", len(n.Journals))
|
||||
for _, j := range n.Journals {
|
||||
fmt.Printf(" [%d] %s by %s (%s, %s)\n",
|
||||
j.JournalID, j.Title, j.Author.DisplayName, j.Rating,
|
||||
j.PostedAt.Format("2006-01-02 15:04"))
|
||||
}
|
||||
section("New watchers", len(n.Watches))
|
||||
for _, w := range n.Watches {
|
||||
fmt.Printf(" %s (%s)\n", w.User.DisplayName, w.WatchedAt.Format("2006-01-02 15:04"))
|
||||
}
|
||||
section("Submission comments", len(n.SubmissionComments))
|
||||
for _, c := range n.SubmissionComments {
|
||||
fmt.Printf(" on submission %d (%q) by %s\n", c.OnSubmission, c.OnTitle, c.Author.DisplayName)
|
||||
}
|
||||
section("Journal comments", len(n.JournalComments))
|
||||
for _, c := range n.JournalComments {
|
||||
fmt.Printf(" on journal %d (%q) by %s\n", c.OnJournal, c.OnTitle, c.Author.DisplayName)
|
||||
}
|
||||
section("Favorites", len(n.Favorites))
|
||||
for _, f := range n.Favorites {
|
||||
fmt.Printf(" %s favorited %d (%q)\n", f.Favoriter.DisplayName, f.SubmissionID, f.SubmissionTitle)
|
||||
}
|
||||
section("Shouts", len(n.Shouts))
|
||||
for _, s := range n.Shouts {
|
||||
fmt.Printf(" shout from %s (%s)\n", s.Author.DisplayName, s.PostedAt.Format("2006-01-02 15:04"))
|
||||
}
|
||||
}
|
||||
|
||||
func section(name string, count int) {
|
||||
fmt.Printf("\n=== %s (%d) ===\n", name, count)
|
||||
}
|
||||
|
||||
func envOr(key, fallback string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
117
examples/priority/main.go
Normal file
117
examples/priority/main.go
Normal file
@@ -0,0 +1,117 @@
|
||||
// priority demonstrates multi-level rate-limiter priority.
|
||||
//
|
||||
// A background gallery crawl runs continuously at fa.PriorityBackground
|
||||
// while interactive submission fetches at fa.PriorityInteractive jump ahead
|
||||
// of it even though the crawl started first and both share one global
|
||||
// token bucket. (fa.PriorityNormal and fa.PriorityLow sit between the two;
|
||||
// the same rule applies higher priority is always served first.)
|
||||
//
|
||||
// The example runs anonymously by default. If the FA_A / FA_B (and ideally
|
||||
// CF_CLEARANCE + FA_UA) environment variables are set, it authenticates
|
||||
// with them.
|
||||
//
|
||||
// Watch the timestamps in the output: once the interactive fetches are
|
||||
// issued, they take the next tokens and the background crawl is pushed back.
|
||||
//
|
||||
// go run ./examples/priority <username> <submission-id> [<submission-id>...]
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
fa "git.anthrove.art/public/go-fa-api"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 3 {
|
||||
log.Fatalf("usage: %s <username> <submission-id> [<submission-id>...]", os.Args[0])
|
||||
}
|
||||
user := os.Args[1]
|
||||
ids := make([]fa.SubmissionID, 0, len(os.Args)-2)
|
||||
for _, arg := range os.Args[2:] {
|
||||
n, err := strconv.ParseInt(arg, 10, 64)
|
||||
if err != nil {
|
||||
log.Fatalf("invalid submission id %q: %v", arg, err)
|
||||
}
|
||||
ids = append(ids, fa.SubmissionID(n))
|
||||
}
|
||||
|
||||
// Priority scheduling must be enabled at construction time; without
|
||||
// WithPrioritizedRateLimiting the WithPriority markers below are inert.
|
||||
opts := []fa.Option{
|
||||
fa.WithUserAgent(envOr("FA_UA", "go-fa-api-example/0.1")),
|
||||
fa.WithPrioritizedRateLimiting(true),
|
||||
}
|
||||
if a, b := os.Getenv("FA_A"), os.Getenv("FA_B"); a != "" && b != "" {
|
||||
log.Printf("using FA_A/FA_B cookies for authenticated requests")
|
||||
opts = append(opts,
|
||||
fa.WithCookies(fa.Cookies{A: a, B: b}),
|
||||
fa.WithCloudflare(fa.CFCookies{Clearance: os.Getenv("CF_CLEARANCE")}),
|
||||
)
|
||||
}
|
||||
client := fa.New(opts...)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
start := time.Now()
|
||||
since := func() string { return fmt.Sprintf("%5.1fs", time.Since(start).Seconds()) }
|
||||
|
||||
// Background crawler: walks the user's gallery at the lowest priority,
|
||||
// keeping the shared token bucket busy so the priority effect is visible.
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
bgCtx := fa.WithBackgroundPriority(ctx)
|
||||
n := 0
|
||||
for sub, err := range client.Gallery(bgCtx, user, fa.ListOptions{}) {
|
||||
if err != nil {
|
||||
if ctx.Err() == nil { // not just our own cancel
|
||||
log.Printf("[%s] background crawl stopped: %v", since(), err)
|
||||
}
|
||||
return
|
||||
}
|
||||
n++
|
||||
fmt.Printf("[%s] background · gallery item %3d (%d) %s\n",
|
||||
since(), n, sub.ID, sub.Title)
|
||||
}
|
||||
fmt.Printf("[%s] background · gallery exhausted after %d items\n", since(), n)
|
||||
}()
|
||||
|
||||
// Give the crawler a head start so it is mid-crawl, holding the queue,
|
||||
// when the interactive fetches arrive.
|
||||
time.Sleep(1500 * time.Millisecond)
|
||||
|
||||
// Interactive fetches: the user is waiting on these. Each takes the very
|
||||
// next token, ahead of the background crawl's pending page fetch.
|
||||
for _, id := range ids {
|
||||
ictx := fa.WithPriority(ctx, fa.PriorityInteractive)
|
||||
t0 := time.Now()
|
||||
sub, err := client.GetSubmission(ictx, id)
|
||||
if err != nil {
|
||||
log.Printf("[%s] interactive · GetSubmission(%d): %v", since(), id, err)
|
||||
continue
|
||||
}
|
||||
fmt.Printf("[%s] INTERACTIVE · got (%d) %q in %.1fs jumped the queue\n",
|
||||
since(), id, sub.Title, time.Since(t0).Seconds())
|
||||
}
|
||||
|
||||
// Foreground work is done stop the background crawler and wait for it.
|
||||
cancel()
|
||||
wg.Wait()
|
||||
fmt.Printf("[%s] done\n", since())
|
||||
}
|
||||
|
||||
func envOr(key, fallback string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
68
examples/search/main.go
Normal file
68
examples/search/main.go
Normal file
@@ -0,0 +1,68 @@
|
||||
// search runs a /search/?q=... query and prints the first N pages of
|
||||
// matches. Works anonymously for general-rated results; mature/adult
|
||||
// searches require login.
|
||||
//
|
||||
// go run ./examples/search dragon
|
||||
// go run ./examples/search "fox knight" --max=2 --rating=general --order=date
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
fa "git.anthrove.art/public/go-fa-api"
|
||||
)
|
||||
|
||||
func main() {
|
||||
maxPages := flag.Int("max", 1, "max pages to fetch")
|
||||
ratingFlag := flag.String("rating", "", "comma-separated: general, mature, adult (empty = all)")
|
||||
orderFlag := flag.String("order", "", "relevancy | date | popularity (empty = default)")
|
||||
flag.Parse()
|
||||
|
||||
if flag.NArg() == 0 {
|
||||
log.Fatalf("usage: %s [flags] <query>", os.Args[0])
|
||||
}
|
||||
query := strings.Join(flag.Args(), " ")
|
||||
|
||||
opts := []fa.Option{fa.WithUserAgent("go-fa-api-example/0.1")}
|
||||
if a, b := os.Getenv("FA_A"), os.Getenv("FA_B"); a != "" && b != "" {
|
||||
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...)
|
||||
|
||||
so := fa.SearchOptions{MaxPages: *maxPages}
|
||||
if *ratingFlag != "" {
|
||||
for _, r := range strings.Split(*ratingFlag, ",") {
|
||||
so.Ratings = append(so.Ratings, fa.ParseRating(strings.TrimSpace(r)))
|
||||
}
|
||||
}
|
||||
if *orderFlag != "" {
|
||||
so.OrderBy = fa.SearchOrder(*orderFlag)
|
||||
}
|
||||
|
||||
count := 0
|
||||
for sub, err := range client.Search(context.Background(), query, so) {
|
||||
if err != nil {
|
||||
log.Fatalf("Search: %v", err)
|
||||
}
|
||||
count++
|
||||
fmt.Printf("[%d] %s %s by %s\n",
|
||||
sub.ID, sub.Title, sub.Rating, sub.Author.DisplayName)
|
||||
}
|
||||
fmt.Printf("\n%d results for %q across up to %d page(s)\n", count, query, *maxPages)
|
||||
}
|
||||
|
||||
func envOr(key, fallback string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
Reference in New Issue
Block a user