plexo-cli 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plexo_cli-0.1.0/.gitignore +9 -0
- plexo_cli-0.1.0/PKG-INFO +91 -0
- plexo_cli-0.1.0/README.md +59 -0
- plexo_cli-0.1.0/pyproject.toml +61 -0
- plexo_cli-0.1.0/src/plexo/__init__.py +4 -0
- plexo_cli-0.1.0/src/plexo/cli.py +190 -0
- plexo_cli-0.1.0/src/plexo/core/__init__.py +1 -0
- plexo_cli-0.1.0/src/plexo/core/adapter.py +139 -0
- plexo_cli-0.1.0/src/plexo/core/client.py +148 -0
- plexo_cli-0.1.0/src/plexo/core/engine.py +266 -0
- plexo_cli-0.1.0/src/plexo/core/qos.py +76 -0
- plexo_cli-0.1.0/src/plexo/core/sparse.py +92 -0
- plexo_cli-0.1.0/src/plexo/proxy/__init__.py +1 -0
- plexo_cli-0.1.0/src/plexo/proxy/server.py +170 -0
- plexo_cli-0.1.0/src/plexo/torrent/__init__.py +1 -0
- plexo_cli-0.1.0/src/plexo/torrent/bencode.py +62 -0
- plexo_cli-0.1.0/src/plexo/torrent/client.py +225 -0
- plexo_cli-0.1.0/src/plexo/torrent/metadata.py +117 -0
- plexo_cli-0.1.0/src/plexo/ui/__init__.py +1 -0
- plexo_cli-0.1.0/src/plexo/ui/dashboard.py +98 -0
- plexo_cli-0.1.0/tests/__init__.py +1 -0
- plexo_cli-0.1.0/tests/test_adapter.py +38 -0
- plexo_cli-0.1.0/tests/test_engine.py +93 -0
- plexo_cli-0.1.0/tests/test_sparse.py +59 -0
- plexo_cli-0.1.0/tests/test_torrent.py +95 -0
plexo_cli-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: plexo-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Multi-network bandwidth bonding download engine & CLI
|
|
5
|
+
Author-email: Jagjot Singh <gotogether@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: bandwidth-aggregation,download-accelerator,multi-interface,network-bonding,proxy,torrent
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Topic :: Internet
|
|
21
|
+
Classifier: Topic :: Utilities
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Requires-Dist: click>=8.1.0
|
|
24
|
+
Requires-Dist: httpx>=0.27.0
|
|
25
|
+
Requires-Dist: psutil>=5.9.0
|
|
26
|
+
Requires-Dist: rich>=13.7.0
|
|
27
|
+
Requires-Dist: urllib3>=2.0.0
|
|
28
|
+
Provides-Extra: test
|
|
29
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'test'
|
|
30
|
+
Requires-Dist: pytest>=8.0.0; extra == 'test'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# Plexo CLI (`plexo-cli`)
|
|
34
|
+
|
|
35
|
+
> **Multi-network bandwidth bonding download accelerator in your terminal.**
|
|
36
|
+
|
|
37
|
+
Combines multiple active internet connections (e.g. **Hostel Wi-Fi + Mobile 5G via USB Tethering + Ethernet**) into a single parallel stream to boost download speeds for direct files and developer tools.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Features
|
|
42
|
+
- **Parallel HTTP Range Slicing**: Binds concurrent download workers to distinct physical network interfaces (source IP socket binding).
|
|
43
|
+
- **Zero-Touch Local Proxy (`plexo proxy`)**: Run a local forward proxy (`127.0.0.1:8899`) to automatically accelerate `pip install`, `git clone`, `curl`, and browser downloads.
|
|
44
|
+
- **Rich Terminal UI**: Live dashboard showing real-time throughput per individual network adapter.
|
|
45
|
+
- **Dynamic Failover**: Auto-detects lagging or disconnected hotspots and re-routes pending chunks to healthy connections without corrupting files.
|
|
46
|
+
- **Sparse File Allocation**: Instant zero-overhead pre-allocation on disk with non-blocking multi-threaded byte writes.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install plexo-cli
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Or install in editable mode from source:
|
|
57
|
+
```bash
|
|
58
|
+
git clone https://github.com/Jagjotkarhu/plexo.git
|
|
59
|
+
cd plexo/packages/plexo-cli
|
|
60
|
+
pip install -e .
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Quickstart
|
|
66
|
+
|
|
67
|
+
### 1. Scan Network Adapters
|
|
68
|
+
```bash
|
|
69
|
+
plexo adapters
|
|
70
|
+
```
|
|
71
|
+
Lists all discovered network interfaces (Wi-Fi, USB cellular tether, Ethernet), their local IPs, and connectivity status.
|
|
72
|
+
|
|
73
|
+
### 2. Download a File with Aggregated Bandwidth
|
|
74
|
+
```bash
|
|
75
|
+
plexo download https://example.com/largefile.zip -o largefile.zip
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 3. Accelerate `pip` or Terminal Tools
|
|
79
|
+
Start the local proxy:
|
|
80
|
+
```bash
|
|
81
|
+
plexo proxy --port 8899
|
|
82
|
+
```
|
|
83
|
+
Then run any tool using the proxy:
|
|
84
|
+
```bash
|
|
85
|
+
pip install torch --proxy http://127.0.0.1:8899
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## License
|
|
91
|
+
MIT © Anmol Kapil & Jagjot Singh
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Plexo CLI (`plexo-cli`)
|
|
2
|
+
|
|
3
|
+
> **Multi-network bandwidth bonding download accelerator in your terminal.**
|
|
4
|
+
|
|
5
|
+
Combines multiple active internet connections (e.g. **Hostel Wi-Fi + Mobile 5G via USB Tethering + Ethernet**) into a single parallel stream to boost download speeds for direct files and developer tools.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
- **Parallel HTTP Range Slicing**: Binds concurrent download workers to distinct physical network interfaces (source IP socket binding).
|
|
11
|
+
- **Zero-Touch Local Proxy (`plexo proxy`)**: Run a local forward proxy (`127.0.0.1:8899`) to automatically accelerate `pip install`, `git clone`, `curl`, and browser downloads.
|
|
12
|
+
- **Rich Terminal UI**: Live dashboard showing real-time throughput per individual network adapter.
|
|
13
|
+
- **Dynamic Failover**: Auto-detects lagging or disconnected hotspots and re-routes pending chunks to healthy connections without corrupting files.
|
|
14
|
+
- **Sparse File Allocation**: Instant zero-overhead pre-allocation on disk with non-blocking multi-threaded byte writes.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install plexo-cli
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Or install in editable mode from source:
|
|
25
|
+
```bash
|
|
26
|
+
git clone https://github.com/Jagjotkarhu/plexo.git
|
|
27
|
+
cd plexo/packages/plexo-cli
|
|
28
|
+
pip install -e .
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Quickstart
|
|
34
|
+
|
|
35
|
+
### 1. Scan Network Adapters
|
|
36
|
+
```bash
|
|
37
|
+
plexo adapters
|
|
38
|
+
```
|
|
39
|
+
Lists all discovered network interfaces (Wi-Fi, USB cellular tether, Ethernet), their local IPs, and connectivity status.
|
|
40
|
+
|
|
41
|
+
### 2. Download a File with Aggregated Bandwidth
|
|
42
|
+
```bash
|
|
43
|
+
plexo download https://example.com/largefile.zip -o largefile.zip
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### 3. Accelerate `pip` or Terminal Tools
|
|
47
|
+
Start the local proxy:
|
|
48
|
+
```bash
|
|
49
|
+
plexo proxy --port 8899
|
|
50
|
+
```
|
|
51
|
+
Then run any tool using the proxy:
|
|
52
|
+
```bash
|
|
53
|
+
pip install torch --proxy http://127.0.0.1:8899
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## License
|
|
59
|
+
MIT © Anmol Kapil & Jagjot Singh
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "plexo-cli"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Multi-network bandwidth bonding download engine & CLI"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Jagjot Singh", email = "gotogether@gmail.com" },
|
|
14
|
+
]
|
|
15
|
+
keywords = [
|
|
16
|
+
"download-accelerator",
|
|
17
|
+
"network-bonding",
|
|
18
|
+
"bandwidth-aggregation",
|
|
19
|
+
"multi-interface",
|
|
20
|
+
"torrent",
|
|
21
|
+
"proxy",
|
|
22
|
+
]
|
|
23
|
+
classifiers = [
|
|
24
|
+
"Development Status :: 4 - Beta",
|
|
25
|
+
"Environment :: Console",
|
|
26
|
+
"Intended Audience :: Developers",
|
|
27
|
+
"License :: OSI Approved :: MIT License",
|
|
28
|
+
"Operating System :: OS Independent",
|
|
29
|
+
"Programming Language :: Python :: 3",
|
|
30
|
+
"Programming Language :: Python :: 3.9",
|
|
31
|
+
"Programming Language :: Python :: 3.10",
|
|
32
|
+
"Programming Language :: Python :: 3.11",
|
|
33
|
+
"Programming Language :: Python :: 3.12",
|
|
34
|
+
"Programming Language :: Python :: 3.13",
|
|
35
|
+
"Programming Language :: Python :: 3.14",
|
|
36
|
+
"Topic :: Internet",
|
|
37
|
+
"Topic :: Utilities",
|
|
38
|
+
]
|
|
39
|
+
dependencies = [
|
|
40
|
+
"httpx>=0.27.0",
|
|
41
|
+
"urllib3>=2.0.0",
|
|
42
|
+
"rich>=13.7.0",
|
|
43
|
+
"click>=8.1.0",
|
|
44
|
+
"psutil>=5.9.0",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[project.optional-dependencies]
|
|
48
|
+
test = [
|
|
49
|
+
"pytest>=8.0.0",
|
|
50
|
+
"pytest-asyncio>=0.23.0",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
[project.scripts]
|
|
54
|
+
plexo = "plexo.cli:main"
|
|
55
|
+
|
|
56
|
+
[tool.hatch.build.targets.wheel]
|
|
57
|
+
packages = ["src/plexo"]
|
|
58
|
+
|
|
59
|
+
[tool.pytest.ini_options]
|
|
60
|
+
pythonpath = ["src"]
|
|
61
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""Command Line Interface for Plexo.
|
|
2
|
+
|
|
3
|
+
Exposes subcommands: download, adapters, proxy, info.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
import time
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
# Ensure UTF-8 output on Windows consoles
|
|
14
|
+
if sys.platform == "win32":
|
|
15
|
+
try:
|
|
16
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
17
|
+
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
|
18
|
+
except Exception:
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
import click
|
|
22
|
+
from rich.console import Console
|
|
23
|
+
from rich.panel import Panel
|
|
24
|
+
from rich.table import Table
|
|
25
|
+
from . import __version__
|
|
26
|
+
from .core.adapter import discover_adapters, get_bonded_pool
|
|
27
|
+
from .core.engine import BondingDownloadEngine, ProgressUpdate
|
|
28
|
+
from .proxy.server import run_proxy
|
|
29
|
+
from .ui.dashboard import TerminalDashboard
|
|
30
|
+
|
|
31
|
+
console = Console()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@click.group()
|
|
35
|
+
@click.version_option(version=__version__, prog_name="plexo")
|
|
36
|
+
def main():
|
|
37
|
+
"""Plexo: Multi-Network Bandwidth Bonding Download Engine."""
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@main.command(name="adapters")
|
|
42
|
+
@click.option("--all", "show_all", is_flag=True, help="Show all adapters including offline/internal.")
|
|
43
|
+
def list_adapters(show_all: bool):
|
|
44
|
+
"""Scan and benchmark active network adapters."""
|
|
45
|
+
console.print("\n[bold cyan]Scanning Network Interfaces...[/bold cyan]")
|
|
46
|
+
adapters = discover_adapters(verify_online=True)
|
|
47
|
+
|
|
48
|
+
table = Table(title="[bold green]Network Interfaces[/bold green]")
|
|
49
|
+
table.add_column("Interface Name", style="bold white")
|
|
50
|
+
table.add_column("Type", style="magenta")
|
|
51
|
+
table.add_column("IPv4 Address", style="cyan")
|
|
52
|
+
table.add_column("Link Speed", style="yellow")
|
|
53
|
+
table.add_column("Internet Route", style="bold")
|
|
54
|
+
|
|
55
|
+
display_list = adapters if show_all else [a for a in adapters if a.is_online]
|
|
56
|
+
|
|
57
|
+
for a in display_list:
|
|
58
|
+
status_text = "[bold green]ONLINE[/bold green]" if a.is_online else "[dim red]OFFLINE[/dim red]"
|
|
59
|
+
speed_str = f"{a.speed_mbps} Mbps" if a.speed_mbps > 0 else "Unknown"
|
|
60
|
+
table.add_row(a.name, a.kind.upper(), a.ip_address, speed_str, status_text)
|
|
61
|
+
|
|
62
|
+
console.print(table)
|
|
63
|
+
online_count = sum(1 for a in adapters if a.is_online)
|
|
64
|
+
|
|
65
|
+
if online_count >= 2:
|
|
66
|
+
console.print(
|
|
67
|
+
f"\n[bold green][OK] Ready for bonding:[/bold green] [yellow]{online_count}[/yellow] independent connections detected.\n"
|
|
68
|
+
)
|
|
69
|
+
elif online_count == 1:
|
|
70
|
+
console.print(
|
|
71
|
+
"\n[yellow][INFO] 1 connection active.[/yellow] Plug in Phone 5G USB tethering or Ethernet to enable multi-network bonding.\n"
|
|
72
|
+
)
|
|
73
|
+
else:
|
|
74
|
+
console.print("\n[bold red][X] No online connections found.[/bold red]\n")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@main.command(name="download")
|
|
78
|
+
@click.argument("url")
|
|
79
|
+
@click.option("-o", "--output", "output_path", type=click.Path(), default=None, help="Output destination path.")
|
|
80
|
+
@click.option("--chunk-size", default=8, type=int, help="Chunk size in MB (default: 8 MB).")
|
|
81
|
+
@click.option("--workers", default=2, type=int, help="Workers per network adapter (default: 2).")
|
|
82
|
+
def download_file(url: str, output_path: Optional[str], chunk_size: int, workers: int):
|
|
83
|
+
"""Download a file using bonded multi-network connections."""
|
|
84
|
+
adapters = get_bonded_pool()
|
|
85
|
+
if not adapters:
|
|
86
|
+
console.print("[bold red]Error: No active online network adapters detected.[/bold red]")
|
|
87
|
+
raise click.Abort()
|
|
88
|
+
|
|
89
|
+
dashboard = TerminalDashboard(console=console)
|
|
90
|
+
chunk_bytes = chunk_size * 1024 * 1024
|
|
91
|
+
|
|
92
|
+
engine = BondingDownloadEngine(
|
|
93
|
+
url=url,
|
|
94
|
+
output_path=output_path,
|
|
95
|
+
adapters=adapters,
|
|
96
|
+
chunk_size=chunk_bytes,
|
|
97
|
+
workers_per_adapter=workers,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
probe = engine._prepare()
|
|
101
|
+
dashboard.setup(probe.filename, probe.total_bytes, adapters)
|
|
102
|
+
|
|
103
|
+
start_time = time.time()
|
|
104
|
+
with dashboard.progress:
|
|
105
|
+
final_path = engine.execute(progress_callback=dashboard.update)
|
|
106
|
+
|
|
107
|
+
elapsed = time.time() - start_time
|
|
108
|
+
total_bytes = final_path.stat().st_size
|
|
109
|
+
|
|
110
|
+
# Calculate checksum
|
|
111
|
+
from .core.sparse import SparseFileWriter
|
|
112
|
+
with SparseFileWriter(final_path, total_bytes) as writer:
|
|
113
|
+
checksum = writer.calculate_checksum()
|
|
114
|
+
|
|
115
|
+
dashboard.finish(final_path.name, total_bytes, elapsed, checksum)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@main.command(name="proxy")
|
|
119
|
+
@click.option("--host", default="127.0.0.1", help="Proxy listening host (default: 127.0.0.1).")
|
|
120
|
+
@click.option("--port", default=8899, type=int, help="Proxy listening port (default: 8899).")
|
|
121
|
+
def start_proxy(host: str, port: int):
|
|
122
|
+
"""Launch local acceleration proxy for pip, git, and curl."""
|
|
123
|
+
run_proxy(host=host, port=port)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@main.command(name="torrent")
|
|
127
|
+
@click.argument("source")
|
|
128
|
+
@click.option("-o", "--output-dir", default=".", help="Directory to save downloaded files.")
|
|
129
|
+
def download_torrent(source: str, output_dir: str):
|
|
130
|
+
"""Download a torrent or magnet link across bonded network adapters."""
|
|
131
|
+
from .torrent.metadata import parse_magnet, parse_torrent_file
|
|
132
|
+
from .torrent.client import TorrentBondingEngine
|
|
133
|
+
|
|
134
|
+
adapters = get_bonded_pool()
|
|
135
|
+
if not adapters:
|
|
136
|
+
console.print("[bold red]Error: No active online network adapters detected.[/bold red]")
|
|
137
|
+
raise click.Abort()
|
|
138
|
+
|
|
139
|
+
if source.startswith("magnet:?"):
|
|
140
|
+
meta = parse_magnet(source)
|
|
141
|
+
else:
|
|
142
|
+
meta = parse_torrent_file(source)
|
|
143
|
+
|
|
144
|
+
console.print(
|
|
145
|
+
Panel.fit(
|
|
146
|
+
f"[bold cyan]Plexo Torrent Swarm Engine[/bold cyan]\n"
|
|
147
|
+
f"Name: [bold yellow]{meta.name}[/bold yellow]\n"
|
|
148
|
+
f"Info Hash: [dim]{meta.info_hash_hex}[/dim]\n"
|
|
149
|
+
f"Active Network Links: [bold green]{len(adapters)}[/bold green]",
|
|
150
|
+
border_style="cyan",
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
dashboard = TerminalDashboard(console=console)
|
|
155
|
+
dashboard.setup(meta.name, meta.total_length, adapters)
|
|
156
|
+
|
|
157
|
+
engine = TorrentBondingEngine(metadata=meta, output_dir=output_dir, adapters=adapters)
|
|
158
|
+
|
|
159
|
+
start_time = time.time()
|
|
160
|
+
with dashboard.progress:
|
|
161
|
+
final_path = engine.download(progress_callback=dashboard.update)
|
|
162
|
+
|
|
163
|
+
elapsed = time.time() - start_time
|
|
164
|
+
total_bytes = final_path.stat().st_size
|
|
165
|
+
|
|
166
|
+
from .core.sparse import SparseFileWriter
|
|
167
|
+
with SparseFileWriter(final_path, total_bytes) as writer:
|
|
168
|
+
checksum = writer.calculate_checksum()
|
|
169
|
+
|
|
170
|
+
dashboard.finish(final_path.name, total_bytes, elapsed, checksum)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@main.command(name="info")
|
|
174
|
+
def show_info():
|
|
175
|
+
"""Display Plexo environment and bonding capabilities."""
|
|
176
|
+
adapters = get_bonded_pool()
|
|
177
|
+
console.print(
|
|
178
|
+
Panel.fit(
|
|
179
|
+
f"[bold cyan]Plexo CLI v{__version__}[/bold cyan]\n"
|
|
180
|
+
f"Bonding Engine: [bold green]Active[/bold green]\n"
|
|
181
|
+
f"Verified Online Adapters: [bold yellow]{len(adapters)}[/bold yellow]\n"
|
|
182
|
+
f"Default Proxy Port: [bold magenta]8899[/bold magenta]",
|
|
183
|
+
border_style="cyan",
|
|
184
|
+
title="System Capabilities",
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
if __name__ == "__main__":
|
|
190
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Core multi-adapter bonding and download engine."""
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Network interface discovery and binding module.
|
|
2
|
+
|
|
3
|
+
Detects active network adapters (Wi-Fi, USB cellular tether, Ethernet),
|
|
4
|
+
filters internal/virtual networks, and verifies outward internet reachability.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import ipaddress
|
|
10
|
+
import platform
|
|
11
|
+
import socket
|
|
12
|
+
import subprocess
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import List, Optional
|
|
15
|
+
import psutil
|
|
16
|
+
import urllib3
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class NetworkAdapter:
|
|
21
|
+
"""Represents a discovered physical or virtual network interface."""
|
|
22
|
+
name: str
|
|
23
|
+
ip_address: str
|
|
24
|
+
netmask: str
|
|
25
|
+
kind: str # "wifi", "usb", "ethernet", "other"
|
|
26
|
+
is_up: bool
|
|
27
|
+
speed_mbps: int = 0
|
|
28
|
+
gateway: Optional[str] = None
|
|
29
|
+
is_online: bool = False
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def display_name(self) -> str:
|
|
33
|
+
kind_label = self.kind.upper()
|
|
34
|
+
return f"{self.name} [{kind_label}] ({self.ip_address})"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def classify_interface(name: str) -> str:
|
|
38
|
+
"""Classifies interface type based on OS naming conventions."""
|
|
39
|
+
name_lower = name.lower()
|
|
40
|
+
if any(k in name_lower for k in ("wi-fi", "wifi", "wireless", "wlan", "802.11")):
|
|
41
|
+
return "wifi"
|
|
42
|
+
if any(k in name_lower for k in ("rndis", "tether", "remote ndis", "apple mobile", "usb", "enx")):
|
|
43
|
+
return "usb"
|
|
44
|
+
if any(k in name_lower for k in ("ethernet", "lan", "eth", "en0", "en1")):
|
|
45
|
+
return "ethernet"
|
|
46
|
+
return "other"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def is_usable_ip(ip_str: str) -> bool:
|
|
50
|
+
"""Checks if an IPv4 address is routable and not loopback/link-local."""
|
|
51
|
+
try:
|
|
52
|
+
ip = ipaddress.IPv4Address(ip_str)
|
|
53
|
+
if ip.is_loopback:
|
|
54
|
+
return False
|
|
55
|
+
if ip.is_link_local: # 169.254.x.x
|
|
56
|
+
return False
|
|
57
|
+
if ip.is_unspecified: # 0.0.0.0
|
|
58
|
+
return False
|
|
59
|
+
return True
|
|
60
|
+
except (ValueError, ipaddress.AddressValueError):
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_adapter_connectivity(ip_address: str, timeout: float = 2.0) -> bool:
|
|
65
|
+
"""Verifies that an outgoing connection bound to this source IP can reach the internet."""
|
|
66
|
+
test_hosts = [("1.1.1.1", 53), ("8.8.8.8", 53)]
|
|
67
|
+
for host, port in test_hosts:
|
|
68
|
+
sock = None
|
|
69
|
+
try:
|
|
70
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
71
|
+
sock.settimeout(timeout)
|
|
72
|
+
# Bind specifically to this adapter's source IP
|
|
73
|
+
sock.bind((ip_address, 0))
|
|
74
|
+
sock.connect((host, port))
|
|
75
|
+
return True
|
|
76
|
+
except (socket.error, OSError):
|
|
77
|
+
continue
|
|
78
|
+
finally:
|
|
79
|
+
if sock:
|
|
80
|
+
sock.close()
|
|
81
|
+
return False
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def discover_adapters(verify_online: bool = True) -> List[NetworkAdapter]:
|
|
85
|
+
"""Discovers all active, routable network adapters."""
|
|
86
|
+
adapters: List[NetworkAdapter] = []
|
|
87
|
+
seen_ips = set()
|
|
88
|
+
|
|
89
|
+
if_addrs = psutil.net_if_addrs()
|
|
90
|
+
if_stats = psutil.net_if_stats()
|
|
91
|
+
|
|
92
|
+
# Exclude common internal virtual switches unless they provide genuine routes
|
|
93
|
+
virtual_keywords = ("vEthernet", "Hyper-V", "VirtualBox", "VMware", "Loopback", "WSL")
|
|
94
|
+
|
|
95
|
+
for iface_name, addrs in if_addrs.items():
|
|
96
|
+
if any(v.lower() in iface_name.lower() for v in virtual_keywords):
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
stats = if_stats.get(iface_name)
|
|
100
|
+
is_up = stats.isup if stats else True
|
|
101
|
+
if not is_up:
|
|
102
|
+
continue
|
|
103
|
+
|
|
104
|
+
for addr in addrs:
|
|
105
|
+
# We focus on IPv4 for multi-adapter bonding
|
|
106
|
+
if addr.family == socket.AF_INET:
|
|
107
|
+
ip = addr.address
|
|
108
|
+
if ip in seen_ips or not is_usable_ip(ip):
|
|
109
|
+
continue
|
|
110
|
+
|
|
111
|
+
seen_ips.add(ip)
|
|
112
|
+
kind = classify_interface(iface_name)
|
|
113
|
+
speed = stats.speed if stats else 0
|
|
114
|
+
|
|
115
|
+
online = False
|
|
116
|
+
if verify_online:
|
|
117
|
+
online = test_adapter_connectivity(ip)
|
|
118
|
+
|
|
119
|
+
adapter = NetworkAdapter(
|
|
120
|
+
name=iface_name,
|
|
121
|
+
ip_address=ip,
|
|
122
|
+
netmask=addr.netmask or "255.255.255.0",
|
|
123
|
+
kind=kind,
|
|
124
|
+
is_up=is_up,
|
|
125
|
+
speed_mbps=speed,
|
|
126
|
+
is_online=online if verify_online else is_up,
|
|
127
|
+
)
|
|
128
|
+
adapters.append(adapter)
|
|
129
|
+
|
|
130
|
+
# Sort so online adapters appear first, prioritized by speed/type
|
|
131
|
+
adapters.sort(key=lambda a: (not a.is_online, a.kind == "other", a.name))
|
|
132
|
+
return adapters
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def get_bonded_pool(min_adapters: int = 1) -> List[NetworkAdapter]:
|
|
136
|
+
"""Returns all verified online adapters ready for bonding."""
|
|
137
|
+
adapters = discover_adapters(verify_online=True)
|
|
138
|
+
online = [a for a in adapters if a.is_online]
|
|
139
|
+
return online if online else adapters
|