Skip to content

feat: add Real-Debrid API client for premium download speeds#325

Open
mvanhorn wants to merge 5 commits intoSurgeDM:mainfrom
mvanhorn:feat/debrid-integration
Open

feat: add Real-Debrid API client for premium download speeds#325
mvanhorn wants to merge 5 commits intoSurgeDM:mainfrom
mvanhorn:feat/debrid-integration

Conversation

@mvanhorn
Copy link
Copy Markdown
Contributor

@mvanhorn mvanhorn commented Apr 5, 2026

Summary

Add a Real-Debrid API client that can unrestrict file hosting links to get direct download URLs. Includes host detection and debrid settings in the TUI.

Why this matters

From the README: "Debrid Integration: Covering subscription costs so we can test and build native Debrid support" is listed as a donation goal. JDownloader has debrid support and it's a major reason power users choose it. No TUI download manager has debrid integration.

Real-Debrid unrestricts links from file hosts like Mega, RapidGator, and Uploaded, providing direct high-speed download URLs for ~$4/month.

Changes

  • Add internal/debrid/ package with Client struct wrapping the Real-Debrid REST API
  • Unrestrict(link) sends a URL to Real-Debrid and gets back a direct download URL
  • SupportedHosts() fetches the list of supported file hosting domains
  • IsSupported(url) checks if a URL's host is debrid-eligible
  • Add DebridSettings to config (enabled, provider, API key) with TUI settings tab
  • Error handling for expired tokens, unsupported hosts, quota exceeded

Wiring into the probe flow (auto-unrestrict debrid-eligible URLs) will follow in a separate PR. This PR establishes the client and settings.

Testing

All tests use mock HTTP servers, no Real-Debrid account needed:

Tests

go test ./... -count=1   # All 19 packages pass

This contribution was developed with AI assistance (Codex + Claude Code).

Greptile Summary

This PR adds a internal/debrid package with a Real-Debrid API client (Unrestrict, SupportedHosts, IsSupported), wires DebridSettings into the config, and adds a working Debrid tab in the TUI settings modal. Previous concerns around the per-call HTTP round-trip, data races on the host cache, and missing TUI wiring have all been addressed in follow-up commits on the branch.

  • P1 — resetSettingToDefault silently no-ops on the Debrid tab (view_settings.go:648): the function has no \"Debrid\" case, so the reset-to-default action does nothing for all three Debrid fields.
  • P2 — API key displayed in plain text (view_settings.go:279): the credential is rendered unmasked in the TUI panel.

Confidence Score: 4/5

Safe to merge after fixing the silent no-op in resetSettingToDefault for the Debrid tab.

One P1 defect remains: resetting Debrid settings to defaults via the UI silently does nothing because resetSettingToDefault has no Debrid branch. All prior review concerns (caching, data race, TUI wiring) have been resolved. The fix is a straightforward addition.

internal/tui/view_settings.go — resetSettingToDefault needs a Debrid case.

Security Review

  • Plain-text credential in TUI (internal/tui/view_settings.go): the debrid api_key is rendered unmasked in the settings panel; an observer with terminal access (screen recording, shoulder surfing) can read the key.

Important Files Changed

Filename Overview
internal/debrid/realdebrid.go New Real-Debrid API client with Unrestrict, SupportedHosts, and IsSupported; includes a properly double-checked RWMutex cache for the host list.
internal/debrid/realdebrid_test.go Good mock-server coverage for happy path, missing key, and API error flows; TestIsSupported does not verify that the cache prevents more than one HTTP call.
internal/config/settings.go Adds DebridSettings struct and wires it into NetworkSettings, GetSettingsMetadata, CategoryOrder, and DefaultSettings; clean and consistent with existing patterns.
internal/config/settings_test.go Existing tests updated to assert 5 categories; comprehensive round-trip, corruption, and partial-JSON tests all pass.
internal/tui/view_settings.go Debrid tab fully wired in getSettingsValues, setSettingValue, and setDebridSetting, but resetSettingToDefault has no Debrid case so the reset action is silently ignored on that tab.

Sequence Diagram

sequenceDiagram
    participant TUI as TUI (view_settings)
    participant Config as config.Settings
    participant Client as debrid.Client
    participant Cache as In-memory cache
    participant RD as Real-Debrid API

    TUI->>Config: Read/write DebridSettings (enabled, provider, api_key)
    Config-->>TUI: DebridSettings values

    Note over TUI,Config: Future wiring (separate PR)
    TUI->>Client: NewClient(apiKey)
    Client->>Client: Unrestrict(link)
    Client->>RD: POST /unrestrict/link
    RD-->>Client: UnrestrictResult{Download: direct URL}

    Client->>Client: IsSupported(url)
    Client->>Cache: supportedHostsCached()
    alt cache hit (< 1h old)
        Cache-->>Client: []string{hosts}
    else cache miss
        Client->>RD: GET /hosts/domains
        RD-->>Client: []string{hosts}
        Client->>Cache: store + timestamp
        Cache-->>Client: []string{hosts}
    end
    Client-->>TUI: true/false
Loading

Comments Outside Diff (3)

  1. internal/tui/view_settings.go, line 247-283 (link)

    P1 Debrid tab will display nothing and edits won't apply

    getSettingsValues, setSettingValue, and resetSettingToDefault all use switch category blocks that handle "General", "Network", "Performance", and "Categories" — but none has a "Debrid" case. When the user opens the Debrid tab: values show empty, toggling enabled does nothing, editing api_key is silently discarded, and Reset has no effect. The tab is visible but completely inert.

    The three functions need "Debrid" cases wired to m.Settings.Network.Debrid.{Enabled,Provider,APIKey}.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: internal/tui/view_settings.go
    Line: 247-283
    
    Comment:
    **Debrid tab will display nothing and edits won't apply**
    
    `getSettingsValues`, `setSettingValue`, and `resetSettingToDefault` all use `switch category` blocks that handle `"General"`, `"Network"`, `"Performance"`, and `"Categories"` — but none has a `"Debrid"` case. When the user opens the Debrid tab: values show empty, toggling `enabled` does nothing, editing `api_key` is silently discarded, and Reset has no effect. The tab is visible but completely inert.
    
    The three functions need `"Debrid"` cases wired to `m.Settings.Network.Debrid.{Enabled,Provider,APIKey}`.
    
    How can I resolve this? If you propose a fix, please make it concise.
  2. internal/tui/view_settings.go, line 248-283 (link)

    P1 Debrid tab is silently non-functional in the TUI

    getSettingsValues, setSettingValue, and resetSettingToDefault all dispatch on category name via switch statements, but none of them have a "Debrid" case. The result is:

    • Display: getSettingsValues("Debrid") returns an empty map, so every Debrid setting renders as blank in the right pane.
    • Editing: setSettingValue("Debrid", ...) falls through all cases and returns nil without writing anything — user edits are silently dropped.
    • Reset: resetSettingToDefault("Debrid", ...) is a no-op for the same reason.

    The tab is visible and navigable, but every interaction with it is a silent no-op. The three functions all need a "Debrid" arm wired to m.Settings.Network.Debrid.*. For example in getSettingsValues:

    case "Debrid":
        values["enabled"]  = m.Settings.Network.Debrid.Enabled
        values["provider"] = m.Settings.Network.Debrid.Provider
        values["api_key"]  = m.Settings.Network.Debrid.APIKey

    And a corresponding setDebridSetting / reset block covering enabled, provider, and api_key.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: internal/tui/view_settings.go
    Line: 248-283
    
    Comment:
    **Debrid tab is silently non-functional in the TUI**
    
    `getSettingsValues`, `setSettingValue`, and `resetSettingToDefault` all dispatch on category name via `switch` statements, but none of them have a `"Debrid"` case. The result is:
    
    - **Display**: `getSettingsValues("Debrid")` returns an empty map, so every Debrid setting renders as blank in the right pane.
    - **Editing**: `setSettingValue("Debrid", ...)` falls through all cases and returns `nil` without writing anything — user edits are silently dropped.
    - **Reset**: `resetSettingToDefault("Debrid", ...)` is a no-op for the same reason.
    
    The tab is visible and navigable, but every interaction with it is a silent no-op. The three functions all need a `"Debrid"` arm wired to `m.Settings.Network.Debrid.*`. For example in `getSettingsValues`:
    
    ```go
    case "Debrid":
        values["enabled"]  = m.Settings.Network.Debrid.Enabled
        values["provider"] = m.Settings.Network.Debrid.Provider
        values["api_key"]  = m.Settings.Network.Debrid.APIKey
    ```
    
    And a corresponding `setDebridSetting` / reset block covering `enabled`, `provider`, and `api_key`.
    
    How can I resolve this? If you propose a fix, please make it concise.
  3. internal/tui/view_settings.go, line 648-709 (link)

    P1 resetSettingToDefault missing Debrid branch

    resetSettingToDefault covers "General", "Network", "Performance", and "Categories" but has no "Debrid" branch. Pressing the reset-to-default key while the Debrid tab is active silently does nothing — Enabled, Provider, and APIKey cannot be reset through the UI.

    A case "Debrid" block mirroring the pattern used for other categories (reset each key to its corresponding defaults.Network.Debrid.* value) is needed to close this gap.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: internal/tui/view_settings.go
    Line: 648-709
    
    Comment:
    **`resetSettingToDefault` missing Debrid branch**
    
    `resetSettingToDefault` covers `"General"`, `"Network"`, `"Performance"`, and `"Categories"` but has no `"Debrid"` branch. Pressing the reset-to-default key while the Debrid tab is active silently does nothing — `Enabled`, `Provider`, and `APIKey` cannot be reset through the UI.
    
    A `case "Debrid"` block mirroring the pattern used for other categories (reset each key to its corresponding `defaults.Network.Debrid.*` value) is needed to close this gap.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: internal/debrid/realdebrid_test.go
Line: 78-90

Comment:
**Cache hit not verified in `TestIsSupported`**

The test calls `IsSupported` three times and checks correctness, but never confirms that the mock server is only contacted once. A counter in the handler would verify that `supportedHostsCached` actually prevents repeated HTTP round-trips:

```go
func TestIsSupported(t *testing.T) {
    var callCount int
    server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        callCount++
        _ = json.NewEncoder(w).Encode([]string{"mega.nz", "rapidgator.net"})
    }))
    defer server.Close()

    client := NewClient("test-key")
    client.baseURL = server.URL

    assert.True(t, client.IsSupported("https://mega.nz/file/abc"))
    assert.True(t, client.IsSupported("https://www.mega.nz/file/abc"))
    assert.False(t, client.IsSupported("https://example.com/file"))

    assert.Equal(t, 1, callCount, "hosts list should be fetched only once due to caching")
}
```

**Rule Used:** What: All code changes must include tests for edge... ([source](https://app.greptile.com/review/custom-context?memory=2b22782d-3452-4d55-b059-e631b2540ce8))

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: internal/tui/view_settings.go
Line: 279-282

Comment:
**Credential rendered in plain text**

The debrid `api_key` value is passed to `formatSettingValue` as a plain string, so it appears unmasked in the TUI (truncated at 30 chars but still readable). Anyone who can see the terminal — screencast, shoulder-surf, screenshot — will see the credential. Consider masking with bullet characters when the field is not actively being edited, similar to how password fields work in other TUIs.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: internal/tui/view_settings.go
Line: 648-709

Comment:
**`resetSettingToDefault` missing Debrid branch**

`resetSettingToDefault` covers `"General"`, `"Network"`, `"Performance"`, and `"Categories"` but has no `"Debrid"` branch. Pressing the reset-to-default key while the Debrid tab is active silently does nothing — `Enabled`, `Provider`, and `APIKey` cannot be reset through the UI.

A `case "Debrid"` block mirroring the pattern used for other categories (reset each key to its corresponding `defaults.Network.Debrid.*` value) is needed to close this gap.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (5): Last reviewed commit: "fix: wire up Debrid settings tab in TUI" | Re-trigger Greptile

Add a debrid package with a Real-Debrid API client that can unrestrict
file hosting links to get direct download URLs. Includes host detection
to check if a URL is from a supported file host (Mega, RapidGator, etc).

Adds debrid settings to the configuration (enabled, provider, API key)
with a new Debrid category in the TUI settings. Wiring into the probe
flow will follow in a separate PR.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 5, 2026

Binary Size Analysis

⚠️ Size Increased

Version Human Readable Raw Bytes
Main 18.91 MB 19829028
PR 18.91 MB 19833124
Difference 4.00 KB 4096

@mvanhorn
Copy link
Copy Markdown
Contributor Author

mvanhorn commented Apr 6, 2026

Cached the SupportedHosts() result with a 1-hour TTL so IsSupported doesn't make a live HTTP call on every invocation. Pushed in cf6ad5c.

Addresses greptile P1 finding: cachedHosts and hostsCached were accessed
without synchronization. Uses double-checked locking pattern to minimize
lock contention on the hot path.
@mvanhorn
Copy link
Copy Markdown
Contributor Author

mvanhorn commented Apr 8, 2026

Fixed the data race on cachedHosts/hostsCached in 6f1fa3a - added sync.RWMutex with double-checked locking so concurrent IsSupported calls are safe.

Regarding the other findings:

  • P1 (live HTTP call): IsSupported already uses supportedHostsCached() which caches for 1 hour. The bot may have missed the indirection.
  • P2 (inline comments): the struct tag comments explain the field's purpose beyond the JSON key name, keeping them.

Comment on lines 122 to 124
func CategoryOrder() []string {
return []string{"General", "Network", "Performance", "Categories"}
return []string{"General", "Network", "Performance", "Debrid", "Categories"}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Debrid TUI tab is visible but non-functional

CategoryOrder() now includes "Debrid", so the tab appears in the settings UI. However, view_settings.go was not updated: getSettingValues has no case "Debrid" (all three fields will display as blank), and setSettingValue also has no case "Debrid" (edits will be silently discarded). The PR description explicitly promises a working TUI settings tab, but the wiring is missing.

view_settings.go needs two additions:

// in getSettingValues
case "Debrid":
    values["enabled"] = m.Settings.Network.Debrid.Enabled
    values["provider"] = m.Settings.Network.Debrid.Provider
    values["api_key"] = m.Settings.Network.Debrid.APIKey
// in setSettingValue switch
case "Debrid":
    return m.setDebridSetting(key, value, meta.Type)

…plus a corresponding setDebridSetting method.

Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/config/settings.go
Line: 122-124

Comment:
**Debrid TUI tab is visible but non-functional**

`CategoryOrder()` now includes `"Debrid"`, so the tab appears in the settings UI. However, `view_settings.go` was not updated: `getSettingValues` has no `case "Debrid"` (all three fields will display as blank), and `setSettingValue` also has no `case "Debrid"` (edits will be silently discarded). The PR description explicitly promises a working TUI settings tab, but the wiring is missing.

`view_settings.go` needs two additions:

```go
// in getSettingValues
case "Debrid":
    values["enabled"] = m.Settings.Network.Debrid.Enabled
    values["provider"] = m.Settings.Network.Debrid.Provider
    values["api_key"] = m.Settings.Network.Debrid.APIKey
```

```go
// in setSettingValue switch
case "Debrid":
    return m.setDebridSetting(key, value, meta.Type)
```

…plus a corresponding `setDebridSetting` method.

How can I resolve this? If you propose a fix, please make it concise.

Add getSettingsValues case for "Debrid" category and setDebridSetting
method so the Debrid tab in settings UI is functional (was visible but
non-interactive). Addresses greptile bot finding.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@mvanhorn
Copy link
Copy Markdown
Contributor Author

Addressed the greptile findings in 2cb1c98 - wired up the Debrid settings tab in view_settings.go (getSettingsValues and setDebridSetting were missing). The caching and synchronization findings were already handled in the existing implementation (supportedHostsCached with RWMutex).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant