update 2025-03-02 04:19:57
This commit is contained in:
parent
b0c91fec6f
commit
04615d488c
|
@ -8,8 +8,8 @@ include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
PKG_NAME:=luci-app-netspeedtest
|
PKG_NAME:=luci-app-netspeedtest
|
||||||
|
|
||||||
PKG_VERSION:=2.3.0
|
PKG_VERSION:=2.3.1
|
||||||
PKG_RELEASE:=20250104
|
PKG_RELEASE:=20250302
|
||||||
|
|
||||||
LUCI_TITLE:=LuCI Support for netspeedtest
|
LUCI_TITLE:=LuCI Support for netspeedtest
|
||||||
LUCI_DEPENDS:=+python3 +iperf3-ssl +homebox
|
LUCI_DEPENDS:=+python3 +iperf3-ssl +homebox
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2012 Matt Martz
|
# Copyright 2012 Matt Martz
|
||||||
# All Rights Reserved.
|
# All Rights Reserved.
|
||||||
|
@ -15,18 +15,18 @@
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import csv
|
import csv
|
||||||
import sys
|
import datetime
|
||||||
import math
|
|
||||||
import errno
|
import errno
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
import re
|
||||||
import signal
|
import signal
|
||||||
import socket
|
import socket
|
||||||
import timeit
|
import sys
|
||||||
import datetime
|
|
||||||
import platform
|
|
||||||
import threading
|
import threading
|
||||||
|
import timeit
|
||||||
import xml.parsers.expat
|
import xml.parsers.expat
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -36,7 +36,7 @@ except ImportError:
|
||||||
gzip = None
|
gzip = None
|
||||||
GZIP_BASE = object
|
GZIP_BASE = object
|
||||||
|
|
||||||
__version__ = '2.1.3'
|
__version__ = '2.1.4b2'
|
||||||
|
|
||||||
|
|
||||||
class FakeShutdownEvent(object):
|
class FakeShutdownEvent(object):
|
||||||
|
@ -49,6 +49,8 @@ class FakeShutdownEvent(object):
|
||||||
"Dummy method to always return false"""
|
"Dummy method to always return false"""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
is_set = isSet
|
||||||
|
|
||||||
|
|
||||||
# Some global variables we use
|
# Some global variables we use
|
||||||
DEBUG = False
|
DEBUG = False
|
||||||
|
@ -56,6 +58,7 @@ _GLOBAL_DEFAULT_TIMEOUT = object()
|
||||||
PY25PLUS = sys.version_info[:2] >= (2, 5)
|
PY25PLUS = sys.version_info[:2] >= (2, 5)
|
||||||
PY26PLUS = sys.version_info[:2] >= (2, 6)
|
PY26PLUS = sys.version_info[:2] >= (2, 6)
|
||||||
PY32PLUS = sys.version_info[:2] >= (3, 2)
|
PY32PLUS = sys.version_info[:2] >= (3, 2)
|
||||||
|
PY310PLUS = sys.version_info[:2] >= (3, 10)
|
||||||
|
|
||||||
# Begin import game to handle Python 2 and Python 3
|
# Begin import game to handle Python 2 and Python 3
|
||||||
try:
|
try:
|
||||||
|
@ -266,17 +269,6 @@ else:
|
||||||
write(arg)
|
write(arg)
|
||||||
write(end)
|
write(end)
|
||||||
|
|
||||||
if PY32PLUS:
|
|
||||||
etree_iter = ET.Element.iter
|
|
||||||
elif PY25PLUS:
|
|
||||||
etree_iter = ET_Element.getiterator
|
|
||||||
|
|
||||||
if PY26PLUS:
|
|
||||||
thread_is_alive = threading.Thread.is_alive
|
|
||||||
else:
|
|
||||||
thread_is_alive = threading.Thread.isAlive
|
|
||||||
|
|
||||||
|
|
||||||
# Exception "constants" to support Python 2 through Python 3
|
# Exception "constants" to support Python 2 through Python 3
|
||||||
try:
|
try:
|
||||||
import ssl
|
import ssl
|
||||||
|
@ -293,6 +285,23 @@ except ImportError:
|
||||||
ssl = None
|
ssl = None
|
||||||
HTTP_ERRORS = (HTTPError, URLError, socket.error, BadStatusLine)
|
HTTP_ERRORS = (HTTPError, URLError, socket.error, BadStatusLine)
|
||||||
|
|
||||||
|
if PY32PLUS:
|
||||||
|
etree_iter = ET.Element.iter
|
||||||
|
elif PY25PLUS:
|
||||||
|
etree_iter = ET_Element.getiterator
|
||||||
|
|
||||||
|
if PY26PLUS:
|
||||||
|
thread_is_alive = threading.Thread.is_alive
|
||||||
|
else:
|
||||||
|
thread_is_alive = threading.Thread.isAlive
|
||||||
|
|
||||||
|
|
||||||
|
def event_is_set(event):
|
||||||
|
try:
|
||||||
|
return event.is_set()
|
||||||
|
except AttributeError:
|
||||||
|
return event.isSet()
|
||||||
|
|
||||||
|
|
||||||
class SpeedtestException(Exception):
|
class SpeedtestException(Exception):
|
||||||
"""Base exception for this module"""
|
"""Base exception for this module"""
|
||||||
|
@ -644,23 +653,7 @@ def get_exception():
|
||||||
return sys.exc_info()[1]
|
return sys.exc_info()[1]
|
||||||
|
|
||||||
|
|
||||||
def distance(origin, destination):
|
|
||||||
"""Determine distance between 2 sets of [lat,lon] in km"""
|
|
||||||
|
|
||||||
lat1, lon1 = origin
|
|
||||||
lat2, lon2 = destination
|
|
||||||
radius = 6371 # km
|
|
||||||
|
|
||||||
dlat = math.radians(lat2 - lat1)
|
|
||||||
dlon = math.radians(lon2 - lon1)
|
|
||||||
a = (math.sin(dlat / 2) * math.sin(dlat / 2) +
|
|
||||||
math.cos(math.radians(lat1)) *
|
|
||||||
math.cos(math.radians(lat2)) * math.sin(dlon / 2) *
|
|
||||||
math.sin(dlon / 2))
|
|
||||||
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
|
|
||||||
d = radius * c
|
|
||||||
|
|
||||||
return d
|
|
||||||
|
|
||||||
|
|
||||||
def build_user_agent():
|
def build_user_agent():
|
||||||
|
@ -769,7 +762,7 @@ def print_dots(shutdown_event):
|
||||||
status
|
status
|
||||||
"""
|
"""
|
||||||
def inner(current, total, start=False, end=False):
|
def inner(current, total, start=False, end=False):
|
||||||
if shutdown_event.isSet():
|
if event_is_set(shutdown_event):
|
||||||
return
|
return
|
||||||
|
|
||||||
sys.stdout.write('.')
|
sys.stdout.write('.')
|
||||||
|
@ -808,7 +801,7 @@ class HTTPDownloader(threading.Thread):
|
||||||
try:
|
try:
|
||||||
if (timeit.default_timer() - self.starttime) <= self.timeout:
|
if (timeit.default_timer() - self.starttime) <= self.timeout:
|
||||||
f = self._opener(self.request)
|
f = self._opener(self.request)
|
||||||
while (not self._shutdown_event.isSet() and
|
while (not event_is_set(self._shutdown_event) and
|
||||||
(timeit.default_timer() - self.starttime) <=
|
(timeit.default_timer() - self.starttime) <=
|
||||||
self.timeout):
|
self.timeout):
|
||||||
self.result.append(len(f.read(10240)))
|
self.result.append(len(f.read(10240)))
|
||||||
|
@ -864,7 +857,7 @@ class HTTPUploaderData(object):
|
||||||
|
|
||||||
def read(self, n=10240):
|
def read(self, n=10240):
|
||||||
if ((timeit.default_timer() - self.start) <= self.timeout and
|
if ((timeit.default_timer() - self.start) <= self.timeout and
|
||||||
not self._shutdown_event.isSet()):
|
not event_is_set(self._shutdown_event)):
|
||||||
chunk = self.data.read(n)
|
chunk = self.data.read(n)
|
||||||
self.total.append(len(chunk))
|
self.total.append(len(chunk))
|
||||||
return chunk
|
return chunk
|
||||||
|
@ -902,7 +895,7 @@ class HTTPUploader(threading.Thread):
|
||||||
request = self.request
|
request = self.request
|
||||||
try:
|
try:
|
||||||
if ((timeit.default_timer() - self.starttime) <= self.timeout and
|
if ((timeit.default_timer() - self.starttime) <= self.timeout and
|
||||||
not self._shutdown_event.isSet()):
|
not event_is_set(self._shutdown_event)):
|
||||||
try:
|
try:
|
||||||
f = self._opener(request)
|
f = self._opener(request)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
|
@ -948,6 +941,11 @@ class SpeedtestResults(object):
|
||||||
self.client = client or {}
|
self.client = client or {}
|
||||||
|
|
||||||
self._share = None
|
self._share = None
|
||||||
|
# datetime.datetime.utcnow() is deprecated starting from 3.12
|
||||||
|
# but datetime.UTC is supported starting from 3.11
|
||||||
|
if sys.version_info.major >= 3 and sys.version_info.minor >= 11:
|
||||||
|
self.timestamp = '%sZ' % datetime.datetime.now(datetime.UTC).isoformat()
|
||||||
|
else:
|
||||||
self.timestamp = '%sZ' % datetime.datetime.utcnow().isoformat()
|
self.timestamp = '%sZ' % datetime.datetime.utcnow().isoformat()
|
||||||
self.bytes_received = 0
|
self.bytes_received = 0
|
||||||
self.bytes_sent = 0
|
self.bytes_sent = 0
|
||||||
|
@ -1074,6 +1072,25 @@ class SpeedtestResults(object):
|
||||||
return json.dumps(self.dict(), **kwargs)
|
return json.dumps(self.dict(), **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_custom_server_response(response):
|
||||||
|
"""Parse the custom server response format and return a list of servers."""
|
||||||
|
servers = []
|
||||||
|
server_blocks = re.findall(r'{(.*?)}', response, re.DOTALL)
|
||||||
|
for block in server_blocks:
|
||||||
|
server = {}
|
||||||
|
for line in block.split('\n'):
|
||||||
|
if '=' in line:
|
||||||
|
key, value = line.split('=', 1)
|
||||||
|
key = key.strip()
|
||||||
|
value = value.strip().strip('"')
|
||||||
|
if key == 'serverid':
|
||||||
|
key = 'id'
|
||||||
|
|
||||||
|
server[key] = value
|
||||||
|
server["url"] = f"http://{server['host']}/speedtest/upload.php"
|
||||||
|
servers.append(server)
|
||||||
|
return servers
|
||||||
|
|
||||||
class Speedtest(object):
|
class Speedtest(object):
|
||||||
"""Class for performing standard speedtest.net testing operations"""
|
"""Class for performing standard speedtest.net testing operations"""
|
||||||
|
|
||||||
|
@ -1096,7 +1113,7 @@ class Speedtest(object):
|
||||||
if config is not None:
|
if config is not None:
|
||||||
self.config.update(config)
|
self.config.update(config)
|
||||||
|
|
||||||
self.servers = {}
|
self.servers = []
|
||||||
self.closest = []
|
self.closest = []
|
||||||
self._best = {}
|
self._best = {}
|
||||||
|
|
||||||
|
@ -1229,8 +1246,8 @@ class Speedtest(object):
|
||||||
return self.config
|
return self.config
|
||||||
|
|
||||||
def get_servers(self, servers=None, exclude=None):
|
def get_servers(self, servers=None, exclude=None):
|
||||||
"""Retrieve a the list of speedtest.net servers, optionally filtered
|
"""Retrieve the list of speedtest.net servers from the new API URL,
|
||||||
to servers matching those specified in the ``servers`` argument
|
optionally filtered to servers matching those specified in the `servers` argument
|
||||||
"""
|
"""
|
||||||
if servers is None:
|
if servers is None:
|
||||||
servers = []
|
servers = []
|
||||||
|
@ -1249,40 +1266,31 @@ class Speedtest(object):
|
||||||
'%s is an invalid server type, must be int' % s
|
'%s is an invalid server type, must be int' % s
|
||||||
)
|
)
|
||||||
|
|
||||||
urls = [
|
url = 'https://www.speedtest.net/api/embed/vz0azjarf5enop8a/config' # New API URL
|
||||||
'://www.speedtest.net/speedtest-servers-static.php',
|
|
||||||
'http://c.speedtest.net/speedtest-servers-static.php',
|
|
||||||
'://www.speedtest.net/speedtest-servers.php',
|
|
||||||
'http://c.speedtest.net/speedtest-servers.php',
|
|
||||||
]
|
|
||||||
|
|
||||||
headers = {}
|
headers = {}
|
||||||
if gzip:
|
if gzip:
|
||||||
headers['Accept-Encoding'] = 'gzip'
|
headers['Accept-Encoding'] = 'gzip'
|
||||||
|
|
||||||
errors = []
|
errors = []
|
||||||
for url in urls:
|
|
||||||
try:
|
try:
|
||||||
request = build_request(
|
request = build_request(url, headers=headers, secure=self._secure)
|
||||||
'%s?threads=%s' % (url,
|
|
||||||
self.config['threads']['download']),
|
|
||||||
headers=headers,
|
|
||||||
secure=self._secure
|
|
||||||
)
|
|
||||||
uh, e = catch_request(request, opener=self._opener)
|
uh, e = catch_request(request, opener=self._opener)
|
||||||
|
|
||||||
if e:
|
if e:
|
||||||
errors.append('%s' % e)
|
errors.append('%s' % e)
|
||||||
raise ServersRetrievalError()
|
raise ServersRetrievalError()
|
||||||
|
|
||||||
stream = get_response_stream(uh)
|
stream = get_response_stream(uh)
|
||||||
|
|
||||||
serversxml_list = []
|
|
||||||
|
serversjson_list = []
|
||||||
while 1:
|
while 1:
|
||||||
try:
|
try:
|
||||||
serversxml_list.append(stream.read(1024))
|
serversjson_list.append(stream.read(1024))
|
||||||
except (OSError, EOFError):
|
except (OSError, EOFError):
|
||||||
raise ServersRetrievalError(get_exception())
|
raise ServersRetrievalError(get_exception())
|
||||||
if len(serversxml_list[-1]) == 0:
|
if len(serversjson_list[-1]) == 0:
|
||||||
break
|
break
|
||||||
|
|
||||||
stream.close()
|
stream.close()
|
||||||
|
@ -1291,63 +1299,34 @@ class Speedtest(object):
|
||||||
if int(uh.code) != 200:
|
if int(uh.code) != 200:
|
||||||
raise ServersRetrievalError()
|
raise ServersRetrievalError()
|
||||||
|
|
||||||
serversxml = ''.encode().join(serversxml_list)
|
serversjson = b''.join(serversjson_list)
|
||||||
|
|
||||||
|
if not serversjson:
|
||||||
|
raise SpeedtestServersError('Empty server list received')
|
||||||
|
|
||||||
|
printer('Servers JSON:\n%s' % serversjson, debug=True)
|
||||||
|
servers_response = serversjson.decode('utf-8')
|
||||||
|
|
||||||
printer('Servers XML:\n%s' % serversxml, debug=True)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
try:
|
elements = parse_custom_server_response(servers_response)
|
||||||
try:
|
except Exception as e:
|
||||||
root = ET.fromstring(serversxml)
|
|
||||||
except ET.ParseError:
|
|
||||||
e = get_exception()
|
|
||||||
raise SpeedtestServersError(
|
raise SpeedtestServersError(
|
||||||
'Malformed speedtest.net server list: %s' % e
|
'Malformed speedtest.net server list: %s' % e
|
||||||
)
|
)
|
||||||
elements = etree_iter(root, 'server')
|
|
||||||
except AttributeError:
|
|
||||||
try:
|
|
||||||
root = DOM.parseString(serversxml)
|
|
||||||
except ExpatError:
|
|
||||||
e = get_exception()
|
|
||||||
raise SpeedtestServersError(
|
|
||||||
'Malformed speedtest.net server list: %s' % e
|
|
||||||
)
|
|
||||||
elements = root.getElementsByTagName('server')
|
|
||||||
except (SyntaxError, xml.parsers.expat.ExpatError):
|
|
||||||
raise ServersRetrievalError()
|
|
||||||
|
|
||||||
for server in elements:
|
for server in elements:
|
||||||
try:
|
if servers and int(server.get('id')) not in servers:
|
||||||
attrib = server.attrib
|
|
||||||
except AttributeError:
|
|
||||||
attrib = dict(list(server.attributes.items()))
|
|
||||||
|
|
||||||
if servers and int(attrib.get('id')) not in servers:
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if (int(attrib.get('id')) in self.config['ignore_servers']
|
if (int(server.get('id')) in self.config['ignore_servers']
|
||||||
or int(attrib.get('id')) in exclude):
|
or int(server.get('id')) in exclude):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
self.servers.append(server)
|
||||||
d = distance(self.lat_lon,
|
|
||||||
(float(attrib.get('lat')),
|
|
||||||
float(attrib.get('lon'))))
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
|
|
||||||
attrib['d'] = d
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.servers[d].append(attrib)
|
|
||||||
except KeyError:
|
|
||||||
self.servers[d] = [attrib]
|
|
||||||
|
|
||||||
break
|
|
||||||
|
|
||||||
except ServersRetrievalError:
|
except ServersRetrievalError:
|
||||||
continue
|
raise
|
||||||
|
|
||||||
if (servers or exclude) and not self.servers:
|
if (servers or exclude) and not self.servers:
|
||||||
raise NoMatchedServers()
|
raise NoMatchedServers()
|
||||||
|
@ -1416,14 +1395,7 @@ class Speedtest(object):
|
||||||
if not self.servers:
|
if not self.servers:
|
||||||
self.get_servers()
|
self.get_servers()
|
||||||
|
|
||||||
for d in sorted(self.servers.keys()):
|
self.closest = self.servers[:limit]
|
||||||
for s in self.servers[d]:
|
|
||||||
self.closest.append(s)
|
|
||||||
if len(self.closest) == limit:
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
continue
|
|
||||||
break
|
|
||||||
|
|
||||||
printer('Closest Servers:\n%r' % self.closest, debug=True)
|
printer('Closest Servers:\n%r' % self.closest, debug=True)
|
||||||
return self.closest
|
return self.closest
|
||||||
|
@ -1889,10 +1861,9 @@ def shell():
|
||||||
printer('Cannot retrieve speedtest server list', error=True)
|
printer('Cannot retrieve speedtest server list', error=True)
|
||||||
raise SpeedtestCLIError(get_exception())
|
raise SpeedtestCLIError(get_exception())
|
||||||
|
|
||||||
for _, servers in sorted(speedtest.servers.items()):
|
for server in speedtest.servers:
|
||||||
for server in servers:
|
|
||||||
line = ('%(id)5s) %(sponsor)s (%(name)s, %(country)s) '
|
line = ('%(id)5s) %(sponsor)s (%(name)s, %(country)s)' % server)
|
||||||
'[%(d)0.2f km]' % server)
|
|
||||||
try:
|
try:
|
||||||
printer(line)
|
printer(line)
|
||||||
except IOError:
|
except IOError:
|
||||||
|
@ -1932,7 +1903,7 @@ def shell():
|
||||||
|
|
||||||
results = speedtest.results
|
results = speedtest.results
|
||||||
|
|
||||||
printer('Hosted by %(sponsor)s (%(name)s) [%(d)0.2f km]: '
|
printer('Hosted by %(sponsor)s (%(name)s): '
|
||||||
'%(latency)s ms' % results.server, quiet)
|
'%(latency)s ms' % results.server, quiet)
|
||||||
|
|
||||||
if args.download:
|
if args.download:
|
||||||
|
|
Loading…
Reference in New Issue