Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions gui/frontend/src/hooks/useSettingsState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,13 @@ const trackerSchemas: Record<string, FieldMeta[]> = {
trackerFieldMeta.UploaderName,
trackerFieldMeta.Anon,
],
FLD: [
trackerFieldMeta.FaviconURL,
trackerFieldMeta.LinkDirName,
trackerFieldMeta.APIKey,
trackerFieldMeta.AnnounceURL,
trackerFieldMeta.Anon,
],
FRIKI: [trackerFieldMeta.FaviconURL, trackerFieldMeta.LinkDirName, trackerFieldMeta.APIKey],
GPW: [
trackerFieldMeta.FaviconURL,
Expand Down
6 changes: 6 additions & 0 deletions internal/config/defaults/example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,12 @@ trackers:
uploader_name: ""
url: "https://filelist.io"
anon: false
FLD:
link_dir_name: ""
api_key: ""
announce_url: ""
url: "https://flood.st"
anon: false
FRIKI:
link_dir_name: ""
api_key: ""
Expand Down
1 change: 1 addition & 0 deletions internal/dupechecking/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ func buildHandlers(deps handlerDeps) map[string]searchHandler {
handlers["SPD"] = spdHandler{cfg: deps.cfg, http: deps.http}
handlers["TL"] = tlHandler{cfg: deps.cfg, http: deps.http}
handlers["TVC"] = tvcHandler{}
handlers["FLD"] = fldHandler{cfg: deps.cfg, http: deps.http}

// Explicit per-tracker stubs
handlers["ASC"] = ascHandler{cfg: deps.cfg, http: deps.http, logger: deps.logger}
Expand Down
98 changes: 98 additions & 0 deletions internal/dupechecking/fld.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package dupechecking

import (
"context"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"

"github.com/autobrr/upbrr/internal/config"
"github.com/autobrr/upbrr/pkg/api"
)

type fldHandler struct {
cfg config.Config
http *http.Client
baseURL string
}

func (h fldHandler) Search(ctx context.Context, meta api.PreparedMetadata, _ string) ([]api.DupeEntry, []string, error) {
_, apiKey, ok := trackerCfgWithAPIKey(h.cfg, "FLD")
if !ok {
return nil, []string{noteSkip("missing api_key for tracker FLD")}, nil
}

tmdb := meta.ExternalIDs.TMDBID
if tmdb == 0 {
return nil, []string{noteSkip("missing tmdb id for FLD dupe search")}, nil
}

category := strings.ToUpper(strings.TrimSpace(meta.ExternalIDs.Category))
if category == "" {
category = strings.ToUpper(strings.TrimSpace(meta.MediaInfoCategory))
}

params := url.Values{}
if category == "TV" {
params.Set("tmdb_id", fmt.Sprintf("tv/%d", tmdb))
if meta.SeasonInt > 0 {
params.Set("show_season_number", strconv.Itoa(meta.SeasonInt))
}
if meta.EpisodeInt > 0 {
params.Set("show_episode_number", strconv.Itoa(meta.EpisodeInt))
}
} else {
params.Set("tmdb_id", fmt.Sprintf("movie/%d", tmdb))
}

headers := map[string]string{
"Authorization": "Bearer " + apiKey,
}

apiURL := "https://flood.st/api/torrents"
if h.baseURL != "" {
apiURL = h.baseURL
}

status, payload, err := doJSONGetAny(ctx, h.http, apiURL, params, headers)
if err != nil || status < 200 || status >= 300 {
return nil, []string{noteSkip("FLD search failed")}, nil
}

rawMap, ok := payload.(map[string]any)
if !ok {
return nil, nil, nil
}

itemsSlice, ok := anyToSlice(rawMap["items"])
if !ok {
return nil, nil, nil
}

entries := make([]api.DupeEntry, 0, len(itemsSlice))
for _, raw := range itemsSlice {
item, ok := raw.(map[string]any)
if !ok {
continue
}
id := stringFromAny(item["id"])
name := stringFromAny(item["name"])
mainURL := stringFromAny(item["main_url"])
size := intFromAny(item["size"])

entry := api.DupeEntry{
Name: name,
ID: id,
Link: mainURL,
}
if size > 0 {
entry.SizeKnown = true
entry.SizeBytes = size
}
entries = append(entries, entry)
}

return entries, nil, nil
}
206 changes: 206 additions & 0 deletions internal/dupechecking/fld_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
package dupechecking

import (
"context"
"io"
"net/http"
"net/http/httptest"
"testing"

"github.com/autobrr/upbrr/internal/config"
"github.com/autobrr/upbrr/pkg/api"
)

func TestFLDHandlerSearchSkips(t *testing.T) {
t.Parallel()

cases := []struct {
name string
apiKey string
tmdbID int
category string
expectedSkip string
}{
{
name: "Missing API Key",
apiKey: "",
tmdbID: 12345,
expectedSkip: "missing api_key for tracker FLD",
},
{
name: "Missing TMDb ID",
apiKey: "key",
tmdbID: 0,
expectedSkip: "missing tmdb id for FLD dupe search",
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
cfg := config.Config{
Trackers: config.TrackersConfig{
Trackers: map[string]config.TrackerConfig{
"FLD": {APIKey: tc.apiKey},
},
},
}
handler := fldHandler{
cfg: cfg,
http: http.DefaultClient,
}
meta := api.PreparedMetadata{
ExternalIDs: api.ExternalIDs{
TMDBID: tc.tmdbID,
Category: tc.category,
},
}
entries, notes, err := handler.Search(context.Background(), meta, "FLD")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(entries) != 0 {
t.Fatalf("expected 0 entries, got %d", len(entries))
}
if len(notes) != 1 || !stringsContains(notes[0], tc.expectedSkip) {
t.Fatalf("expected skip note containing %q, got %v", tc.expectedSkip, notes)
}
})
}
}

func TestFLDHandlerSearchMovie(t *testing.T) {
t.Parallel()

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if auth := r.Header.Get("Authorization"); auth != "Bearer key123" {
t.Errorf("unexpected authorization header")
}
if id := r.URL.Query().Get("tmdb_id"); id != "movie/12345" {
t.Errorf("expected tmdb_id movie/12345, got %q", id)
}

w.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(w, `{
"items": [
{
"id": "1",
"name": "Movie.2026.1080p.BluRay-GRP",
"main_url": "https://flood.st/torrents/1",
"size": 4500000000
}
]
}`)
}))
defer server.Close()

cfg := config.Config{
Trackers: config.TrackersConfig{
Trackers: map[string]config.TrackerConfig{
"FLD": {APIKey: "key123"},
},
},
}
handler := fldHandler{
cfg: cfg,
http: server.Client(),
baseURL: server.URL,
}

meta := api.PreparedMetadata{
ExternalIDs: api.ExternalIDs{
TMDBID: 12345,
Category: "MOVIE",
},
}

entries, notes, err := handler.Search(context.Background(), meta, "FLD")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(notes) != 0 {
t.Fatalf("unexpected notes: %v", notes)
}
if len(entries) != 1 {
t.Fatalf("expected 1 entry, got %d", len(entries))
}
if entries[0].Name != "Movie.2026.1080p.BluRay-GRP" {
t.Errorf("expected name Movie.2026.1080p.BluRay-GRP, got %q", entries[0].Name)
}
if entries[0].ID != "1" {
t.Errorf("expected ID 1, got %q", entries[0].ID)
}
if entries[0].Link != "https://flood.st/torrents/1" {
t.Errorf("expected link https://flood.st/torrents/1, got %q", entries[0].Link)
}
if !entries[0].SizeKnown || entries[0].SizeBytes != 4500000000 {
t.Errorf("expected size 4500000000, got %d (known: %t)", entries[0].SizeBytes, entries[0].SizeKnown)
}
}

func TestFLDHandlerSearchTV(t *testing.T) {
t.Parallel()

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
if id := q.Get("tmdb_id"); id != "tv/54321" {
t.Errorf("expected tmdb_id tv/54321, got %q", id)
}
if s := q.Get("show_season_number"); s != "2" {
t.Errorf("expected season 2, got %q", s)
}
if e := q.Get("show_episode_number"); e != "3" {
t.Errorf("expected episode 3, got %q", e)
}

w.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(w, `{"items": []}`)
}))
defer server.Close()

cfg := config.Config{
Trackers: config.TrackersConfig{
Trackers: map[string]config.TrackerConfig{
"FLD": {APIKey: "key123"},
},
},
}
handler := fldHandler{
cfg: cfg,
http: server.Client(),
baseURL: server.URL,
}

meta := api.PreparedMetadata{
ExternalIDs: api.ExternalIDs{
TMDBID: 54321,
Category: "TV",
},
SeasonInt: 2,
EpisodeInt: 3,
}

entries, notes, err := handler.Search(context.Background(), meta, "FLD")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(notes) != 0 {
t.Fatalf("unexpected notes: %v", notes)
}
if len(entries) != 0 {
t.Fatalf("expected 0 entries, got %d", len(entries))
}
}

func stringsContains(s, sub string) bool {
return len(s) >= len(sub) && (s == sub || stringsContainsSimple(s, sub))
}

func stringsContainsSimple(s, sub string) bool {
for i := 0; i <= len(s)-len(sub); i++ {
if s[i:i+len(sub)] == sub {
return true
}
}
return false
}
9 changes: 9 additions & 0 deletions internal/services/bbcode/tracker_profiles.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ func FinalizeTrackerDescription(tracker string, desc string) string {
return finalizeFFDescription(trimmed)
case "FL":
return finalizeFLDescription(trimmed)
case "FLD":
return finalizeFLDDescription(trimmed)
case "GPW":
return finalizeGPWDescription(trimmed)
case "HDS":
Expand Down Expand Up @@ -128,6 +130,13 @@ func finalizeFLDescription(value string) string {
return removeExtraLines(value)
}

func finalizeFLDDescription(value string) string {
value = strings.ReplaceAll(value, "[user]", "")
value = strings.ReplaceAll(value, "[/user]", "")
value = strings.ReplaceAll(value, "[img]", "[img width=300]")
return removeExtraLines(value)
}

func finalizeGPWDescription(value string) string {
value = removeSup(value)
value = removeSub(value)
Expand Down
14 changes: 14 additions & 0 deletions internal/services/bbcode/tracker_profiles_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,17 @@ func TestFinalizeTrackerDescriptionTL(t *testing.T) {
t.Fatalf("expected comparison converted for TL, got %q", got)
}
}

func TestFinalizeTrackerDescriptionFLD(t *testing.T) {
input := "[user]John[/user]\n[img]https://img.example/a.png[/img]\n[img width=350]https://img.example/b.png[/img]"
got := FinalizeTrackerDescription("FLD", input)
if strings.Contains(got, "[user]") || strings.Contains(got, "[/user]") {
t.Fatalf("expected user tags removed for FLD, got %q", got)
}
if !strings.Contains(got, "[img width=300]https://img.example/a.png[/img]") {
t.Fatalf("expected img tags resized to 300 for FLD, got %q", got)
}
if !strings.Contains(got, "[img width=350]https://img.example/b.png[/img]") {
t.Fatalf("expected pre-sized img tags preserved for FLD, got %q", got)
}
}
Loading
Loading