From 40f7a8686c476a19d59ef48ec891b853867b688e Mon Sep 17 00:00:00 2001 From: sgerner Date: Sun, 31 May 2026 16:36:47 -0700 Subject: [PATCH] use browser headers for audiobookbay --- internal/search/audiobookbay.go | 15 ++++- internal/search/audiobookbay_test.go | 92 ++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 internal/search/audiobookbay_test.go diff --git a/internal/search/audiobookbay.go b/internal/search/audiobookbay.go index 410c2e6..69b65ec 100644 --- a/internal/search/audiobookbay.go +++ b/internal/search/audiobookbay.go @@ -14,6 +14,8 @@ import ( "github.com/PuerkitoBio/goquery" ) +const abbBrowserUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36" + // AudioBookBay searches an AudioBookBay-style scrape source for audiobook torrents. // Mirrors and trackers are loaded from the runtime sources registry. type AudioBookBay struct { @@ -59,7 +61,7 @@ func (a *AudioBookBay) searchDomain(ctx context.Context, domain, query string) ( q.Set("s", query) q.Set("tt", "1") req.URL.RawQuery = q.Encode() - req.Header.Set("User-Agent", a.cfg.UserAgent) + setABBRequestHeaders(req) resp, err := a.client.Do(req) if err != nil { @@ -137,6 +139,7 @@ func ResolveABBMagnet(ctx context.Context, client *http.Client, userAgent, abbPa if len(fallbackTrackers) == 0 { return "", fmt.Errorf("no AudioBookBay fallback trackers configured (registry not loaded?)") } + _ = userAgent // retained for API compatibility; ABB now uses a browser-like header set. infoHashRe := regexp.MustCompile(`(?i)Info\s*Hash:.*?]*>\s*([0-9a-fA-F]{40})`) trackerRe := regexp.MustCompile(`((?:udp|http)://[^<]+)`) @@ -149,7 +152,7 @@ func ResolveABBMagnet(ctx context.Context, client *http.Client, userAgent, abbPa if err != nil { continue } - req.Header.Set("User-Agent", userAgent) + setABBRequestHeaders(req) resp, err := client.Do(req) if err != nil { @@ -206,3 +209,11 @@ func ResolveABBMagnet(ctx context.Context, client *http.Client, userAgent, abbPa } return "", fmt.Errorf("failed to resolve ABB magnet from all domains") } + +func setABBRequestHeaders(req *http.Request) { + req.Header.Set("User-Agent", abbBrowserUserAgent) + req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8") + req.Header.Set("Accept-Language", "en-US,en;q=0.9") + req.Header.Set("Upgrade-Insecure-Requests", "1") + req.Header.Set("Accept-Encoding", "identity") +} diff --git a/internal/search/audiobookbay_test.go b/internal/search/audiobookbay_test.go new file mode 100644 index 0000000..b5b421e --- /dev/null +++ b/internal/search/audiobookbay_test.go @@ -0,0 +1,92 @@ +package search + +import ( + "context" + "io" + "net/http" + "strings" + "testing" + + "github.com/JeremiahM37/librarr/internal/config" + "github.com/JeremiahM37/librarr/internal/sources/sourcestest" +) + +type abbRoundTripper struct { + body string + req *http.Request +} + +func (r *abbRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + r.req = req.Clone(req.Context()) + return &http.Response{ + StatusCode: http.StatusOK, + Header: make(http.Header), + Body: io.NopCloser(strings.NewReader(r.body)), + Request: req, + }, nil +} + +func TestAudioBookBaySearchSetsBrowserHeaders(t *testing.T) { + rt := &abbRoundTripper{ + body: ` +
+ + +
+ `, + } + reg, err := sourcestest.Registry() + if err != nil { + t.Fatalf("load registry: %v", err) + } + cfg := &config.Config{ + UserAgent: "test", + Sources: reg, + } + cfg.Sources.AudioBookBay.Mirrors = []string{"audiobookbay.lu"} + a := &AudioBookBay{cfg: cfg, client: &http.Client{Transport: rt}} + + results, err := a.searchDomain(context.Background(), "audiobookbay.lu", "The Martian") + if err != nil { + t.Fatalf("searchDomain returned error: %v", err) + } + if len(results) != 1 { + t.Fatalf("expected 1 result, got %d", len(results)) + } + if got := rt.req.Header.Get("User-Agent"); got != abbBrowserUserAgent { + t.Fatalf("User-Agent = %q, want %q", got, abbBrowserUserAgent) + } + if got := rt.req.Header.Get("Accept-Language"); got != "en-US,en;q=0.9" { + t.Fatalf("Accept-Language = %q, want en-US,en;q=0.9", got) + } + if got := rt.req.Header.Get("Upgrade-Insecure-Requests"); got != "1" { + t.Fatalf("Upgrade-Insecure-Requests = %q, want 1", got) + } +} + +func TestResolveABBMagnetUsesBrowserHeaders(t *testing.T) { + rt := &abbRoundTripper{ + body: ` +

The Martian

+ + + +
Info Hash:0123456789ABCDEF0123456789ABCDEF01234567
udp://tracker.example:1337/announce
+ `, + } + client := &http.Client{Transport: rt} + + magnet, err := ResolveABBMagnet(context.Background(), client, "test", "/abss/the-martian-andy-weir/", []string{"audiobookbay.lu"}, []string{"udp://fallback.example:1337/announce"}) + if err != nil { + t.Fatalf("ResolveABBMagnet returned error: %v", err) + } + if !strings.HasPrefix(magnet, "magnet:?xt=urn:btih:0123456789ABCDEF0123456789ABCDEF01234567") { + t.Fatalf("unexpected magnet: %s", magnet) + } + if got := rt.req.Header.Get("User-Agent"); got != abbBrowserUserAgent { + t.Fatalf("User-Agent = %q, want %q", got, abbBrowserUserAgent) + } + if got := rt.req.Header.Get("Accept-Encoding"); got != "identity" { + t.Fatalf("Accept-Encoding = %q, want identity", got) + } +}