Skip to content

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

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

feat: add Real-Debrid API client for premium download speeds#325
mvanhorn wants to merge 6 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 introduces a internal/debrid package with a Real-Debrid API client, DebridSettings in config, and a working Debrid tab in the TUI settings UI. Previous concerns (data race on cached hosts, live HTTP call per IsSupported invocation, missing TUI wiring, redundant comments) have all been addressed in follow-up commits.

  • P1 — resetSettingToDefault in view_settings.go is missing a case \"Debrid\" branch. Pressing the Reset key while on the Debrid settings tab calls resetSettingToDefault(\"Debrid\", key, defaults), which falls through the switch silently, so no field is ever restored to its default.

Confidence Score: 4/5

Safe to merge after fixing the missing Debrid case in resetSettingToDefault; all other changes are correct.

One P1 defect remains: pressing Reset on any Debrid setting silently does nothing because resetSettingToDefault has no Debrid branch. All prior concerns from earlier review rounds are resolved. Everything else is P2 or lower.

internal/tui/view_settings.go — resetSettingToDefault function missing Debrid case

Important Files Changed

Filename Overview
internal/tui/view_settings.go getSettingsValues and setSettingValue/setDebridSetting correctly wired for Debrid, but resetSettingToDefault is missing a Debrid case, silently dropping all reset actions on the Debrid tab.
internal/debrid/realdebrid.go Well-structured Real-Debrid client with correct RWMutex double-check locking for the 5-minute host cache; no issues found.
internal/debrid/realdebrid_test.go Good mock-server coverage for happy path and error cases; TestIsSupported doesn't verify the cache limits HTTP calls to one, leaving the caching contract untested.
internal/config/settings.go Clean addition of DebridSettings struct, metadata, and defaults; CategoryOrder correctly includes Debrid.
internal/config/settings_test.go Tests updated to expect 5 categories; comprehensive round-trip, corruption, and metadata validation coverage.

Sequence Diagram

sequenceDiagram
    participant TUI as TUI (view_settings)
    participant Client as debrid.Client
    participant Cache as Host Cache (RWMutex)
    participant RD as Real-Debrid API

    Note over TUI,RD: Unrestrict flow
    TUI->>Client: Unrestrict(link)
    Client->>RD: POST /unrestrict/link (Bearer token)
    RD-->>Client: UnrestrictResult{Download URL}
    Client-->>TUI: *UnrestrictResult

    Note over TUI,RD: IsSupported flow (with caching)
    TUI->>Client: IsSupported(url)
    Client->>Cache: RLock check cachedHosts (TTL 5m)
    alt cache hit
        Cache-->>Client: []string (cached)
    else cache miss
        Cache-->>Client: nil
        Client->>Cache: Lock (double-check)
        Client->>RD: GET /hosts/domains (no auth)
        RD-->>Client: []string domains
        Client->>Cache: store hosts + timestamp
        Cache-->>Client: []string
    end
    Client-->>TUI: bool
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/tui/view_settings.go
Line: 648-709

Comment:
**`resetSettingToDefault` silently ignores Debrid resets**

`resetSettingToDefault` has no `case "Debrid"` branch. When a user navigates to the Debrid tab and presses the Reset key, `update_settings.go:148` calls this function with `currentCategory == "Debrid"`, the switch falls through without doing anything, and the reset is silently discarded. A Debrid branch mirroring the same pattern used by the other categories is needed here.

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/debrid/realdebrid_test.go
Line: 78-90

Comment:
**Cache behaviour is not verified by the test**

`TestIsSupported` makes three `IsSupported` calls but the mock server never counts requests, so the test won't catch a regression where the cache is bypassed and every call fires a separate HTTP round-trip. A request counter on the test server (e.g., `atomic.Int32`) would confirm only one network call is made for the three lookups.

**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.

Reviews (6): Last reviewed commit: "fix: reduce host cache TTL to 5min and u..." | 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

Comment thread internal/debrid/realdebrid.go
Comment thread internal/debrid/realdebrid.go Outdated
@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.

Comment thread internal/debrid/realdebrid.go
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).

Address greptile P1 findings:
- Reduce supportedHostsCached TTL from 1 hour to 5 minutes for
  fresher host availability checks
- Capture time.Now() once and reuse for consistent TTL comparisons
  across the read-lock and write-lock paths

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

Checked the greptile findings against the code:

  • P1 "IsSupported makes live HTTP call": Already cached — IsSupported calls supportedHostsCached() (line 161) which caches the hosts list with a 5-minute TTL.
  • P1 "Unsynchronized cache fields": Already synchronized — cachedHosts and hostsCached are protected by c.mu (sync.RWMutex). Read path uses RLock/RUnlock (lines 129-135), write path uses Lock/Unlock (lines 137-138) with double-checked locking at lines 140-143.
  • P1 "Debrid TUI tab non-functional": Already implemented — view_settings.go handles the Debrid category at lines 278-281 (getSettingsForCategory) and lines 309-310 (setDebridSetting).
  • P2 inline comments: Removed the ones that restated field names.

@sonarqubecloud
Copy link
Copy Markdown

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