mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2025-10-23 15:11:08 +00:00
feat(proxies): ✨ Add SurfsharkVPN support
Original code by @p0llux12 - Discord - Introduced `SurfsharkVPN` class for proxy service integration. - Updated configuration to include `surfsharkvpn` in proxy providers. - Removed legacy `nordvpn` configuration from YAML. - Enhanced `dl.py` and `search.py` to utilize `SurfsharkVPN`.
This commit is contained in:
@@ -48,7 +48,7 @@ from unshackle.core.constants import DOWNLOAD_LICENCE_ONLY, AnyTrack, context_se
|
|||||||
from unshackle.core.credential import Credential
|
from unshackle.core.credential import Credential
|
||||||
from unshackle.core.drm import DRM_T, PlayReady, Widevine
|
from unshackle.core.drm import DRM_T, PlayReady, Widevine
|
||||||
from unshackle.core.events import events
|
from unshackle.core.events import events
|
||||||
from unshackle.core.proxies import Basic, Hola, NordVPN
|
from unshackle.core.proxies import Basic, Hola, NordVPN, SurfsharkVPN
|
||||||
from unshackle.core.service import Service
|
from unshackle.core.service import Service
|
||||||
from unshackle.core.services import Services
|
from unshackle.core.services import Services
|
||||||
from unshackle.core.titles import Movie, Movies, Series, Song, Title_T
|
from unshackle.core.titles import Movie, Movies, Series, Song, Title_T
|
||||||
@@ -309,6 +309,8 @@ class dl:
|
|||||||
self.proxy_providers.append(Basic(**config.proxy_providers["basic"]))
|
self.proxy_providers.append(Basic(**config.proxy_providers["basic"]))
|
||||||
if config.proxy_providers.get("nordvpn"):
|
if config.proxy_providers.get("nordvpn"):
|
||||||
self.proxy_providers.append(NordVPN(**config.proxy_providers["nordvpn"]))
|
self.proxy_providers.append(NordVPN(**config.proxy_providers["nordvpn"]))
|
||||||
|
if config.proxy_providers.get("surfsharkvpn"):
|
||||||
|
self.proxy_providers.append(SurfsharkVPN(**config.proxy_providers["surfsharkvpn"]))
|
||||||
if binaries.HolaProxy:
|
if binaries.HolaProxy:
|
||||||
self.proxy_providers.append(Hola())
|
self.proxy_providers.append(Hola())
|
||||||
for proxy_provider in self.proxy_providers:
|
for proxy_provider in self.proxy_providers:
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from unshackle.core import binaries
|
|||||||
from unshackle.core.config import config
|
from unshackle.core.config import config
|
||||||
from unshackle.core.console import console
|
from unshackle.core.console import console
|
||||||
from unshackle.core.constants import context_settings
|
from unshackle.core.constants import context_settings
|
||||||
from unshackle.core.proxies import Basic, Hola, NordVPN
|
from unshackle.core.proxies import Basic, Hola, NordVPN, SurfsharkVPN
|
||||||
from unshackle.core.service import Service
|
from unshackle.core.service import Service
|
||||||
from unshackle.core.services import Services
|
from unshackle.core.services import Services
|
||||||
from unshackle.core.utils.click_types import ContextData
|
from unshackle.core.utils.click_types import ContextData
|
||||||
@@ -69,6 +69,8 @@ def search(ctx: click.Context, no_proxy: bool, profile: Optional[str] = None, pr
|
|||||||
proxy_providers.append(Basic(**config.proxy_providers["basic"]))
|
proxy_providers.append(Basic(**config.proxy_providers["basic"]))
|
||||||
if config.proxy_providers.get("nordvpn"):
|
if config.proxy_providers.get("nordvpn"):
|
||||||
proxy_providers.append(NordVPN(**config.proxy_providers["nordvpn"]))
|
proxy_providers.append(NordVPN(**config.proxy_providers["nordvpn"]))
|
||||||
|
if config.proxy_providers.get("surfsharkvpn"):
|
||||||
|
proxy_providers.append(SurfsharkVPN(**config.proxy_providers["surfsharkvpn"]))
|
||||||
if binaries.HolaProxy:
|
if binaries.HolaProxy:
|
||||||
proxy_providers.append(Hola())
|
proxy_providers.append(Hola())
|
||||||
for proxy_provider in proxy_providers:
|
for proxy_provider in proxy_providers:
|
||||||
|
|||||||
@@ -71,7 +71,6 @@ class Config:
|
|||||||
self.headers: dict = kwargs.get("headers") or {}
|
self.headers: dict = kwargs.get("headers") or {}
|
||||||
self.key_vaults: list[dict[str, Any]] = kwargs.get("key_vaults", [])
|
self.key_vaults: list[dict[str, Any]] = kwargs.get("key_vaults", [])
|
||||||
self.muxing: dict = kwargs.get("muxing") or {}
|
self.muxing: dict = kwargs.get("muxing") or {}
|
||||||
self.nordvpn: dict = kwargs.get("nordvpn") or {}
|
|
||||||
self.proxy_providers: dict = kwargs.get("proxy_providers") or {}
|
self.proxy_providers: dict = kwargs.get("proxy_providers") or {}
|
||||||
self.serve: dict = kwargs.get("serve") or {}
|
self.serve: dict = kwargs.get("serve") or {}
|
||||||
self.services: dict = kwargs.get("services") or {}
|
self.services: dict = kwargs.get("services") or {}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from .basic import Basic
|
from .basic import Basic
|
||||||
from .hola import Hola
|
from .hola import Hola
|
||||||
from .nordvpn import NordVPN
|
from .nordvpn import NordVPN
|
||||||
|
from .surfsharkvpn import SurfsharkVPN
|
||||||
|
|
||||||
__all__ = ("Basic", "Hola", "NordVPN")
|
__all__ = ("Basic", "Hola", "NordVPN", "SurfsharkVPN")
|
||||||
|
|||||||
124
unshackle/core/proxies/surfsharkvpn.py
Normal file
124
unshackle/core/proxies/surfsharkvpn.py
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import json
|
||||||
|
import re
|
||||||
|
import random
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from unshackle.core.proxies.proxy import Proxy
|
||||||
|
|
||||||
|
|
||||||
|
class SurfsharkVPN(Proxy):
|
||||||
|
def __init__(self, username: str, password: str, server_map: Optional[dict[str, int]] = None):
|
||||||
|
"""
|
||||||
|
Proxy Service using SurfsharkVPN Service Credentials.
|
||||||
|
|
||||||
|
A username and password must be provided. These are Service Credentials, not your Login Credentials.
|
||||||
|
The Service Credentials can be found here: https://my.surfshark.com/vpn/manual-setup/main/openvpn
|
||||||
|
"""
|
||||||
|
if not username:
|
||||||
|
raise ValueError("No Username was provided to the SurfsharkVPN Proxy Service.")
|
||||||
|
if not password:
|
||||||
|
raise ValueError("No Password was provided to the SurfsharkVPN Proxy Service.")
|
||||||
|
if not re.match(r"^[a-z0-9]{48}$", username + password, re.IGNORECASE) or "@" in username:
|
||||||
|
raise ValueError(
|
||||||
|
"The Username and Password must be SurfsharkVPN Service Credentials, not your Login Credentials. "
|
||||||
|
"The Service Credentials can be found here: https://my.surfshark.com/vpn/manual-setup/main/openvpn"
|
||||||
|
)
|
||||||
|
|
||||||
|
if server_map is not None and not isinstance(server_map, dict):
|
||||||
|
raise TypeError(f"Expected server_map to be a dict mapping a region to a server ID, not '{server_map!r}'.")
|
||||||
|
|
||||||
|
self.username = username
|
||||||
|
self.password = password
|
||||||
|
self.server_map = server_map or {}
|
||||||
|
|
||||||
|
self.countries = self.get_countries()
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
countries = len(set(x.get("country") for x in self.countries if x.get("country")))
|
||||||
|
servers = sum(1 for x in self.countries if x.get("connectionName"))
|
||||||
|
|
||||||
|
return f"{countries} Countr{['ies', 'y'][countries == 1]} ({servers} Server{['s', ''][servers == 1]})"
|
||||||
|
|
||||||
|
def get_proxy(self, query: str) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Get an HTTP(SSL) proxy URI for a SurfsharkVPN server.
|
||||||
|
"""
|
||||||
|
query = query.lower()
|
||||||
|
if re.match(r"^[a-z]{2}\d+$", query):
|
||||||
|
# country and surfsharkvpn server id, e.g., au-per, be-anr, us-bos
|
||||||
|
hostname = f"{query}.prod.surfshark.com"
|
||||||
|
else:
|
||||||
|
if query.isdigit():
|
||||||
|
# country id
|
||||||
|
country = self.get_country(by_id=int(query))
|
||||||
|
elif re.match(r"^[a-z]+$", query):
|
||||||
|
# country code
|
||||||
|
country = self.get_country(by_code=query)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"The query provided is unsupported and unrecognized: {query}")
|
||||||
|
if not country:
|
||||||
|
# SurfsharkVPN doesnt have servers in this region
|
||||||
|
return
|
||||||
|
|
||||||
|
server_mapping = self.server_map.get(country["countryCode"].lower())
|
||||||
|
if server_mapping:
|
||||||
|
# country was set to a specific server ID in config
|
||||||
|
hostname = f"{country['code'].lower()}{server_mapping}.prod.surfshark.com"
|
||||||
|
else:
|
||||||
|
# get the random server ID
|
||||||
|
random_server = self.get_random_server(country["countryCode"])
|
||||||
|
if not random_server:
|
||||||
|
raise ValueError(
|
||||||
|
f"The SurfsharkVPN Country {query} currently has no random servers. "
|
||||||
|
"Try again later. If the issue persists, double-check the query."
|
||||||
|
)
|
||||||
|
hostname = random_server
|
||||||
|
|
||||||
|
return f"https://{self.username}:{self.password}@{hostname}:443"
|
||||||
|
|
||||||
|
def get_country(self, by_id: Optional[int] = None, by_code: Optional[str] = None) -> Optional[dict]:
|
||||||
|
"""Search for a Country and it's metadata."""
|
||||||
|
if all(x is None for x in (by_id, by_code)):
|
||||||
|
raise ValueError("At least one search query must be made.")
|
||||||
|
|
||||||
|
for country in self.countries:
|
||||||
|
if all(
|
||||||
|
[
|
||||||
|
by_id is None or country["id"] == int(by_id),
|
||||||
|
by_code is None or country["countryCode"] == by_code.upper(),
|
||||||
|
]
|
||||||
|
):
|
||||||
|
return country
|
||||||
|
|
||||||
|
def get_random_server(self, country_id: str):
|
||||||
|
"""
|
||||||
|
Get the list of random Server for a Country.
|
||||||
|
|
||||||
|
Note: There may not always be more than one recommended server.
|
||||||
|
"""
|
||||||
|
country = [x["connectionName"] for x in self.countries if x["countryCode"].lower() == country_id.lower()]
|
||||||
|
try:
|
||||||
|
country = random.choice(country)
|
||||||
|
return country
|
||||||
|
except Exception:
|
||||||
|
raise ValueError("Could not get random countrycode from the countries list.")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_countries() -> list[dict]:
|
||||||
|
"""Get a list of available Countries and their metadata."""
|
||||||
|
res = requests.get(
|
||||||
|
url="https://api.surfshark.com/v3/server/clusters/all",
|
||||||
|
headers={
|
||||||
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if not res.ok:
|
||||||
|
raise ValueError(f"Failed to get a list of SurfsharkVPN countries [{res.status_code}]")
|
||||||
|
|
||||||
|
try:
|
||||||
|
return res.json()
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
raise ValueError("Could not decode list of SurfsharkVPN countries, not JSON data.")
|
||||||
@@ -145,13 +145,6 @@ services:
|
|||||||
# EXAMPLE:
|
# EXAMPLE:
|
||||||
# api_key: "service_specific_key"
|
# api_key: "service_specific_key"
|
||||||
|
|
||||||
# Legacy NordVPN configuration (use proxy_providers instead)
|
|
||||||
nordvpn:
|
|
||||||
username: ""
|
|
||||||
password: ""
|
|
||||||
servers:
|
|
||||||
- us: 12
|
|
||||||
|
|
||||||
# External proxy provider services
|
# External proxy provider services
|
||||||
proxy_providers:
|
proxy_providers:
|
||||||
nordvpn:
|
nordvpn:
|
||||||
@@ -159,6 +152,13 @@ proxy_providers:
|
|||||||
password: password_from_service_credentials
|
password: password_from_service_credentials
|
||||||
servers:
|
servers:
|
||||||
- us: 12 # force US server #12 for US proxies
|
- us: 12 # force US server #12 for US proxies
|
||||||
|
surfsharkvpn:
|
||||||
|
username: your_surfshark_service_username # Service credentials from https://my.surfshark.com/vpn/manual-setup/main/openvpn
|
||||||
|
password: your_surfshark_service_password # Service credentials (not your login password)
|
||||||
|
servers:
|
||||||
|
- us: 3844 # force US server #3844 for US proxies
|
||||||
|
- gb: 2697 # force GB server #2697 for GB proxies
|
||||||
|
- au: 4621 # force AU server #4621 for AU proxies
|
||||||
basic:
|
basic:
|
||||||
GB:
|
GB:
|
||||||
- "socks5://username:password@bhx.socks.ipvanish.com:1080" # 1 (Birmingham)
|
- "socks5://username:password@bhx.socks.ipvanish.com:1080" # 1 (Birmingham)
|
||||||
|
|||||||
Reference in New Issue
Block a user