First commit

This commit is contained in:
2026-03-11 18:31:39 +01:00 Unverified
commit 369ef93d66
7 changed files with 605 additions and 0 deletions
+165
View File
@@ -0,0 +1,165 @@
# Custom
*.cookies
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
+57
View File
@@ -0,0 +1,57 @@
# UNIT3D Seedbox Manager
For when you have a non-static IP on your seedbox and this is a script that can automagically manage it for you.
## Usage
```cmd
seedbox.py tracker username [ip] [options]
```
> It is up to **you** as user to fill in the `tracker.py` file with all your own trackers. There are two trackers in the file as an example.
## Requirements
You will need a `Netscape HTTP Cookie File` with the cookie `remember_web_*` from the UNIT3D tracker of your choice. Basically, login and then save the cookies to a file called `unit3d.cookies` and place it in the script folder or use the `--cookie-file` option with a custom file.
## All options
- `--help`, `--print | --no-print`, `--seedbox-name`, `--delete | --no-delete`, `--cookie-file`
## Example
```cmd
python ./seedbox.py TRACKER USERNAME 1.1.1.2 --no-delete --seedbox-name SeedExample1 --print
| Name IP ID
| -----------------------------------------------------
| MainBox 192.168.12.234 1448
| SeedExample 1.1.1.1 1455
Successfully added Seedbox "SeedExample1" (1.1.1.2) to TRACKER
```
## Help
```cmd
usage: seedbox.py tracker username [ip] [options]
Automagically add or remove seedbox entries on any UNIT3D
positional arguments:
tracker Tracker name or abbreviation.
username username
ip IP of the seedbox
options:
-h, --help show this help message and exit
--print, --no-print Prints list of seedboxes (default: False)
--seedbox-name SEEDBOX_NAME
Name of the seedbox (default: PikminBox)
--delete, --no-delete
Deletes all old seedbox entries (default: False)
--cookie-file COOKIE_FILE
Cookies to be used when making requests (default: ./unit3d.cookies)
For bugs/errors, please DM me on FnP or make an issue on GitHub
```
+17
View File
@@ -0,0 +1,17 @@
#!/bin/bash
# 1. Update qBittorrent port
python3 /pia/scripts/qbitport.py "$1"
# 2. Get WireGuard IP
ip_adress=$(wg show | grep 'endpoint: ' | awk -F ':' '{print $2}' | xargs)
# 3. Update Unit3D seedboxes
trackers=("fnp" "sp" "rs" "ulcx" "dps" "lum")
for tracker in "${trackers[@]}"; do
python3 /pia/scripts/seedbox.py "$tracker" swedish_wiking "$ip_adress" --delete --seedbox-name MainFrame
done
# 4. Update MaM seedbox
python3 /pia/scripts/mam_seedbox.py
+73
View File
@@ -0,0 +1,73 @@
# mam_seedbox.py
import json
import urllib.error
import urllib.request
from http.cookies import SimpleCookie
from pathlib import Path
__location__ = Path(__file__).parent.resolve()
cookie_file = __location__.joinpath("mam_id")
api_url = "https://t.myanonamouse.net/json/dynamicSeedbox.php"
def save_cookie(headers):
set_cookie_header = headers.get("Set-Cookie")
if set_cookie_header:
cookie = SimpleCookie(set_cookie_header)
if "mam_id" in cookie:
cookie_file.write_text(cookie["mam_id"].value)
def load_cookie() -> str:
if not cookie_file.exists():
print("Warning: mam_id file not found. Proceeding without it...", flush=True)
return ""
mam_id = cookie_file.read_text().strip()
return f"mam_id={mam_id}"
def main():
print("Updating MaM Dynamic Seedbox IP...", flush=True)
req = urllib.request.Request(api_url)
cookie_val = load_cookie()
if cookie_val:
req.add_header("Cookie", cookie_val)
status_code = None
data = {}
headers = {}
try:
with urllib.request.urlopen(req) as response:
status_code = response.status
data = json.loads(response.read().decode("utf-8"))
headers = response.headers
except urllib.error.HTTPError as e:
status_code = e.code
try:
data = json.loads(e.read().decode("utf-8"))
except json.JSONDecodeError:
data = {"msg": "Unknown server error"}
except urllib.error.URLError as e:
print(f"Failed to connect to MaM: {e.reason}", flush=True)
return
msg = data.get("msg", "No message provided")
match status_code:
case 200:
print(f"Successfully updated MaM seedbox: {msg}", flush=True)
save_cookie(headers)
case 429:
print(f"Failed to update MaM seedbox (Rate Limited): {msg}", flush=True)
case 403:
print(f"Failed to update MaM seedbox (Forbidden): {msg}", flush=True)
case _:
print(f"Something went VERY wrong... HTTP {status_code}: {msg}", flush=True)
if __name__ == "__main__":
main()
+81
View File
@@ -0,0 +1,81 @@
# qbitport.py
import argparse
import json
import time
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path
parser = argparse.ArgumentParser(
prog="qBittorrent Port Manager",
description="Update your qBittorrent port",
usage=f"{Path(__file__).name} port [options]",
)
parser.add_argument("port", type=str, help="Forwarding Port")
parser.add_argument(
"--host",
type=str,
default="localhost",
help="IP/hostname of qBittorrent instance (default: %(default)s)",
)
parser.add_argument(
"--host-port",
type=int,
default=8080,
help="qBittorrent port (default: %(default)s)",
)
args = parser.parse_args()
def ping_until_connected(url, max_retries=12):
req = urllib.request.Request(url, method="HEAD")
retries = 0
while retries < max_retries:
try:
with urllib.request.urlopen(req, timeout=5) as response:
if response.status == 200:
return True
except (urllib.error.URLError, TimeoutError):
print(
f"{url} is unreachable, trying again in 5 sec... ({retries + 1}/{max_retries})",
flush=True,
)
time.sleep(5)
retries += 1
print("Error: Could not connect to qBittorrent after maximum retries.", flush=True)
exit(1)
def main():
base_url = f"http://{args.host}:{args.host_port}"
print("Establishing connection with qBittorrent...", flush=True)
ping_until_connected(base_url)
print("Connection established", flush=True)
print(f"Setting qBittorrent port settings ({args.port})...", flush=True)
prefs_data = urllib.parse.urlencode(
{"json": json.dumps({"listen_port": int(args.port)})}
).encode("utf-8")
prefs_req = urllib.request.Request(
f"{base_url}/api/v2/app/setPreferences", data=prefs_data
)
try:
with urllib.request.urlopen(prefs_req, timeout=10) as response:
if response.status == 200:
print("Port has been set!", flush=True)
except urllib.error.HTTPError as e:
print(
f"HTTP Error {e.code}: Failed to set port. (Is auth bypass enabled in qBittorrent?)",
flush=True,
)
if __name__ == "__main__":
main()
+201
View File
@@ -0,0 +1,201 @@
# seedbox.py
import argparse
import re
import urllib.parse
import urllib.request
from pathlib import Path
from trackers import trackers
parser = argparse.ArgumentParser(
prog="Unit3D seedbox manager",
description="Automagically add or remove seedbox entries on any Unit3D",
usage=f"{Path(__file__).name} tracker username [ip] [options] ",
epilog="For bugs/errors, please DM me on FnP or make an issue on GitHub",
)
parser.add_argument("tracker", type=str, help="Tracker")
parser.add_argument("username", type=str, help="Username")
parser.add_argument("ip", nargs="?", type=str, help="IP of the seedbox")
parser.add_argument(
"--print",
default=False,
action=argparse.BooleanOptionalAction,
help="Prints list of seedboxes",
)
parser.add_argument(
"--seedbox-name",
type=str,
default="PikminBox",
help="Name of the seedbox (default: %(default)s)",
)
parser.add_argument(
"--delete",
default=False,
action=argparse.BooleanOptionalAction,
help="Deletes all old seedbox entries",
)
parser.add_argument(
"--cookie-file",
type=Path,
default=Path(__file__).parent.joinpath("unit3d.cookies"),
help="Cookies to be used",
)
parsed_args = parser.parse_args()
COOKIE_HEADER = ""
def generateHeaders(host: str) -> dict:
headers = {
"Host": host,
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.5",
"Origin": f"https://{host}",
"DNT": "1",
"Sec-GPC": "1",
"Connection": "keep-alive",
"Upgrade-Insecure-Requests": "1",
}
if COOKIE_HEADER:
headers["Cookie"] = COOKIE_HEADER
return headers
def parseTracker(tracker: str) -> dict:
for t in trackers:
if tracker.lower() in [x.lower() for x in t.values()]:
return t
print(f'No tracker in database matched "{tracker}"')
exit(1)
def parseCookieFile(file: Path, domain: str) -> None:
global COOKIE_HEADER
with open(file, "r") as f:
for line in f:
if not re.match(r"^\#", line) and not re.match(r"^\s*$", line):
lineFields = line.strip().split("\t")
if domain in lineFields[0]:
COOKIE_HEADER = f"{lineFields[5]}={lineFields[6]}"
return
print(f"No remember_web_* cookie was found in {file}")
exit(1)
def getSeedboxes(domain: str) -> tuple[list[dict], str]:
try:
req = urllib.request.Request(
f"https://{domain}/users/{parsed_args.username}/seedboxes",
headers=generateHeaders(domain),
)
with urllib.request.urlopen(req) as resp:
html = resp.read().decode("utf-8")
token_match = re.search(
r'<meta\s+name="csrf-token"\s+content="([^"]+)"', html, re.IGNORECASE
)
if not token_match:
raise Exception("Could not find CSRF token on the page.")
add_token = token_match.group(1)
pattern = re.compile(
r'<td>\s*(.*?)\s*</td>\s*<td>\s*(.*?)\s*</td>.*?action="[^"]+/seedboxes/(\d+)".*?name="_token"\s+value="([^"]+)"',
re.DOTALL | re.IGNORECASE,
)
seedboxes = [
{
"name": m.group(1).strip(),
"ip": m.group(2).strip(),
"id": m.group(3),
"token": m.group(4),
}
for m in pattern.finditer(html)
]
return seedboxes, add_token
except Exception as e:
print(f"Error while getting seedboxes: {e}", flush=True)
exit(1)
def addSeedbox(domain: str, token: str) -> bool:
try:
data = urllib.parse.urlencode(
{
"_token": token,
"name": parsed_args.seedbox_name,
"ip": parsed_args.ip,
}
).encode("utf-8")
req = urllib.request.Request(
f"https://{domain}/users/{parsed_args.username}/seedboxes",
data=data,
headers=generateHeaders(domain),
)
with urllib.request.urlopen(req) as resp:
html = resp.read().decode("utf-8")
error_match = re.search(
r'<div[^>]*id="ERROR_COPY"[^>]*>(.*?)</div>',
html,
re.DOTALL | re.IGNORECASE,
)
if error_match:
clean_error = re.sub(r"<[^>]+>", "", error_match.group(1))
error_msg = " ".join(
[x.strip() for x in clean_error.split("\n") if x.strip()]
)
raise Exception(error_msg)
return True
except Exception as e:
print("Failed to add seedbox", flush=True)
print(f"Error: {e}", flush=True)
return False
def main():
tracker = parseTracker(parsed_args.tracker)
domain = tracker["domain"]
parseCookieFile(parsed_args.cookie_file, domain)
seedboxes, add_token = getSeedboxes(domain)
if parsed_args.print:
print(f"Tracker: {tracker['name']}")
print(f"\n| {'Name':<25}{'IP':<20}{'ID':<5}")
print("| " + "-" * 53)
for sbox in seedboxes:
print(f"| {sbox['name']:<25}{sbox['ip']:<20}{sbox['id']:<5}")
print()
if parsed_args.delete:
for sbox in seedboxes:
print(f'Deleting Seedbox: "{sbox["name"]}" ({sbox["ip"]})', flush=True)
data = urllib.parse.urlencode(
{"_token": sbox["token"], "_method": "DELETE"}
).encode("utf-8")
req = urllib.request.Request(
f"https://{domain}/users/{parsed_args.username}/seedboxes/{sbox['id']}",
data=data,
headers=generateHeaders(domain),
)
urllib.request.urlopen(req)
if parsed_args.ip:
if addSeedbox(domain, add_token):
print(
f"Successfully added Seedbox \"{parsed_args.seedbox_name}\" ({parsed_args.ip}) to {tracker['name']}",
flush=True,
)
if __name__ == "__main__":
main()
+11
View File
@@ -0,0 +1,11 @@
# trackers.py
trackers = [
{"name": "FearNoPeer", "abbr": "FNP", "domain": "fearnopeer.com"},
{"name": "SeedPool", "abbr": "SP", "domain": "seedpool.org"},
{"name": "Rastastugan", "abbr": "RS", "domain": "rastastugan.org"},
{"name": "ULCX", "abbr": "ULCX", "domain": "upload.cx"},
{"name": "Darkpeers", "abbr": "DPS", "domain": "darkpeers.org"},
{"name": "Luminarr", "abbr": "LUM", "domain": "luminarr.me"},
]