First commit
This commit is contained in:
+165
@@ -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/
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
@@ -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"},
|
||||
]
|
||||
Reference in New Issue
Block a user