Skip to content

Commit

Permalink
Merge pull request #5 from trishmapow/v1-api
Browse files Browse the repository at this point in the history
v2 new API, more accurate, show Mbps
  • Loading branch information
trishmapow authored Aug 31, 2019
2 parents 5e8a637 + 29a552f commit 857a186
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 50 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
FROM python:slim-buster

RUN pip install requests reverse_geocode tabulate
RUN pip install requests tabulate

WORKDIR /home/

Expand Down
39 changes: 17 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,39 +1,34 @@
# nordvpn-tools

## nordvpn_best.py
## nordvpn_best.py v2
Outputs a table of servers in the specified city and country that have a load % below MAX_LOAD (default 30).

Requirements: `pip3 install requests reverse_geocode tabulate`. Uses the reverse_geocode library to (very roughly) convert coordinates to cities (offline), try larger cities if not working.
Requirements: `pip3 install requests tabulate`.

v2 uses new NordVPN endpoint /v1/servers which has much more server info, reverse_geocode no longer needed.

```
usage: nordvpn_best.py [-h] [--city CITY] [--country COUNTRY] [--load LOAD]
usage: nordvpn_best.py [-h] [--load LOAD] [--debug] LOC
Shows low load NordVPN servers in a given city and/or country.
positional arguments:
LOC 'city, country_code' or 'country_code' e.g. US, GB
optional arguments:
-h, --help show this help message and exit
--city CITY find servers in '[city] [country]'
--country COUNTRY find servers in [country] (full name)
--load LOAD set maximum load (1-99)
-h, --help show this help message and exit
--load LOAD set maximum load (1-99)
--debug
```

Sample output:
```
$ python3 nordvpn_best.py --city 'Sydney, Australia' --load 20 13:07:23
Name IP Load % Categories
-------------- --------------- -------- -------------------------
Australia #197 104.222.131.42 19 Standard VPN servers, P2P
Australia #200 104.222.131.45 17 Standard VPN servers, P2P
Australia #205 45.121.210.197 16 Standard VPN servers, P2P
Australia #213 43.245.163.164 17 Standard VPN servers, P2P
Australia #295 144.48.36.35 19 Standard VPN servers, P2P
Australia #368 69.161.194.117 17 Standard VPN servers, P2P
Australia #390 103.212.227.149 19 Standard VPN servers, P2P
Australia #392 103.212.227.117 14 Standard VPN servers, P2P
Australia #423 103.212.227.155 17 Standard VPN servers, P2P
68 servers online in Sydney, Australia (approximate)
9 of which have <20% load
$ python3 nordvpn_best.py 'Sydney, AU' --load 20
Name Load % Mbps IP Groups
-------------- -------- ------ -------------- -------------------------
Australia #200 19 150 104.222.131.45 Standard VPN servers, P2P
Australia #202 13 150 104.222.131.47 Standard VPN servers, P2P
2 servers online in Sydney AU with <20% load
```

### Run With Docker
Expand Down
74 changes: 47 additions & 27 deletions nordvpn_best.py
Original file line number Diff line number Diff line change
@@ -1,46 +1,66 @@
'''
NordVPN Server Finder
github/trishmapow, 2019
v2.0 using new Nord API
'''

import requests
import json
import reverse_geocode
from tabulate import tabulate
import sys
import argparse

NORD_API_BASE = "https://api.nordvpn.com"
MAX_LOAD = 30
NORD_API_BASE = "https://api.nordvpn.com/v1"
MAX_LOAD_DEFAULT = 30
VERBOSE = False

def get_servers(country_code: str, city: str = None, max_load: int = MAX_LOAD_DEFAULT):
# trial and error, undocumented API
fields = ["fields[servers.name]", "fields[servers.locations.country.code]", "fields[servers.locations.country.city.name]"]
fields.extend(["fields[station]", "fields[load]", "fields[specifications]", "fields[servers.groups.title]"])

def get_servers(country: str, city: str = None):
servers = requests.request("GET", NORD_API_BASE + "/server")
filtered = [srv for srv in servers.json() if srv['country'].lower() == country.lower()]
# perhaps there's a country filter? would reduce network usage
load_filter = f"filters[servers.load][$lt]={max_load}"
url = NORD_API_BASE + "/servers?limit=16384&" + '&'.join(fields) + f"&{load_filter}"
if (VERBOSE):
print(url)

servers = requests.request("GET", url)
filtered = [srv for srv in servers.json() if srv['locations'][0]['country']['code'].lower() == country_code.lower()]
if city is None:
return filtered
else:
return list(filter(lambda srv: reverse_geocode.search([(float(srv["location"]["lat"]), float(srv["location"]["long"]))])[0]["city"].lower() == city.lower(), filtered))
return [srv for srv in filtered if srv['locations'][0]['country']['city']['name'].lower() == city.lower()]

if __name__ == '__main__':
parser = argparse.ArgumentParser(description="Shows low load NordVPN servers in a given city and/or country.")
parser.add_argument('--city', help="find servers in '[city], [country]'")
parser.add_argument('--country', help="find servers in '[country]' (full name)")
parser.add_argument('location', metavar='LOC', type=str, help="'city, country_code' or 'country_code' e.g. US, GB")
parser.add_argument('--load', type=int, help="set maximum load (1-99)")
parser.add_argument('--debug', action='store_true')
args = parser.parse_args()

if not (args.city or args.country):
parser.error("No action requested, choose --city or --country")
elif args.city:
loc = args.city.strip().split(",")
if len(loc) != 2:
parser.error("Expected '[city], [country]'")
city = loc[0].strip()
country = loc[1].strip()
else:
if args.debug:
VERBOSE = True

loc = list(map(lambda x: x.strip(), args.location.split(',')))
if len(loc) == 2:
city = loc[0]
country = loc[1]
elif len(loc) == 1:
city = None
country = args.country.strip()
country = loc[0]
else:
parser.error("'city, country_code' or 'country_code' expected")

if VERBOSE:
print(f"city={city} country={country}")

if args.load is not None and args.load >= 1 and args.load <= 99:
MAX_LOAD = args.load
servers = get_servers(country, city, args.load)
else:
servers = get_servers(country, city)

servers = get_servers(country, city)
headers = ["Name", "Load %", "Mbps", "IP", "Groups"]
x = [[s['name'], s['load'], [x['values'][0]['value'] for x in s['specifications'] if x['identifier'] == 'network_mbps'][0], s['station'], (', '.join([g['title'] for g in s['groups']][:-1]))] for s in servers]

x = [[s['name'], s['ip_address'], s['load'], (', '.join([cat['name'] for cat in s['categories']]))] for s in servers if int(s['load']) < MAX_LOAD]
print(tabulate(x, headers=["Name", "IP", "Load %", "Categories"]))
print(f"{len(servers)} servers online in {city} {country} (approximate)")
print(f"{len(x)} of which have <{MAX_LOAD}% load")
print(tabulate(x, headers=headers))
print(f"{len(servers)} servers online in {city or ''} {country} with <{args.load or MAX_LOAD_DEFAULT}% load")

0 comments on commit 857a186

Please sign in to comment.