mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-11 19:45:06 +00:00
feat(proxy): try bare --proxy providers in proxy_providers key order
Basic stays first and Control D last; the rest follow their config key order. Geofence and proxy_map lookups go through pick_proxy and skip a provider that fails.
This commit is contained in:
@@ -125,14 +125,18 @@ forwarder for it. See [Control D](#control-d).
|
||||
### Provider load order
|
||||
|
||||
When you do a download without `--no-proxy`, unshackle instantiates every configured
|
||||
proxy provider once, in this fixed order. It uses that same order for bare-query
|
||||
resolution:
|
||||
proxy provider once. It uses that same order for bare-query resolution:
|
||||
|
||||
**Basic → ExpressVPN → NordVPN → Proton VPN → Surfshark → Windscribe → Gluetun → Hola → Control D**
|
||||
1. Basic is always first, so your own proxies win.
|
||||
2. The other proxy providers follow in the order of their keys under `proxy_providers` in
|
||||
`unshackle.yaml`. Proxy providers that load without a config key, such as Hola, come
|
||||
after the listed ones in this default order:
|
||||
**ExpressVPN → NordVPN → Proton VPN → Surfshark → Windscribe → Gluetun → Hola**
|
||||
3. Control D is always last.
|
||||
|
||||
So if you have both Basic and NordVPN configured and use `--proxy us`, a `us` entry in
|
||||
your Basic config wins, because unshackle tries Basic first. To skip ahead to a specific
|
||||
proxy provider, prefix the query (`--proxy nordvpn:us`).
|
||||
So if you list `windscribevpn` above `nordvpn` and use `--proxy us`, a `us` entry in your
|
||||
Basic config answers first, then Windscribe. To skip ahead to a specific proxy provider,
|
||||
prefix the query (`--proxy nordvpn:us`).
|
||||
|
||||
If a proxy provider cannot load, for example because its server list cannot be fetched
|
||||
or its config is not valid, unshackle logs a warning and continues with the other proxy
|
||||
@@ -1025,21 +1029,14 @@ The returned proxy is HTTPS on **port 443**.
|
||||
|
||||
## Developer notes
|
||||
|
||||
!!! note "Two resolution paths (developers)"
|
||||
unshackle has **two** proxy-resolution code paths that are intentionally not
|
||||
identical:
|
||||
|
||||
- `unshackle/commands/dl.py` inlines its own logic and loads **all nine** providers,
|
||||
including Windscribe and Gluetun.
|
||||
- `unshackle/core/proxies/resolve.py::initialize_proxy_providers()` is the shared
|
||||
resolver used by the REST API handlers and the remote-service client. It **omits**
|
||||
Windscribe and Gluetun.
|
||||
|
||||
Both paths accept the same query grammar (a direct URI, a `provider:query`, a bare
|
||||
country query tried against providers in order, or a `controld://` resolver), but the
|
||||
set of available providers differs. Keep this in mind when you add a proxy provider:
|
||||
an entry in `dl.py` does not make it available over the API, and the opposite is also
|
||||
true.
|
||||
!!! note "One resolution path (developers)"
|
||||
`dl`, `search`, the REST API handlers and the remote-service client all load proxy
|
||||
providers through `load_proxy_providers()` in `unshackle/core/proxies/resolve.py`.
|
||||
The REST API and the remote-service client call it through
|
||||
`initialize_proxy_providers()`, which leaves out Gluetun because Gluetun starts a
|
||||
local Docker container. All paths accept the same query grammar: a direct URI, a
|
||||
`provider:query`, a bare query tried against providers in order, or a `controld://`
|
||||
resolver.
|
||||
|
||||
The remote-service client resolves `--proxy` with its own
|
||||
`resolve_remote_proxy_arg()`. It changes a `controld:` query to a `controld://`
|
||||
|
||||
@@ -61,8 +61,11 @@ headers:
|
||||
- **Type:** `dict` · **Default:** `{}`
|
||||
|
||||
Proxy/VPN provider configuration. Each sub-key names a proxy provider, and unshackle passes its
|
||||
block straight to that provider's constructor. See [Proxies & VPN](../../guide/proxies-and-vpn.md)
|
||||
for the full proxy provider guide. Recognised proxy providers and their exit ports:
|
||||
block straight to that provider's constructor. A bare `--proxy` query tries Basic first, then
|
||||
the other proxy providers in the order of their sub-keys, and Control D last (see
|
||||
[Provider load order](../../guide/proxies-and-vpn.md#provider-load-order)). See
|
||||
[Proxies & VPN](../../guide/proxies-and-vpn.md) for the full proxy provider guide. Recognised proxy
|
||||
providers and their exit ports:
|
||||
|
||||
| Provider | Config key | Credentials | Proxy scheme/port |
|
||||
|----------|-----------|-------------|-------------------|
|
||||
|
||||
@@ -63,6 +63,17 @@ def test_raise_errors_raises_the_load_failure() -> None:
|
||||
load_proxy_providers(OTHERS, raise_errors=True)
|
||||
|
||||
|
||||
def test_bare_query_order_follows_config_key_order_with_controld_last(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
from unshackle.core.proxies import ControlD
|
||||
|
||||
order = {"controld": {"token": "t"}, "surfsharkvpn": CREDENTIALS, "nordvpn": CREDENTIALS, "basic": {"us": "x"}}
|
||||
monkeypatch.setattr(config, "proxy_providers", order)
|
||||
monkeypatch.setattr(ControlD, "get_locations", lambda self: [])
|
||||
loaded = load_proxy_providers(("expressvpn", "protonvpn", "hola"), quiet=True)
|
||||
names = [getattr(p, "name", p.__class__.__name__) for p in loaded]
|
||||
assert names == ["Basic", "SurfsharkVPN", "NordVPN", "ControlD"]
|
||||
|
||||
|
||||
def test_rest_path_loads_windscribe_but_not_gluetun(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
from unshackle.core.proxies import Gluetun, WindscribeVPN
|
||||
from unshackle.core.proxies.resolve import initialize_proxy_providers
|
||||
|
||||
@@ -54,7 +54,7 @@ def find_provider(proxy_providers: List[Any], name: str, hint: str = "") -> Any:
|
||||
|
||||
def load_proxy_providers(exclude: tuple[str, ...] = (), raise_errors: bool = False, quiet: bool = False) -> List[Any]:
|
||||
"""
|
||||
Build the proxy providers set in config, in the order a bare query tries them.
|
||||
Build the proxy providers set in config: Basic first, then config key order, Control D last.
|
||||
|
||||
Each proxy provider loads on its own. One that fails is logged and kept as Unavailable, so a bare
|
||||
query skips it and a query that names it reports why. *raise_errors* raises the failure instead.
|
||||
@@ -75,8 +75,15 @@ def load_proxy_providers(exclude: tuple[str, ...] = (), raise_errors: bool = Fal
|
||||
)
|
||||
|
||||
settings = config.proxy_providers
|
||||
# Control D is last: a bare region reaches it only when nothing else answers, since it changes the account.
|
||||
classes = (Basic, ExpressVPN, NordVPN, ProtonVPN, SurfsharkVPN, WindscribeVPN, Gluetun, Hola, ControlD)
|
||||
rank = {key: i for i, key in enumerate(settings)}
|
||||
classes = [
|
||||
Basic,
|
||||
*sorted(
|
||||
(ExpressVPN, NordVPN, ProtonVPN, SurfsharkVPN, WindscribeVPN, Gluetun, Hola),
|
||||
key=lambda c: rank.get(c.__name__.lower(), len(rank)),
|
||||
),
|
||||
ControlD,
|
||||
]
|
||||
|
||||
def build(cls: type) -> Any:
|
||||
key = cls.__name__.lower()
|
||||
|
||||
+15
-41
@@ -25,7 +25,7 @@ from unshackle.core.constants import AnyTrack
|
||||
from unshackle.core.credential import Credential
|
||||
from unshackle.core.drm import DRM_T
|
||||
from unshackle.core.proxies.basic import Basic
|
||||
from unshackle.core.proxies.resolve import find_provider
|
||||
from unshackle.core.proxies.resolve import pick_proxy
|
||||
from unshackle.core.search_result import SearchResult
|
||||
from unshackle.core.session import (
|
||||
BACKOFF_FACTOR,
|
||||
@@ -229,40 +229,16 @@ class Service(metaclass=ABCMeta):
|
||||
self.log.info(
|
||||
f"Found service-specific proxy mapping: {full_proxy_key} -> {sanitize_proxy_for_log(mapped_value)}"
|
||||
)
|
||||
if proxy_provider_name:
|
||||
# Specific provider requested
|
||||
try:
|
||||
proxy_provider = find_provider(ctx.obj.proxy_providers, proxy_provider_name)
|
||||
except ValueError:
|
||||
proxy_provider = None
|
||||
if proxy_provider:
|
||||
mapped_proxy_uri = proxy_provider.get_proxy(mapped_value)
|
||||
if mapped_proxy_uri:
|
||||
proxy = mapped_proxy_uri
|
||||
self.log.info(
|
||||
f"Using mapped proxy from {proxy_provider.__class__.__name__}: "
|
||||
f"{sanitize_proxy_for_log(proxy, mask_host=isinstance(proxy_provider, Basic))}"
|
||||
)
|
||||
else:
|
||||
self.log.warning(
|
||||
f"Failed to get proxy for mapped value '{sanitize_proxy_for_log(mapped_value)}', using default"
|
||||
)
|
||||
else:
|
||||
self.log.warning(f"Proxy provider '{proxy_provider_name}' not found, using default proxy")
|
||||
else:
|
||||
for proxy_provider in ctx.obj.proxy_providers:
|
||||
mapped_proxy_uri = proxy_provider.get_proxy(mapped_value)
|
||||
if mapped_proxy_uri:
|
||||
proxy = mapped_proxy_uri
|
||||
self.log.info(
|
||||
f"Using mapped proxy from {proxy_provider.__class__.__name__}: "
|
||||
f"{sanitize_proxy_for_log(proxy, mask_host=isinstance(proxy_provider, Basic))}"
|
||||
)
|
||||
break
|
||||
else:
|
||||
self.log.warning(
|
||||
f"No provider could resolve mapped value '{sanitize_proxy_for_log(mapped_value)}', using default"
|
||||
)
|
||||
try:
|
||||
proxy_provider, proxy = pick_proxy(ctx.obj.proxy_providers, mapped_value, proxy_provider_name)
|
||||
self.log.info(
|
||||
f"Using mapped proxy from {proxy_provider.__class__.__name__}: "
|
||||
f"{sanitize_proxy_for_log(proxy, mask_host=isinstance(proxy_provider, Basic))}"
|
||||
)
|
||||
except ValueError:
|
||||
self.log.warning(
|
||||
f"No provider could resolve mapped value '{sanitize_proxy_for_log(mapped_value)}', using default"
|
||||
)
|
||||
|
||||
if not proxy:
|
||||
# don't override the explicit proxy set by the user, even if they may be geoblocked
|
||||
@@ -284,12 +260,10 @@ class Service(metaclass=ABCMeta):
|
||||
self.log.info(
|
||||
f"Service is Geoblocked in your region, getting a Proxy to {requested_proxy}"
|
||||
)
|
||||
for proxy_provider in ctx.obj.proxy_providers:
|
||||
proxy = proxy_provider.get_proxy(requested_proxy)
|
||||
if proxy:
|
||||
self.log.info(f"Got Proxy from {proxy_provider.__class__.__name__}")
|
||||
break
|
||||
if not proxy:
|
||||
try:
|
||||
proxy_provider, proxy = pick_proxy(ctx.obj.proxy_providers, requested_proxy)
|
||||
self.log.info(f"Got Proxy from {proxy_provider.__class__.__name__}")
|
||||
except ValueError:
|
||||
self.log.warning(
|
||||
f"No proxy available for {requested_proxy}. "
|
||||
f"Pass --proxy with a proxy in {requested_proxy}, or the request can fail."
|
||||
|
||||
Reference in New Issue
Block a user