🎁 Sync 2026-03-15 08:51:25

This commit is contained in:
github-actions[bot] committed 2026-03-15 08:51:25 +08:00
1 parent 2d75925beb
commit a485533d81
10 files changed
+597 -354

No files matched your search

+34 -29
View File
@@ -1,41 +1,44 @@
# SPDX-License-Identifier: GPL-3.0-only # SPDX-License-Identifier: GPL-3.0-only
# Copyright (c) 2020-2024 sirpdboy herboy2008@gmail.com # Copyright (c) 2022-2026 sirpdboy herboy2008@gmail.com
# #
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=homebox PKG_NAME:=homebox
PKG_VERSION:=0.0.0.2024101306 PKG_VERSION:=1.0.1
PKG_REAL_VER:=0.0.0-dev.2024101306 PKG_RELEASE:=4
PKG_RELEASE:=3
ifeq ($(ARCH),aarch64)
H_ARCH:=arm64
else ifeq ($(ARCH),arm)
H_ARCH:=arm
ifneq ($(filter aarch64%,$(ARCH)),)
PKG_ARCH:=arm64
PKG_HASH:=skip
else ifneq ($(filter arm%,$(ARCH)),)
PKG_ARCH:=armv7
PKG_HASH:=skip
else ifeq ($(ARCH),i386) else ifeq ($(ARCH),i386)
H_ARCH:=386 PKG_ARCH:=386
PKG_HASH:=skip
else ifeq ($(ARCH),mips)
H_ARCH:=mips
else ifeq ($(ARCH),mipsel)
H_ARCH:=mips
else ifeq ($(ARCH),x86_64) else ifeq ($(ARCH),x86_64)
H_ARCH:=amd64 PKG_ARCH:=amd64
PKG_HASH:=skip
else
$(error Unsupported architecture: $(ARCH))
endif endif
PKG_SOURCE:=homebox-linux-$(PKG_ARCH)-musl-v$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://github.com/sirpdboy/homebox/releases/download/v$(PKG_VERSION)/
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION)
include $(INCLUDE_DIR)/package.mk include $(INCLUDE_DIR)/package.mk
define Package/$(PKG_NAME) define Package/$(PKG_NAME)
SECTION:=net SECTION:=net
CATEGORY:=Network CATEGORY:=Network
TITLE:=A Toolbox for Home Local Networks Speed Test SUBMENU:=Speed Test
URL:=https://github.com/XGHeaven/homebox TITLE:=Home Local Network Speed Test Tool
DEPENDS:=@(i386||x86_64||arm||aarch64||mipsel||mips) +libstdcpp URL:=https://github.com/XGHeaven/homebox
DEPENDS:=@(i386||x86_64||arm||aarch64)
MAINTAINER:=sirpdboy <herboy2008@gmail.com>
endef endef
define Package/$(PKG_NAME)/description define Package/$(PKG_NAME)/description
@@ -43,9 +46,11 @@ define Package/$(PKG_NAME)/description
endef endef
define Build/Prepare define Build/Prepare
mkdir -p $(PKG_BUILD_DIR) ( \
[ ! -f $(PKG_BUILD_DIR)/server-linux-$(H_ARCH).tar.gz ] && wget https://github.com/XGHeaven/homebox/releases/download/v$(PKG_REAL_VER)/server-linux-$(H_ARCH).tar.gz -O $(PKG_BUILD_DIR)/server-linux-$(H_ARCH).tar.gz pushd $(PKG_BUILD_DIR) ; \
tar -xzvf $(PKG_BUILD_DIR)/server-linux-$(H_ARCH).tar.gz -C $(PKG_BUILD_DIR) $(TAR) -zxf $(DL_DIR)/$(PKG_SOURCE) -C . ; \
popd ; \
)
endef endef
define Build/Compile define Build/Compile
@@ -53,7 +58,7 @@ endef
define Package/$(PKG_NAME)/install define Package/$(PKG_NAME)/install
$(INSTALL_DIR) $(1)/usr/bin $(INSTALL_DIR) $(1)/usr/bin
$(INSTALL_BIN) $(PKG_BUILD_DIR)/server-linux-$(H_ARCH) $(1)/usr/bin/homebox $(INSTALL_BIN) $(PKG_BUILD_DIR)/$(PKG_NAME) $(1)/usr/bin/homebox
endef endef
$(eval $(call BuildPackage,$(PKG_NAME))) $(eval $(call BuildPackage,$(PKG_NAME)))
+2 -2
View File
@@ -9,8 +9,8 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-netspeedtest PKG_NAME:=luci-app-netspeedtest
PKG_VERSION:=5.1.3 PKG_VERSION:=5.2.0
PKG_RELEASE:=2 PKG_RELEASE:=3
LUCI_TITLE:=LuCI Support for netspeedtest LUCI_TITLE:=LuCI Support for netspeedtest
LUCI_DEPENDS:=+ookla-speedtest +homebox +python3-light +python3-pkg-resources +python3-xml +python3-email \ LUCI_DEPENDS:=+ookla-speedtest +homebox +python3-light +python3-pkg-resources +python3-xml +python3-email \
@@ -1,4 +1,4 @@
/* Copyright (C) 2021-2026 sirpdboy herboy2008@gmail.com https://github.com/sirpdboy/luci-app-netspeedtest */ /* Copyright (C) 2021-2026 sirpdboy herboy2008@gmail.com */
'use strict'; 'use strict';
'require view'; 'require view';
'require fs'; 'require fs';
@@ -6,36 +6,142 @@
'require uci'; 'require uci';
'require form'; 'require form';
'require poll'; 'require poll';
var state = {
running: false,
port: 3300,
enabled: false,
operationInProgress: false,
operationType: null // 'start' 或 'stop'
};
const logPath = '/tmp/netspeedtest.log';
function checkProcess(quick = false) {
if (quick) {
return fs.exec('/usr/bin/pgrep', ['homebox'])
.then(function(res) {
return res.code === 0 && res.stdout.trim() !== '';
})
.catch(function() {
return false;
});
} else {
return fs.exec('/usr/bin/pgrep', ['homebox'])
.then(function(res) {
if (res.code === 0 && res.stdout.trim()) {
return {
running: true,
pid: res.stdout.trim()
};
}
return fs.exec('/bin/ps', ['-w', '-C', 'homebox', '-o', 'pid='])
.then(function(psRes) {
var pid = psRes.stdout.trim();
return {
running: pid !== '',
pid: pid || null
};
});
})
.catch(function(err) {
return { running: false, pid: null };
});
}
}
function controlService(action, port) {
if (action === 'start') {
return fs.exec('/usr/bin/killall', ['homebox'])
.catch(function() { return Promise.resolve(); })
.then(function() {
var command = 'nohup /usr/bin/homebox serve --port ' + port + ' > ' + logPath + ' 2>&1 &';
return fs.exec('/bin/sh', ['-c', command]);
});
} else {
return fs.exec('/etc/init.d/netspeedtest', ['stop']);
}
}
function saveConfiguration(newPort, enabled) {
const uciContent = `config netspeedtest 'config'
\toption homebox_port '${newPort}'
\toption homebox_enabled '${enabled ? '1' : '0'}'
`;
return fs.write('/etc/config/netspeedtest', uciContent)
.then(() => {
if (enabled) {
return fs.exec('/etc/init.d/netspeedtest', ['enable']);
} else {
return fs.exec('/etc/init.d/netspeedtest', ['disable']);
}
})
.then(() => {
return uci.load('netspeedtest');
});
}
return view.extend({ return view.extend({
handleSaveApply: null,
handleSave: null,
handleReset: null,
load: function() {
return Promise.all([
uci.load('netspeedtest')
]).then(() => {
var port = uci.get('netspeedtest', 'config', 'homebox_port');
if (port) {
state.port = parseInt(port);
}
var enabled = uci.get('netspeedtest', 'config', 'homebox_enabled');
state.enabled = enabled === '1';
});
},
render: function() { render: function() {
var state = {
running: false,
port: 3300
};
var container = E('div'); var container = E('div');
var statusSection = E('div', { 'class': 'cbi-section' }); var statusSection = E('div', { 'class': 'cbi-section' });
var statusIcon = E('span', { 'style': 'margin-right: 5px;' }); var statusIcon = E('span', { 'style': 'margin-right: 5px;' });
var statusText = E('span'); var statusText = E('span');
var toggleBtn = E('button', { 'class': 'btn cbi-button' }); var toggleBtn = E('button', { 'class': 'btn cbi-button' });
var statusMessage = E('div', { style: 'text-align: center; padding: 2em;' }, [ var saveBtn = E('button', {
E('img', { 'class': 'btn cbi-button cbi-button-apply',
src: 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMjQiIGhlaWdodD0iMTAyNCIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCI+PHBhdGggZmlsbD0iI2RmMDAwMCIgZD0iTTk0Mi40MjEgMjM0LjYyNGw4MC44MTEtODAuODExLTE1My4wNDUtMTUzLjA0NS04MC44MTEgODAuODExYy03OS45NTctNTEuNjI3LTE3NS4xNDctODEuNTc5LTI3Ny4zNzYtODEuNTc5LTI4Mi43NTIgMC01MTIgMjI5LjI0OC01MTIgNTEyIDAgMTAyLjIyOSAyOS45NTIgMTk3LjQxOSA4MS41NzkgMjc3LjM3NmwtODAuODExIDgwLjgxMSAxNTMuMDQ1IDE1My4wNDUgODAuODExLTgwLjgxMWM3OS45NTcgNTEuNjI3IDE3NS4xNDcgODEuNTc5IDI3Ny4zNzYgODEuNTc5IDI4Mi43NTIgMCA1MTItMjI5LjI0OCA1MTItNTEyIDAtMTAyLjIyOS0yOS45NTItMTk3LjQxOS04MS41NzktMjc3LjM3NnpNMTk0Ljk0NCA1MTJjMC0xNzUuMTA0IDE0MS45NTItMzE3LjA1NiAzMTcuMDU2LTMxNy4wNTYgNDggMCA5My40ODMgMTAuNjY3IDEzNC4yMjkgMjkuNzgxbC00MjEuNTQ3IDQyMS41NDdjLTE5LjA3Mi00MC43ODktMjkuNzM5LTg2LjI3Mi0yOS43MzktMTM0LjI3MnpNNTEyIDgyOS4wNTZjLTQ4IDAtOTMuNDgzLTEwLjY2Ny0xMzQuMjI5LTI5Ljc4MWw0MjEuNTQ3LTQyMS41NDdjMTkuMDcyIDQwLjc4OSAyOS43ODEgODYuMjcyIDI5Ljc4MSAxMzQuMjI5LTAuMDQzIDE3NS4xNDctMTQxLjk5NSAzMTcuMDk5LTMxNy4wOTkgMzE3LjA5OXoiLz48L3N2Zz4=', 'style': 'margin-left: 10px;'
style: 'width: 100px; height: 100px; margin-bottom: 1em;' }, _('Save'));
}),
E('h2', {}, _('Homebox Service Not Running')),
E('p', {}, _('Please enable the Homebox service'))
]);
var enableCheckbox = E('input', {
'type': 'checkbox',
'id': 'homebox_enable',
'class': 'cbi-input-checkbox'
});
var statusMessage = E('div', { style: 'text-align: center; padding: 2em;' }, [
E('div', { style: 'font-size: 5em; color: #f39c12; margin-bottom: 0.2em;' }, '⚠️'),
E('h2', {}, _('Homebox Service Not Running')),
E('p', { style: 'color: #666; margin-top: 1em;' }, _('Please start the Homebox service'))
]);
var isHttps = window.location.protocol === 'https:'; var isHttps = window.location.protocol === 'https:';
var iframe; var iframe;
if (!isHttps) { var portInput = E('input', {
iframe = E('iframe', { 'type': 'number',
src: window.location.origin + ':' + state.port, 'class': 'cbi-input-text',
style: 'border:none;width: 100%; min-height: 80vh; border: none; border-radius: 3px;overflow:hidden !important;' 'style': 'width: 100px;',
}); 'value': state.port,
} 'min': 1024,
'max': 65535,
'placeholder': '3300'
});
var portError = E('span', {
'class': 'error',
'style': 'color: red; margin-left: 10px; display: none;'
}, _('Port range must be 1024-65535'));
function createHttpsButton() { function createHttpsButton() {
return E('div', { return E('div', {
@@ -49,57 +155,57 @@ return view.extend({
class: 'cbi-button cbi-button-apply', class: 'cbi-button cbi-button-apply',
style: 'display: inline-block; margin-top: 1em; padding: 10px 20px; font-size: 16px; text-decoration: none; color: white;' style: 'display: inline-block; margin-top: 1em; padding: 10px 20px; font-size: 16px; text-decoration: none; color: white;'
}, _('Open Web Interface')) }, _('Open Web Interface'))
]); ]);
} }
async function checkProcess() {
try {
// 尝试使用pgrep
const res = await fs.exec('/usr/bin/pgrep', ['homebox']);
return {
running: res.code === 0,
pid: res.stdout.trim() || null
};
} catch (err) {
// 回退到ps方法
try {
const psRes = await fs.exec('/bin/ps', ['-w', '-C', 'homebox', '-o', 'pid=']);
const pid = psRes.stdout.trim();
return {
running: pid !== '',
pid: pid || null
};
} catch (err) {
return { running: false, pid: null };
}
}
}
function controlService(action) {
var command = action === 'start'
? 'nohup /usr/bin/homebox >> /tmp/netspeedtest.log 2>&1 &'
: '/usr/bin/killall homebox';
return fs.exec('/bin/sh', ['-c', command]);
}
function updateStatus() { function updateStatus() {
statusIcon.textContent = state.running ? '✓' : '✗'; statusIcon.textContent = state.running ? '✓' : '✗';
statusIcon.style.color = state.running ? 'green' : 'red'; statusIcon.style.color = state.running ? 'green' : 'red';
statusText.textContent = _('Homebox Server') + (state.running ? _('RUNNING') : _('NOT RUNNING')); statusText.textContent = _('Homebox Server') + ' ' + (state.running ? _('RUNNING') : _('NOT RUNNING'));
statusText.style.color = state.running ? 'green' : 'red'; statusText.style.color = state.running ? 'green' : 'red';
statusText.style.fontWeight = 'bold'; statusText.style.fontWeight = 'bold';
statusText.style.fontSize = '0.92rem'; statusText.style.fontSize = '0.92rem';
toggleBtn.textContent = state.running ? _('Stop Server') : _('Start Server'); if (state.operationInProgress) {
toggleBtn.className = `btn cbi-button cbi-button-${state.running ? 'reset' : 'apply'}`; if (state.operationType === 'start') {
toggleBtn.textContent = _('Starting...');
} else if (state.operationType === 'stop') {
toggleBtn.textContent = _('Stopping...');
}
toggleBtn.disabled = true;
if (state.operationType === 'stop' && !state.running) {
console.log('Stop confirmed - operation complete');
state.operationInProgress = false;
state.operationType = null;
toggleBtn.textContent = state.running ? _('Stop Server') : _('Start Server');
toggleBtn.className = 'btn cbi-button cbi-button-' + (state.running ? 'reset' : 'apply');
toggleBtn.disabled = false;
} else if (state.operationType === 'start' && state.running) {
console.log('Start confirmed - operation complete');
state.operationInProgress = false;
state.operationType = null;
toggleBtn.textContent = state.running ? _('Stop Server') : _('Start Server');
toggleBtn.className = 'btn cbi-button cbi-button-' + (state.running ? 'reset' : 'apply');
toggleBtn.disabled = false;
}
} else {
toggleBtn.textContent = state.running ? _('Stop Server') : _('Start Server');
toggleBtn.className = 'btn cbi-button cbi-button-' + (state.running ? 'reset' : 'apply');
toggleBtn.disabled = false;
}
enableCheckbox.checked = state.enabled;
portInput.value = state.port;
// Update container content based on state and protocol container.innerHTML = '';
container.textContent = '';
if (state.running) { if (state.running) {
if (isHttps) { if (isHttps) {
container.appendChild(createHttpsButton()); container.appendChild(createHttpsButton());
} else { } else {
iframe = E('iframe', {
src: 'http://' + window.location.hostname + ':' + state.port,
style: 'border:none;width: 100%; min-height: 80vh; border: none; border-radius: 3px;overflow:hidden !important;'
});
container.appendChild(iframe); container.appendChild(iframe);
} }
} else { } else {
@@ -107,59 +213,185 @@ return view.extend({
} }
} }
toggleBtn.addEventListener('click', ui.createHandlerFn(this, function() { toggleBtn.addEventListener('click', function(ev) {
ev.preventDefault();
if (toggleBtn.disabled || state.operationInProgress) return;
var action = state.running ? 'stop' : 'start'; var action = state.running ? 'stop' : 'start';
return controlService(action) var startTime = Date.now();
.then(checkProcess)
.then(res => { state.operationInProgress = true;
state.running = res.running; state.operationType = action;
updateStatus();
controlService(action, state.port)
.then(function() {
return new Promise(function(resolve, reject) {
var checkCount = 0;
var maxChecks = 30;
function doCheck() {
checkProcess(true).then(function(isRunning) {
if (action === 'stop' && !isRunning) {
console.log('Stop success after', Date.now() - startTime, 'ms');
resolve({ running: false });
} else if (action === 'start' && isRunning) {
console.log('Start success after', Date.now() - startTime, 'ms');
checkProcess().then(resolve).catch(resolve);
} else if (checkCount < maxChecks) {
checkCount++;
setTimeout(doCheck, 200);
} else {
console.log('Check timeout, using full check');
checkProcess().then(resolve).catch(resolve);
}
}).catch(function() {
if (checkCount < maxChecks) {
checkCount++;
setTimeout(doCheck, 100);
} else {
checkProcess().then(resolve).catch(resolve);
}
});
}
doCheck();
});
})
.then(function(res) {
if (res) {
if (typeof res === 'boolean') {
state.running = res;
} else {
state.running = res.running || false;
if (res.port) {
state.port = res.port;
}
}
}
state.operationInProgress = false;
state.operationType = null;
updateStatus(); updateStatus();
var message = action === 'start' ?
_('Homebox server started on port %s').replace('%s', state.port) :
_('Homebox server stopped');
})
.catch(function(err) {
console.error('Service control error:', err);
// 发生错误时重新检查
checkProcess().then(function(res) {
state.running = res.running || false;
if (res.port) state.port = res.port;
state.operationInProgress = false;
state.operationType = null;
updateStatus();
}).catch(function() {
state.operationInProgress = false;
state.operationType = null;
updateStatus();
});
}); });
})); });
saveBtn.addEventListener('click', function(ev) {
ev.preventDefault();
var newPort = parseInt(portInput.value, 10);
if (isNaN(newPort) || newPort < 1024 || newPort > 65535) {
portError.style.display = 'inline';
return;
}
portError.style.display = 'none';
saveBtn.disabled = true;
saveBtn.textContent = _('Saving...');
saveConfiguration(newPort, enableCheckbox.checked)
.then(function() {
state.port = newPort;
state.enabled = enableCheckbox.checked;
updateStatus();
})
.catch(function(err) {
})
.finally(function() {
saveBtn.disabled = false;
saveBtn.textContent = _('Save');
});
});
enableCheckbox.addEventListener('change', function(ev) {
saveConfiguration(state.port, enableCheckbox.checked)
.then(function() {
state.enabled = enableCheckbox.checked;
updateStatus();
var message = enableCheckbox.checked ?
_('Auto start enabled') :
_('Auto start disabled');
})
.catch(function(err) {
enableCheckbox.checked = state.enabled;
});
});
statusSection.appendChild(E('div', { 'style': 'margin: 15px' }, [ statusSection.appendChild(E('div', { 'style': 'margin: 15px' }, [
E('h3', {}, _('Throughput speedtest Homebox')), E('h3', {}, _('Throughput speedtest Homebox')),
E('div', { 'class': 'cbi-map-descr' }, [statusIcon, statusText]), E('div', { 'class': 'cbi-map-descr' }, [statusIcon, statusText]),
E('div', {'class': 'cbi-value', 'style': 'margin-top: 20px'}, [
E('div', {'class': 'cbi-value-title'}, _('Homebox service control')), E('div', {'class': 'cbi-value'}, [
E('div', {'class': 'cbi-value-field'}, toggleBtn), E('div', {'class': 'cbi-value-title'}, _('Auto Start')),
E('div', { 'style': 'text-align: right; font-style: italic; margin-top: 20px;' }, [ E('div', {'class': 'cbi-value-field'}, [
_('© github '), enableCheckbox,
E('a', { E('label', {'for': 'homebox_enable', 'style': 'margin-left: 5px;'})
'href': 'https://github.com/sirpdboy',
'target': '_blank',
'style': 'text-decoration: none;'
}, 'by sirpdboy')
]) ])
]),
E('div', {'class': 'cbi-value' }, [
E('div', {'class': 'cbi-value-title'}, _('Port Setting')),
E('div', {'class': 'cbi-value-field', 'style': 'align-items: center;'}, [
portInput,
saveBtn,
portError
])
]),
E('div', {'class': 'cbi-value'}, [
E('div', {'class': 'cbi-value-title'}, _('Service Control')),
E('div', {'class': 'cbi-value-field'}, toggleBtn)
]) ])
])); ]));
// Initial status check checkProcess().then(function(res) {
checkProcess().then(res => {
state.running = res.running; state.running = res.running;
if (res.port) {
state.port = res.port;
}
updateStatus(); updateStatus();
toggleBtn.disabled = false;
// Start polling poll.add(function() {
poll.add(() => { return checkProcess().then(function(res) {
return checkProcess().then(res => { if (res.running !== state.running || (res.port && res.port !== state.port)) {
if (res.running !== state.running) {
state.running = res.running; state.running = res.running;
if (res.port) {
state.port = res.port;
}
updateStatus(); updateStatus();
toggleBtn.disabled = false;
} }
}); });
}, 5); }, 5);
poll.start();
}); });
return E('div', {}, [ return E('div', {}, [
statusSection, statusSection,
container container
]); ]);
}, }
handleSaveApply: null,
handleSave: null,
handleReset: null
}); });
@@ -9,7 +9,7 @@
var state = { var state = {
running: false, running: false,
port: null port: 5201
}; };
const logPath = '/tmp/netspeedtest.log'; const logPath = '/tmp/netspeedtest.log';
@@ -34,10 +34,9 @@ async function checkProcess() {
} }
} }
function controlService(action, port) {
function controlService(action) {
const commands = { const commands = {
start: `/usr/bin/iperf3 -s -D -p 5201 --logfile ${logPath} 2>&1`, start: `/usr/bin/iperf3 -s -D -p ${port} --logfile ${logPath} 2>&1`,
stop: '/usr/bin/killall -q iperf3' stop: '/usr/bin/killall -q iperf3'
}; };
@@ -50,45 +49,105 @@ function controlService(action) {
throw err; throw err;
}); });
} }
function saveConfiguration(newPort, enabled) {
const uciContent = `config netspeedtest 'config'
\toption iperf3port '${newPort}'
\toption iperf3_enabled '${enabled ? '1' : '0'}'
`;
return fs.write('/etc/config/netspeedtest', uciContent)
.then(() => {
// 更新开机自启
if (enabled) {
return fs.exec('/etc/init.d/netspeedtest', ['enable']);
} else {
return fs.exec('/etc/init.d/netspeedtest', ['disable']);
}
});
}
return view.extend({ return view.extend({
handleSaveApply: null, handleSaveApply: null,
handleSave: null, handleSave: null,
handleReset: null, handleReset: null,
load: function() { load: function() {
return Promise.all([ return Promise.all([
uci.load('netspeedtest') uci.load('netspeedtest')
]); ]).then(() => {
const port = uci.get('netspeedtest', 'config', 'iperf3port');
if (port) {
state.port = parseInt(port);
}
});
}, },
render: function() { render: function() {
// 创建状态元素
const statusIcon = E('span', { 'style': 'margin-right: 5px;' }); const statusIcon = E('span', { 'style': 'margin-right: 5px;' });
const btnGroup = E('div', { 'class': 'cbi-value-field', 'style': 'display: flex; gap: 10px;' });
const statusText = E('span'); const statusText = E('span');
const portInput = E('input', {
'type': 'number',
'class': 'cbi-input-text',
'style': 'width: 100px;',
'value': state.port,
'min': 1024,
'max': 65535,
'placeholder': '5201'
});
const enableCheckbox = E('input', {
'type': 'checkbox',
'id': 'iperf3_enable',
'class': 'cbi-input-checkbox',
'checked': state.enabled
});
const portError = E('span', {
'class': 'error',
'style': 'color: red; margin-left: 10px; display: none;'
}, _('Port range must be 1024-65535'));
const statusContainer = E('div', { 'class': 'cbi-map-descr' }, [
statusIcon,
statusText
]);
const toggleBtn = E('button', { const toggleBtn = E('button', {
'class': 'btn cbi-button', 'class': 'btn cbi-button',
'click': ui.createHandlerFn(this, function() { 'click': ui.createHandlerFn(this, function() {
const action = state.running ? 'stop' : 'start'; const action = state.running ? 'stop' : 'start';
toggleBtn.disabled = true; // 禁用按钮 toggleBtn.disabled = true;
return controlService(action) controlService(action, state.port)
.then(() => checkProcess()) .then(() => {
.then(res => { return new Promise(resolve => setTimeout(resolve, 500));
state.running = res.running; })
updateStatus(); .then(() => checkProcess())
toggleBtn.disabled = false; // 恢复按钮 .then(res => {
}) state.running = res.running;
.catch(err => { updateStatus();
ui.addNotification(null, E('p', _('Error: ') + err.message), 'error'); toggleBtn.disabled = false;
toggleBtn.disabled = false; // 出错时也要恢复按钮 })
}); .catch(err => {
ui.addNotification(null, E('p', _('Error: ') + err.message), 'error');
toggleBtn.disabled = false;
});
}) })
}); });
const savePortBtn = E('button', {
'class': 'btn cbi-button cbi-button-apply',
'style': 'margin-left: 10px;',
'click': ui.createHandlerFn(this, function() {
const newPort = parseInt(portInput.value);
if (isNaN(newPort) || newPort < 1024 || newPort > 65535) {
portError.style.display = 'inline';
return;
}
portError.style.display = 'none';
saveConfiguration(newPort,state.enabled)
})
}, _('Save'));
function updateStatus() { function updateStatus() {
statusIcon.textContent = state.running ? '✓' : '✗'; statusIcon.textContent = state.running ? '✓' : '✗';
statusIcon.style.color = state.running ? 'green' : 'red'; statusIcon.style.color = state.running ? 'green' : 'red';
@@ -98,51 +157,54 @@ return view.extend({
statusText.style['font-size'] = '0.92rem'; statusText.style['font-size'] = '0.92rem';
toggleBtn.textContent = state.running ? _('Stop Server') : _('Start Server'); toggleBtn.textContent = state.running ? _('Stop Server') : _('Start Server');
toggleBtn.className = `btn cbi-button cbi-button-${state.running ? 'reset' : 'apply'}`; toggleBtn.className = `btn cbi-button cbi-button-${state.running ? 'reset' : 'apply'}`;
portInput.value = state.port;
} }
// 初始化状态
statusIcon.textContent = '...'; statusIcon.textContent = '...';
statusText.textContent = _('Checking status...'); statusText.textContent = _('Checking status...');
toggleBtn.textContent = _('Loading...'); toggleBtn.textContent = _('Loading...');
toggleBtn.disabled = true; toggleBtn.disabled = true;
const statusSection = E('div', { 'class': 'cbi-section' }, [
// 构建UI E('div', { 'style': 'margin: 15px' }, [
const statusSection = E('div', { 'class': 'cbi-section' }, [ E('h3', {}, _('Throughput speedtest Iperf3')),
E('div', { 'style': 'margin: 15px' }, [ statusContainer,
E('h3', {}, _('Throughput speedtest Iperf3')),
E('div', { 'class': 'cbi-map-descr' }, [statusIcon, statusText]), E('div', {'class': 'cbi-value', 'style': 'margin-top: 20px'}, [
E('div', {'class': 'cbi-value', 'style': 'margin-top: 20px'}, [ E('div', {'class': 'cbi-value-title'}, _('Port Setting')),
E('div', {'class': 'cbi-value-title'}, _('Iperf3 service control')), E('div', {'class': 'cbi-value-field'}, [
E('div', {'class': 'cbi-value-field'}, toggleBtn), portInput,
savePortBtn,
E('div', {'class': 'cbi-value-title'}, _('Download iperf3 client')), portError
E('div', {'class': 'cbi-value-field'}, [ ])
E('div', { ]),
'class': 'cbi-value-field',
'style': 'display: flex;' E('div', {'class': 'cbi-value'}, [
}, [ E('div', {'class': 'cbi-value-title'}, _('Service Control')),
E('button', { E('div', {'class': 'cbi-value-field'}, toggleBtn),
'class': 'btn cbi-button cbi-button-save', ]),
'click': ui.createHandlerFn(this, () => window.open('https://iperf.fr/iperf-download.php', '_blank'))
}, _('Official Website')), E('div', {'class': 'cbi-value'}, [
E('button', { E('div', {'class': 'cbi-value-title'}, _('Download iperf3 client')),
'class': 'btn cbi-button cbi-button-save', E('div', {'class': 'cbi-value-field'}, [
'click': ui.createHandlerFn(this, () => window.open('https://github.com/sirpdboy/luci-app-netspeedtest/releases', '_blank')) E('div', {
}, _('GitHub')) 'class': 'cbi-value-field',
'style': 'display: flex;'
}, [
E('button', {
'class': 'btn cbi-button cbi-button-save',
'click': ui.createHandlerFn(this, () => window.open('https://iperf.fr/iperf-download.php', '_blank'))
}, _('Official Website')),
E('button', {
'class': 'btn cbi-button cbi-button-save',
'click': ui.createHandlerFn(this, () => window.open('https://github.com/sirpdboy/netspeedtest/releases', '_blank'))
}, _('GitHub'))
])
])
]) ])
]) ])
]), ]);
E('div', { 'style': 'text-align: right; font-style: italic; margin-top: 20px;' }, [
_('© github '),
E('a', {
'href': 'https://github.com/sirpdboy/luci-app-netspeedtest',
'target': '_blank',
'style': 'text-decoration: none;'
}, 'by sirpdboy')
])
])
]);
// 初始化状态检查 // 初始化状态检查
checkProcess().then(res => { checkProcess().then(res => {
@@ -161,8 +223,6 @@ const statusSection = E('div', { 'class': 'cbi-section' }, [
}, 5); }, 5);
}); });
return statusSection; return statusSection;
} }
});
});
@@ -78,21 +78,11 @@ return view.extend({
E('small', {}, _('Refresh every %s seconds.').format(L.env.pollinterval)) E('small', {}, _('Refresh every %s seconds.').format(L.env.pollinterval))
]), ]),
E('div', { 'class': 'cbi-section-actions cbi-section-actions-right' }) E('div', { 'class': 'cbi-section-actions cbi-section-actions-right' })
]),
E('div', { 'style': 'text-align: right; font-style: italic; margin-top: 10px;' }, [
E('span', {}, [
_('© github '),
E('a', {
'href': 'https://github.com/sirpdboy',
'target': '_blank',
'style': 'text-decoration: none;'
}, 'by sirpdboy')
])
]) ])
]); ]);
} },
// handleSaveApply: null, handleSaveApply: null,
// handleSave: null, handleSave: null,
// handleReset: null handleReset: null
}); });
@@ -1,5 +1,5 @@
// Copyright (C) 2023-2025 muink
// Copyright (C) 2019-2026 sirpdboy // Copyright (C) 2019-2026 sirpdboy
// Fixed version - better Ookla result parsing
'use strict'; 'use strict';
'require view'; 'require view';
@@ -25,7 +25,6 @@ return view.extend({
]); ]);
}, },
// 检测可用版本
detectVersions: function(res) { detectVersions: function(res) {
var hasOokla = !!(res[0] && res[0].path); var hasOokla = !!(res[0] && res[0].path);
var hasPython = !!(res[1] && res[1].path); var hasPython = !!(res[1] && res[1].path);
@@ -37,17 +36,14 @@ return view.extend({
}; };
}, },
// 检查是否真正在测试中(文件新鲜且内容为Testing)
isTesting: function(resultContent, resultMtime) { isTesting: function(resultContent, resultMtime) {
if (!resultContent || resultContent.length === 0) return false; if (!resultContent || resultContent.length === 0) return false;
if (resultContent[0].trim() !== 'Testing') return false; if (resultContent[0].trim() !== 'Testing') return false;
// 检查文件是否在超时时间内
var fileAge = Date.now() - resultMtime; var fileAge = Date.now() - resultMtime;
return fileAge < Timeout; return fileAge < Timeout;
}, },
// 解析结果内容
parseResult: function(content) { parseResult: function(content) {
if (!content || content.length === 0) { if (!content || content.length === 0) {
return { type: 'none', data: null }; return { type: 'none', data: null };
@@ -55,17 +51,13 @@ return view.extend({
var firstLine = content[0].trim(); var firstLine = content[0].trim();
// 检查是否是Testing状态
if (firstLine === 'Testing') { if (firstLine === 'Testing') {
return { type: 'testing', data: null }; return { type: 'testing', data: null };
} }
// 检查是否是失败状态
if (firstLine === 'Test failed') { if (firstLine === 'Test failed') {
return { type: 'failed', data: null }; return { type: 'failed', data: null };
} }
// 检查是否包含Result URL (Ookla格式)
var resultUrl = null; var resultUrl = null;
for (var i = 0; i < content.length; i++) { for (var i = 0; i < content.length; i++) {
var line = content[i]; var line = content[i];
@@ -82,18 +74,13 @@ return view.extend({
return { type: 'url', data: resultUrl }; return { type: 'url', data: resultUrl };
} }
// 检查是否是直接URL (Python格式)
if (firstLine.match(/^https?:\/\//) || firstLine.match(/\.png$/)) { if (firstLine.match(/^https?:\/\//) || firstLine.match(/\.png$/)) {
return { type: 'url', data: firstLine }; return { type: 'url', data: firstLine };
} }
// 检查是否是速度数据
if (firstLine.match(/Download:/i) || firstLine.match(/Upload:/i)) { if (firstLine.match(/Download:/i) || firstLine.match(/Upload:/i)) {
return { type: 'speed', data: content.join('\n') }; return { type: 'speed', data: content.join('\n') };
} }
// 其他情况,可能是完整的速度测试结果
// 检查是否包含Download和Upload信息
var hasDownload = false; var hasDownload = false;
var hasUpload = false; var hasUpload = false;
for (var j = 0; j < content.length; j++) { for (var j = 0; j < content.length; j++) {
@@ -114,18 +101,13 @@ return view.extend({
var result_stat = nodes.querySelector('#speedtest_result'); var result_stat = nodes.querySelector('#speedtest_result');
var start_btn = nodes.querySelector('.cbi-button-apply'); var start_btn = nodes.querySelector('.cbi-button-apply');
// 解析结果
var result = this.parseResult(result_content); var result = this.parseResult(result_content);
var is_testing = this.isTesting(result_content, result_mtime); var is_testing = this.isTesting(result_content, result_mtime);
// 更新测试按钮状态
if (start_btn) { if (start_btn) {
start_btn.disabled = is_testing; start_btn.disabled = is_testing;
} }
// 更新结果状态
if (result_stat) { if (result_stat) {
if (is_testing) { if (is_testing) {
result_stat.innerHTML = "<span style='color:green;font-weight:bold;margin-left:20px'>" + result_stat.innerHTML = "<span style='color:green;font-weight:bold;margin-left:20px'>" +
@@ -212,7 +194,6 @@ return view.extend({
break; break;
case 'unknown': case 'unknown':
// 未知格式,直接显示
result_stat.innerHTML = "<div style='margin-left:20px; padding:10px; background:#f5f5f5; border-radius:4px'>" + result_stat.innerHTML = "<div style='margin-left:20px; padding:10px; background:#f5f5f5; border-radius:4px'>" +
"<strong>" + _('Test Results:') + "</strong><br>" + "<strong>" + _('Test Results:') + "</strong><br>" +
"<pre style='margin:5px 0 0 0; font-family:inherit; white-space:pre-wrap; color:#2e7d32'>" + "<pre style='margin:5px 0 0 0; font-family:inherit; white-space:pre-wrap; color:#2e7d32'>" +
@@ -221,9 +202,8 @@ return view.extend({
break; break;
default: default:
// 无结果
result_stat.innerHTML = "<span style='color:gray;margin-left:20px'>" + result_stat.innerHTML = "<span style='color:gray;margin-left:20px'>" +
"<em>" + _('No test results yet. Click "Start Speed Test" to begin.') + "</em>" + "<em>" + _('No test results yet.') + "</em>" +
"</span>"; "</span>";
} }
} }
@@ -241,9 +221,8 @@ return view.extend({
var is_testing = this.isTesting(result_content, result_mtime); var is_testing = this.isTesting(result_content, result_mtime);
var m, s, o; var m, s, o;
m = new form.Map('netspeedtest', _('WAN SpeedTest')); m = new form.Map('netspeedtest', _('Wan SpeedTest'));
// 结果显示区域
s = m.section(form.TypedSection, '_result'); s = m.section(form.TypedSection, '_result');
s.anonymous = true; s.anonymous = true;
s.render = function(section_id) { s.render = function(section_id) {
@@ -353,7 +332,6 @@ return view.extend({
]); ]);
case 'unknown': case 'unknown':
// 未知格式,直接显示
return E('div', { id: result_id, class: 'cbi-section' }, [ return E('div', { id: result_id, class: 'cbi-section' }, [
E('div', { style: 'margin-left:20px; padding:10px; background:#f5f5f5; border-radius:4px' }, [ E('div', { style: 'margin-left:20px; padding:10px; background:#f5f5f5; border-radius:4px' }, [
E('strong', {}, _('Test Results:')), E('strong', {}, _('Test Results:')),
@@ -365,20 +343,17 @@ return view.extend({
]); ]);
default: default:
// 无结果
return E('div', { id: result_id, class: 'cbi-section' }, [ return E('div', { id: result_id, class: 'cbi-section' }, [
E('span', { style: 'color:gray;margin-left:20px' }, [ E('span', { style: 'color:gray;margin-left:20px' }, [
E('em', {}, _('No test results yet. Click "Start Speed Test" to begin.')) E('em', {}, _('No test results yet.'))
]) ])
]); ]);
} }
}; };
// 配置部分
s = m.section(form.NamedSection, 'config', 'netspeedtest'); s = m.section(form.NamedSection, 'config', 'netspeedtest');
s.anonymous = true; s.anonymous = true;
// 版本选择
o = s.option(form.ListValue, 'test_version', _('Select Test Version')); o = s.option(form.ListValue, 'test_version', _('Select Test Version'));
if (has_ookla) { if (has_ookla) {
@@ -404,7 +379,6 @@ return view.extend({
o.inputtitle = _('Click to start speed test'); o.inputtitle = _('Click to start speed test');
o.inputstyle = 'apply'; o.inputstyle = 'apply';
// 只有真正在测试中才禁用按钮
if (is_testing) { if (is_testing) {
o.readonly = true; o.readonly = true;
} }
@@ -413,17 +387,14 @@ return view.extend({
var btn = this; var btn = this;
btn.disabled = true; btn.disabled = true;
// 获取选中的版本
var versionSelect = document.getElementById('widget.cbid.netspeedtest.config.test_version'); var versionSelect = document.getElementById('widget.cbid.netspeedtest.config.test_version');
if (!versionSelect) { if (!versionSelect) {
versionSelect = document.querySelector('select[name="test_version"]'); versionSelect = document.querySelector('select[name="test_version"]');
} }
var version = versionSelect ? versionSelect.value : (has_ookla ? 'ookla' : 'python'); var version = versionSelect ? versionSelect.value : (has_ookla ? 'ookla' : 'python');
// 先写入Testing状态
return fs.write(ResultFile, 'Testing\n') return fs.write(ResultFile, 'Testing\n')
.then(function() { .then(function() {
// 在后台执行测试脚本
var cmd = 'nohup ' + SpeedtestScript; var cmd = 'nohup ' + SpeedtestScript;
if (version) { if (version) {
cmd += ' --version ' + version; cmd += ' --version ' + version;
@@ -445,9 +416,7 @@ return view.extend({
return m.render() return m.render()
.then(L.bind(function(m, nodes) { .then(L.bind(function(m, nodes) {
nodes.result_mtime = result_mtime; // 保存结果文件修改时间 nodes.result_mtime = result_mtime;
// 添加轮询 - 每2秒检查一次结果
poll.add(L.bind(function() { poll.add(L.bind(function() {
return Promise.all([ return Promise.all([
L.resolveDefault(fs.stat('/usr/bin/ookla-speedtest'), {}), L.resolveDefault(fs.stat('/usr/bin/ookla-speedtest'), {}),
@@ -469,7 +438,6 @@ return view.extend({
handleReset: null handleReset: null
}); });
// 辅助函数
function escapeHTML(str) { function escapeHTML(str) {
if (!str) return ''; if (!str) return '';
return String(str) return String(str)
@@ -70,41 +70,29 @@ msgstr "Iperf3服务端"
msgid "Command failed" msgid "Command failed"
msgstr "命令无效" msgstr "命令无效"
msgid "Failed to read log file"
msgstr "读取日志失败"
msgid "No log content available"
msgstr "没有日志记录"
msgid "Error: " msgid "Error: "
msgstr "错误: " msgstr "错误: "
msgid "Iperf3 service control" msgid "Service Control"
msgstr "Iperf3 服务控制" msgstr "服务控制"
msgid "Listen Port"
msgstr "监听端口"
msgid "Invalid format. Use [::]:port or ip:port"
msgstr "格式错误.如 [::]:端口 或 ip:端口"
msgid "Enable Log View"
msgstr "开启日志显示"
msgid "Download iperf3 client" msgid "Download iperf3 client"
msgstr "下载iperf3客户端" msgstr "下载iperf3客户端"
msgid "Run Log" msgid "Refresh every %s seconds."
msgstr "运行日志" msgstr "每 %s 秒刷新"
msgid "Refresh Log" msgid "Clear logs"
msgstr "刷新日志" msgstr "清除日志"
msgid "Official Website" msgid "Official Website"
msgstr "官方网站" msgstr "官方网站"
msgid "Please enable the Homebox service" msgid "Please start the Homebox service"
msgstr "请将homebox服务启用" msgstr "请启动homebox服务"
msgid "Homebox service control" msgid "Homebox service control"
msgstr "Homebox服务控制" msgstr "Homebox服务控制"
@@ -145,6 +133,18 @@ msgstr "由于浏览器安全策略,Homebox接口https不能直接嵌入。"
msgid "Select speed measurement station" msgid "Select speed measurement station"
msgstr "选择测速站点" msgstr "选择测速站点"
msgid "Iperf3 Port"
msgstr "Iperf3监听端口"
msgid "Port range must be 1024-65535"
msgstr "端口范围1024-65535"
msgid "Auto Start"
msgstr "自动启动"
msgid "Port Setting"
msgstr "监听端口"
msgid "" msgid ""
msgstr "" msgstr ""
+64
View File
@@ -0,0 +1,64 @@
#!/bin/sh /etc/rc.common
# netspeedtest init script
START=95
STOP=15
USE_PROCD=1
PATH=/usr/bin:/bin:/usr/sbin:/sbin
start_service() {
START_IPERF3=$(uci get netspeedtest.config.iperf3_enabled 2>/dev/null || echo "0")
START_HOMEBOX=$(uci get netspeedtest.config.homebox_enabled 2>/dev/null || echo "0")
[ "$START_HOMEBOX" = "1" ] || return 0
port=$(uci get netspeedtest.config.homebox_port 2>/dev/null || echo "3300")
procd_open_instance
procd_set_param command /usr/bin/homebox serve --port "$port"
procd_set_param stdout 1
procd_set_param stderr 1
procd_set_param respawn
procd_close_instance
}
stop_service() {
logger -t netspeedtest "Stopping homebox"
ubus call service delete '{ "name": "netspeedtest" }' 2>/dev/null
killall homebox 2>/dev/null || true
local retries=5
while pidof homebox >/dev/null 2>&1 && [ $retries -gt 0 ]; do
sleep 0.2
retries=$((retries-1))
done
}
service_triggers() {
procd_add_reload_trigger "netspeedtest"
}
reload_service() {
stop
start
}
status() {
START_HOMEBOX=$(uci get netspeedtest.config.homebox_enabled 2>/dev/null || echo "0")
if [ "$START_HOMEBOX" != "1" ]; then
return 3
fi
_procd_status
}
boot() {
sleep 2
start
}
@@ -1,88 +0,0 @@
#!/bin/sh
# Copyright (C) 2019-2026 sirpdboy
# RPC plugin for netspeedtest
. /usr/share/libubox/jshn.sh
get_ookla_servers() {
local servers="[]"
if [ -f '/usr/bin/ookla-speedtest' ] && [ -x '/usr/bin/ookla-speedtest' ]; then
local json_output=$(/usr/bin/ookla-speedtest --servers --format=json 2>/dev/null)
if [ -n "$json_output" ]; then
servers=$(echo "$json_output" | sed -n 's/.*"servers":\(\[.*\]\).*/\1/p')
[ -z "$servers" ] && servers="[]"
fi
fi
echo "$servers"
}
get_python_servers() {
local servers="[]"
local temp_file="/tmp/python_servers.tmp"
if [ -f '/usr/bin/speedtest' ] && [ -x '/usr/bin/speedtest' ]; then
/usr/bin/speedtest --list > "$temp_file" 2>/dev/null
if [ -s "$temp_file" ]; then
local json=""
local count=0
while IFS= read -r line; do
if echo "$line" | grep -qE '^[0-9]+\)'; then
local id=$(echo "$line" | sed -E 's/^([0-9]+)\).*/\1/')
local sponsor=$(echo "$line" | sed -E 's/^[0-9]+\)[[:space:]]+([^(]+).*/\1/' | sed -e 's/[[:space:]]*$//' -e 's/"/\\"/g')
local city_country=$(echo "$line" | grep -o '([^)]*)' | head -1 | sed 's/[()]//g')
local city=$(echo "$city_country" | cut -d',' -f1 | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' -e 's/"/\\"/g')
local country=$(echo "$city_country" | cut -d',' -f2 | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' -e 's/"/\\"/g')
local distance=$(echo "$line" | grep -o '\[[0-9.]* km\]' | sed 's/\[//g' | sed 's/ km\]//g')
[ -z "$distance" ] && distance="0"
if [ $count -gt 0 ]; then
json="$json,"
fi
json="$json{\"id\":$id,\"sponsor\":\"$sponsor\",\"city\":\"$city\",\"country\":\"$country\",\"distance\":$distance}"
count=$((count + 1))
[ $count -ge 10 ] && break
fi
done < "$temp_file"
[ -n "$json" ] && servers="[$json]"
fi
rm -f "$temp_file"
fi
echo "$servers"
}
case "$1" in
list)
echo '{
"get_servers": {
"signature": [],
"help": "Get speedtest servers"
}
}'
;;
call)
case "$2" in
get_servers)
json_init
json_add_string "ookla" "$(get_ookla_servers)"
json_add_string "python" "$(get_python_servers)"
json_dump
;;
*)
echo '{"error": "Method not found"}'
;;
esac
;;
*)
echo '{"error": "Invalid command"}'
;;
esac
+23 -11
View File
@@ -1,51 +1,63 @@
#
# Copyright (C) 2024 sbwml <admin@cooluc.com>
# Copyright (c) 2025-2026 sirpdboy <herboy2008@gmail.com>
#
# This is free software, licensed under the GPL-3.0 License.
#
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=ookla-speedtest PKG_NAME:=ookla-speedtest
PKG_VERSION:=1.2.0 PKG_VERSION:=1.2.0
PKG_RELEASE:=2 PKG_RELEASE:=3
ifeq ($(ARCH),aarch64) ifeq ($(ARCH),aarch64)
PKG_ARCH_SUFFIX:=aarch64
PKG_HASH:=skip PKG_HASH:=skip
else ifeq ($(ARCH),arm) else ifeq ($(ARCH),arm)
ARM_CPU_FEATURES:=$(word 2,$(subst +,$(space),$(call qstrip,$(CONFIG_CPU_TYPE)))) ARM_CPU_FEATURES:=$(word 2,$(subst +,$(space),$(call qstrip,$(CONFIG_CPU_TYPE))))
ifeq ($(ARM_CPU_FEATURES),) ifeq ($(ARM_CPU_FEATURES),)
ARCH:=armel PKG_ARCH_SUFFIX:=armel
PKG_HASH:=skip PKG_HASH:=skip
else else
ARCH:=armhf PKG_ARCH_SUFFIX:=armhf
PKG_HASH:=skip PKG_HASH:=skip
endif endif
else ifeq ($(ARCH),i386) else ifeq ($(ARCH),i386)
PKG_ARCH_SUFFIX:=i386
PKG_HASH:=skip PKG_HASH:=skip
else ifeq ($(ARCH),x86_64) else ifeq ($(ARCH),x86_64)
PKG_ARCH_SUFFIX:=x86_64
PKG_HASH:=skip PKG_HASH:=skip
endif endif
PKG_SOURCE:=ookla-speedtest-$(PKG_VERSION)-linux-$(ARCH).tgz PKG_SOURCE:=ookla-speedtest-$(PKG_VERSION)-linux-$(PKG_ARCH_SUFFIX).tgz
PKG_SOURCE_URL:=https://install.speedtest.net/app/cli PKG_SOURCE_URL:=https://install.speedtest.net/app/cli
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION)-$(PKG_ARCH_SUFFIX)
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION)
PKG_MAINTAINER:=sbwml <admin@cooluc.com> PKG_MAINTAINER:=sbwml <admin@cooluc.com>
include $(INCLUDE_DIR)/package.mk include $(INCLUDE_DIR)/package.mk
define Package/$(PKG_NAME) define Package/$(PKG_NAME)
SECTION:=net SECTION:=net
CATEGORY:=Network CATEGORY:=Network
SUBMENU:=Speed Test
TITLE:=Speedtest CLI by Ookla TITLE:=Speedtest CLI by Ookla
URL:=https://www.speedtest.net/
DEPENDS:=@(aarch64||arm||i386||x86_64) +ca-certificates DEPENDS:=@(aarch64||arm||i386||x86_64) +ca-certificates
URL:=https://www.speedtest.net/ URL:=https://www.speedtest.net/
endef endef
define Package/$(PKG_NAME)/description define Package/$(PKG_NAME)/description
The Global Broadband Speed Test Speedtest CLI by Ookla is the official command line client
for testing internet bandwidth using speedtest.net servers.
endef endef
define Build/Prepare define Build/Prepare
( \ ( \
pushd $(PKG_BUILD_DIR) ; \ pushd $(PKG_BUILD_DIR) ; \
$(TAR) -zxf $(DL_DIR)/ookla-speedtest-$(PKG_VERSION)-linux-$(ARCH).tgz -C . ; \ $(TAR) -zxf $(DL_DIR)/ookla-speedtest-$(PKG_VERSION)-linux-$(PKG_ARCH_SUFFIX).tgz -C . ; \
popd ; \ popd ; \
) )
endef endef
@@ -58,4 +70,4 @@ define Package/$(PKG_NAME)/install
$(INSTALL_BIN) $(PKG_BUILD_DIR)/speedtest $(1)/usr/bin/ookla-speedtest $(INSTALL_BIN) $(PKG_BUILD_DIR)/speedtest $(1)/usr/bin/ookla-speedtest
endef endef
$(eval $(call BuildPackage,$(PKG_NAME))) $(eval $(call BuildPackage,$(PKG_NAME)))