Compare commits

..

No commits in common. "af5873f37a8a63ae957a93d9c5e7f0696e853945" and "7e09519e626e058abe5eab9447df74d6ee60ee76" have entirely different histories.

14 changed files with 257 additions and 341 deletions

4
.envrc
View File

@ -1,3 +1 @@
source_url "https://raw.githubusercontent.com/cachix/devenv/82c0147677e510b247d8b9165c54f73d32dfd899/direnvrc" "sha256-7u4iDd1nZpxL4tCzmPG0dQgC5V+/44Ba+tHkPob1v2k="
use devenv
use nix

9
.gitignore vendored
View File

@ -1,10 +1 @@
.direnv
# Devenv
.devenv*
devenv.local.nix
# direnv
.direnv
# pre-commit
.pre-commit-config.yaml

45
add_qbt_trackers.py Executable file
View File

@ -0,0 +1,45 @@
#!/usr/bin/env python3
"""add_qbt_trackers.py
Description:
This script fetches torrent tracker URLs from plaintext lists hosted on the web
and adds them to each torrent in a qBittorrent instance.
Usage:
add_qbt_trackers.py (HOSTNAME) (USERNAME) (PASSWORD)
add_qbt_trackers.py -h
Examples:
add_qbt_trackers.py "http://localhost:8080" "admin" "adminadmin"
Options:
-h, --help show this help message and exit
"""
import requests
from docopt import docopt
from qbittorrent import Client
if __name__ == "__main__":
args = docopt(__doc__) # type: ignore
# Initialize client and login
qb = Client(args["HOSTNAME"])
qb.login(username=args["USERNAME"], password=args["PASSWORD"])
live_trackers_list_urls = [
"https://newtrackon.com/api/stable",
"https://trackerslist.com/best.txt",
"https://trackerslist.com/http.txt",
"https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_best.txt",
]
combined_trackers_urls = []
for url in live_trackers_list_urls:
response = requests.get(url, timeout=60)
tracker_urls = [x for x in response.text.splitlines() if x != ""]
combined_trackers_urls.extend(tracker_urls)
for torrent in qb.torrents():
qb.add_trackers(torrent.get("hash"), "\n".join(combined_trackers_urls)) # type: ignore

View File

@ -1,116 +0,0 @@
{
"nodes": {
"devenv": {
"locked": {
"dir": "src/modules",
"lastModified": 1730745597,
"owner": "cachix",
"repo": "devenv",
"rev": "7cfc04e544e67adf803c3634b53a911c670e046e",
"type": "github"
},
"original": {
"dir": "src/modules",
"owner": "cachix",
"repo": "devenv",
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1696426674,
"owner": "edolstra",
"repo": "flake-compat",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"gitignore": {
"inputs": {
"nixpkgs": [
"pre-commit-hooks",
"nixpkgs"
]
},
"locked": {
"lastModified": 1709087332,
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1716977621,
"owner": "cachix",
"repo": "devenv-nixpkgs",
"rev": "4267e705586473d3e5c8d50299e71503f16a6fb6",
"type": "github"
},
"original": {
"owner": "cachix",
"ref": "rolling",
"repo": "devenv-nixpkgs",
"type": "github"
}
},
"nixpkgs-stable": {
"locked": {
"lastModified": 1730741070,
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "d063c1dd113c91ab27959ba540c0d9753409edf3",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-24.05",
"repo": "nixpkgs",
"type": "github"
}
},
"pre-commit-hooks": {
"inputs": {
"flake-compat": "flake-compat",
"gitignore": "gitignore",
"nixpkgs": [
"nixpkgs"
],
"nixpkgs-stable": "nixpkgs-stable"
},
"locked": {
"lastModified": 1730814269,
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"rev": "d70155fdc00df4628446352fc58adc640cd705c2",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"type": "github"
}
},
"root": {
"inputs": {
"devenv": "devenv",
"nixpkgs": "nixpkgs",
"pre-commit-hooks": "pre-commit-hooks"
}
}
},
"root": "root",
"version": 7
}

View File

@ -1,34 +0,0 @@
{ pkgs, lib, config, inputs, ... }:
{
# https://devenv.sh/basics/
env.GREET = "devenv";
# https://devenv.sh/packages/
packages = [
pkgs.git
pkgs.python312Packages.beautifulsoup4
pkgs.python312Packages.black
pkgs.python312Packages.bpython
pkgs.python312Packages.docopt
pkgs.python312Packages.isort
pkgs.python312Packages.pandas
pkgs.python312Packages.pytest
pkgs.python312Packages.qbittorrent-api
pkgs.python312Packages.requests
pkgs.python312Packages.resend
pkgs.python312Packages.rich
pkgs.python312Packages.tabulate
pkgs.pyright
pkgs.shellcheck
];
# https://devenv.sh/languages/
# languages.rust.enable = true;
languages.python.enable = true;
enterShell = ''
hello
git --version
'';
}

View File

@ -1,15 +0,0 @@
# yaml-language-server: $schema=https://devenv.sh/devenv.schema.json
inputs:
nixpkgs:
url: github:cachix/devenv-nixpkgs/rolling
# If you're using non-OSS software, you can set allowUnfree to true.
# allowUnfree: true
# If you're willing to use a package that's vulnerable
# permittedInsecurePackages:
# - "openssl-1.1.1w"
# If you have more than one devenv you can merge them
#imports:
# - ./backend

View File

@ -15,8 +15,9 @@ Options:
-h, --help show this help message and exit
"""
import qbittorrentapi
from docopt import docopt
from qbittorrent import Client
from tabulate import tabulate
# convert byte units
@ -42,24 +43,35 @@ def human_bytes(input_bytes: int) -> str:
return ""
def print_ssv():
with qbittorrentapi.Client(
host="localhost", port=8080, username="", password=""
) as qbt_client:
try:
qbt_client.auth_log_in()
except qbittorrentapi.LoginFailed as e:
print(e)
sorted_torrents = sorted(
qbt_client.torrents_info(), key=lambda d: d.ratio, reverse=True
)
def print_table():
qb = Client("http://localhost:8080/")
qb.login()
table = []
headers = ["Name", "Total Size", "Trackers Count", "Ratio", "Uploaded"]
sorted_torrents = sorted(qb.torrents(), key=lambda d: d["ratio"], reverse=True) # type: ignore
for torrent in sorted_torrents:
name = torrent.name
size = human_bytes(torrent.total_size)
trackers = torrent.trackers_count
ratio = torrent.ratio
uploaded = human_bytes(torrent.uploaded)
row = []
row.append(torrent["name"]) # type: ignore
row.append(human_bytes(int(torrent["total_size"]))) # type: ignore
row.append(torrent["trackers_count"]) # type: ignore
row.append(torrent["ratio"]) # type: ignore
row.append(human_bytes(int(torrent["uploaded"]))) # type: ignore
table.append(row)
print(tabulate(table, headers, tablefmt="grid"))
def print_ssv():
qb = Client("http://localhost:8080/")
qb.login()
sorted_torrents = sorted(qb.torrents(), key=lambda d: d["ratio"], reverse=True) # type: ignore
print("Name Size Trackers Ratio Uploaded")
for torrent in sorted_torrents:
name = torrent["name"] # type: ignore
size = human_bytes(int(torrent["total_size"])) # type: ignore
trackers = torrent["trackers_count"] # type: ignore
ratio = torrent["ratio"] # type: ignore
uploaded = human_bytes(int(torrent["uploaded"])) # type: ignore
print(f"{name} {size} {trackers} {ratio} {uploaded}")

View File

@ -18,8 +18,8 @@ Options:
-h, --help show this help message and exit
"""
import qbittorrentapi
from docopt import docopt
from qbittorrent import Client
# convert byte units
@ -48,26 +48,21 @@ def human_bytes(bites: int) -> str:
if __name__ == "__main__":
args = docopt(__doc__) # type: ignore
# Initialize client and login
qb = Client(args["HOSTNAME"])
qb.login(username=args["USERNAME"], password=args["PASSWORD"])
# get total_completed_bytes
completed_torrent_sizes = []
total_added_bytes = int()
with qbittorrentapi.Client(
host=args["HOSTNAME"], username=args["USERNAME"], password=args["PASSWORD"]
) as qbt_client:
try:
qbt_client.auth_log_in()
except qbittorrentapi.LoginFailed as e:
print(e)
for torrent in qbt_client.torrents_info():
if torrent.completion_on != 0:
completed_torrent_sizes.append(torrent.total_size)
total_added_bytes = sum(
[torrent.total_size for torrent in qbt_client.torrents_info()]
)
for torrent in qb.torrents():
if torrent["completion_on"] != 0: # type: ignore
completed_torrent_sizes.append(torrent["total_size"]) # type: ignore
total_completed_bytes = sum(completed_torrent_sizes)
# get total_added_bytes
total_added_bytes = sum([torrent["total_size"] for torrent in qb.torrents()]) # type: ignore
# print the results
print(f"\nTotal completed size: {human_bytes(total_completed_bytes)}")
print(f"Total added size: {human_bytes(total_added_bytes)}\n")

102
qbth.py
View File

@ -17,25 +17,15 @@ Options:
import json
import os
import subprocess
from shutil import which
import qbittorrentapi
import requests
from bs4 import BeautifulSoup
from docopt import docopt
from qbittorrent import Client
args = docopt(__doc__)
conn_info = dict(
host=args["HOSTNAME"],
username=args["USERNAME"],
password=args["PASSWORD"],
)
try:
with qbittorrentapi.Client(**conn_info) as qbt_client:
qbt_client.auth_log_in()
except qbittorrentapi.LoginFailed as e:
print(e)
qb = Client(args["HOSTNAME"])
qb.login(username=args["USERNAME"], password=args["PASSWORD"])
def add_torrents(urls: list[str]):
@ -45,16 +35,9 @@ def add_torrents(urls: list[str]):
Params:
urls: list of strings that are URLs.
"""
with qbittorrentapi.Client(**conn_info) as qbt_client:
for url in urls:
response = requests.get(url)
if response.status_code == 200:
if qbt_client.torrents_add(url, category="distro") != "Ok.":
raise Exception("Failed to add torrent: " + os.path.basename(url))
else:
qb.download_from_link(url, category="distro")
print(f"Added {os.path.basename(url)}")
else:
print(f"{response.status_code}: {url}")
def add_torrents_from_html(webpage_url: str, torrent_substring: str):
@ -69,21 +52,11 @@ def add_torrents_from_html(webpage_url: str, torrent_substring: str):
"""
reqs = requests.get(webpage_url, timeout=60)
soup = BeautifulSoup(reqs.text, "html.parser")
with qbittorrentapi.Client(**conn_info) as qbt_client:
for link in soup.find_all("a"):
if torrent_substring in link.get("href"):
url = f"{webpage_url}/{link.get('href')}"
response = requests.get(url)
if response.status_code == 200:
if qbt_client.torrents_add(url, category="distro") != "Ok.":
raise Exception(
"Failed to add torrent: " + os.path.basename(url)
)
else:
print(f"Added {os.path.basename(url)}")
else:
print(f"{response.status_code}: {url}")
qb.download_from_link(url, category="distro")
print(f"Added {link.get('href')}")
def remove_torrents(distro_substring: str):
@ -96,13 +69,10 @@ def remove_torrents(distro_substring: str):
distro_substring: a string that is a substring of the distro
torrent's file name.
"""
with qbittorrentapi.Client(**conn_info) as qbt_client:
for torrent in qbt_client.torrents_info():
if distro_substring in torrent.name:
qbt_client.torrents_delete(
torrent_hashes=torrent.hash, delete_files=True
)
print(f"Removed {torrent.name}")
for torrent in qb.torrents():
if distro_substring in torrent.get("name"): # type: ignore
qb.delete_permanently(torrent.get("hash")) # type: ignore
print(f"Removed {torrent.get('name')}") # type: ignore
def add_almalinux(rel_ver: str):
@ -172,7 +142,8 @@ def add_devuan(rel_ver: str):
relver: the Devuan release version.
"""
url = f"https://files.devuan.org/devuan_{rel_ver}.torrent"
add_torrents([url])
qb.download_from_link(url, category="distro")
print(f"Added {os.path.basename(url)}")
def remove_devuan(rel_ver: str):
@ -204,13 +175,11 @@ def remove_fedora(rel_ver: str):
Params:
relver: the Fedora release version.
"""
with qbittorrentapi.Client(**conn_info) as qbt_client:
for torrent in qbt_client.torrents_info():
if torrent.name.startswith("Fedora") and torrent.name.endswith(rel_ver):
qbt_client.torrents_delete(
torrent_hashes=torrent.hash, delete_files=True
)
print(f"Removed {torrent.name}")
torrents = qb.torrents()
for torrent in torrents:
if torrent.get("name").startswith("Fedora") and torrent.get("name").endswith(rel_ver): # type: ignore
qb.delete_permanently(torrent.get("hash")) # type: ignore
print(f"Removed {torrent.get('name')}") # type: ignore
def add_freebsd(rel_ver: str):
@ -225,12 +194,9 @@ def add_freebsd(rel_ver: str):
reqs = requests.get(url, timeout=60)
data = reqs.text.split("\n")
with qbittorrentapi.Client(**conn_info) as qbt_client:
for line in data:
if line.startswith("magnet:"):
if qbt_client.torrents_add(line) != "Ok.":
raise Exception("Failed to add torrent: " + line.split("=")[2])
qb.download_from_link(line, category="distro")
print(f"Added {line.split('=')[2]}")
@ -300,25 +266,9 @@ def add_nixos():
url = "https://api.github.com/repos/AnimMouse/NixOS-ISO-Torrents/releases/latest"
reqs = requests.get(url, timeout=60)
json_data = json.loads(reqs.text)
with qbittorrentapi.Client(**conn_info) as qbt_client:
for item in json_data["assets"]:
response = requests.get(item["browser_download_url"])
if response.status_code == 200:
if (
qbt_client.torrents_add(
item["browser_download_url"], category="distro"
)
!= "Ok."
):
raise Exception(
"Failed to add torrent: "
+ os.path.basename(item["browser_download_url"])
)
else:
qb.download_from_link(item["browser_download_url"], category="distro")
print(f"Added {os.path.basename(item['browser_download_url'])}")
else:
print(f"{response.status_code}: {item['browser_download_url']}")
def remove_nixos():
@ -339,16 +289,8 @@ def add_qubes(rel_ver: str):
relver: the QubesOS release version.
"""
url = f"https://mirrors.edge.kernel.org/qubes/iso/Qubes-R{rel_ver}-x86_64.torrent"
response = requests.get(url)
if response.status_code == 200:
with qbittorrentapi.Client(**conn_info) as qbt_client:
if qbt_client.torrents_add(url, category="distro") != "Ok.":
raise Exception("Failed to add torrent: " + os.path.basename(url))
else:
qb.download_from_link(url, category="distro")
print(f"Added {os.path.basename(url)}")
else:
print(f"{response.status_code}: {url}")
def remove_qubes(rel_ver: str):
@ -414,10 +356,6 @@ def remove_tails(rel_ver: str):
if __name__ == "__main__":
# Check if gum is installed.
if which("gum") is None:
exit("Please install gum first. https://github.com/charmbracelet/gum")
# Run the gum program in a subprocess to allow easy selecting of distro
# torrents.
distro_selection = subprocess.run(

View File

@ -39,9 +39,9 @@ Options:
import json
import qbittorrentapi
import requests
from docopt import docopt
from qbittorrent import Client
def get_torrent_health_data() -> list[dict]:
@ -62,8 +62,6 @@ def convert_size_to_bytes(size: str) -> int:
Example: 42G --> 45097156608 bytes
"""
total_bytes = int()
if size.endswith("T"):
total_bytes = int(size.split("T")[0]) * (1024**4)
@ -159,14 +157,8 @@ if __name__ == "__main__":
dry_run = args["--dry-run"]
# Initialize client and login
qbt_client = qbittorrentapi.Client(
host=hostname, username=username, password=password
)
try:
qbt_client.auth_log_in()
except qbittorrentapi.LoginFailed as e:
print(e)
qb = Client(hostname)
qb.login(username=username, password=password)
# Fill the knapsack
knapsack = fill_knapsack(max_seeders, knapsack_size, smaller)
@ -191,15 +183,13 @@ if __name__ == "__main__":
for torrent in knapsack:
if "gen.lib.rus.ec" in torrent["link"]:
new_torrent = torrent["link"].replace("gen.lib.rus.ec", "libgen.is")
qbt_client.torrents_add(new_torrent, category="scihub")
qb.download_from_link(new_torrent, category="scihub")
if "libgen.rs" in torrent["link"]:
new_torrent = torrent["link"].replace("libgen.rs", "libgen.is")
qbt_client.torrents_add(new_torrent, category="scihub")
qb.download_from_link(new_torrent, category="scihub")
# print(f"Added {torrent['name']}")
qbt_client.auth_log_out()
print("----------------")
print(f"Count: {len(knapsack)} torrents")
print(f"Total combined size: {get_knapsack_weight(knapsack)}")

63
seed_armbian_torrents.py Executable file
View File

@ -0,0 +1,63 @@
#!/usr/bin/env python3
"""seed_armbian_torrents.py
Description:
Armbian torrents seed script
This script will download Armbian torrent files and add them to a qBittorrent
instance. It's intended to be run under /etc/cron.weekly or used in a systemd
timer.
This is a Python implementation of https://docs.armbian.com/Community_Torrent/
for qBittorrent.
Usage:
seed_armbian_torrents.py (HOSTNAME) (USERNAME) (PASSWORD)
seed_armbian_torrents.py -h
Examples:
seed_armbian_torrents.py "http://localhost:8080" "admin" "adminadmin"
seed_armbian_torrents.py "https://cat.seedhost.eu/lol/qbittorrent" "lol" "pw"
Options:
-h, --help show this help message and exit.
"""
import os
from io import BytesIO
from tempfile import TemporaryDirectory
from zipfile import ZipFile
import requests
from docopt import docopt
from qbittorrent import Client
if __name__ == "__main__":
args = docopt(__doc__)
# Initialize client and login
qb = Client(args["HOSTNAME"])
qb.login(username=args["USERNAME"], password=args["PASSWORD"])
with TemporaryDirectory() as tmp_dir:
req = requests.get(
"https://dl.armbian.com/torrent/all-torrents.zip",
stream=True,
timeout=60,
)
with ZipFile(BytesIO(req.content)) as zip_file:
zip_file.extractall(tmp_dir)
for torrent in qb.torrents():
if "Armbian" in torrent.get("name"): # type: ignore
qb.delete_permanently(torrent.get("hash")) # type: ignore
print(f"Removed {torrent.get('name')}") # type: ignore
torrent_files = os.listdir(tmp_dir)
for torrent_file in torrent_files:
with open(os.path.join(tmp_dir, torrent_file), "rb") as tf:
qb.download_from_file(tf, category="distro")
print(
f"Added {os.path.basename(os.path.join(tmp_dir, torrent_file))}"
)

44
shell.nix Normal file
View File

@ -0,0 +1,44 @@
with import <nixpkgs> { };
let
python-qbittorrent = pkgs.python312Packages.buildPythonPackage rec {
name = "python-qbittorrent-${version}";
version = "0.4.3";
src = pkgs.fetchurl {
url = "https://files.pythonhosted.org/packages/86/25/a5ad35ad229c8016a8c98327495e649cb795be2fda63f8cace6c9a739af7/python-qbittorrent-${version}.tar.gz";
sha256 = "4e22cf89890628b054a60aa4bd1161a68c2b0fad48ef0886fa4d325e69d3828a";
};
meta = {
homepage = "https://github.com/v1k45/python-qBittorrent";
description = "Python wrapper for qBittorrent Web API (for versions above v3.1.x)";
license = lib.licenses.mit;
maintainers = with maintainers; [ v1k45 ];
};
nativeBuildInputs = with pkgs.python312Packages; [
pip
requests
];
};
in
mkShell {
buildInputs = with pkgs; [
python312Packages.beautifulsoup4
python312Packages.black
python312Packages.bpython
python312Packages.docopt
python312Packages.isort
python312Packages.pandas
python312Packages.pytest
python312Packages.requests
python312Packages.resend
python312Packages.rich
python312Packages.tabulate
pyright
python-qbittorrent
shellcheck
];
}

View File

@ -28,8 +28,8 @@ import subprocess
import tempfile
from pathlib import Path
import qbittorrentapi
from docopt import docopt
from qbittorrent import Client
from rich.console import Console
from rich.text import Text
@ -53,18 +53,11 @@ if __name__ == "__main__":
)
torrent_infohashes = []
for item in auth_data["instances"]:
with qbittorrentapi.Client(
host=item["hostname"],
username=item["username"],
password=item["password"],
) as qbt_client:
try:
qbt_client.auth_log_in()
except qbittorrentapi.LoginFailed as e:
print(e)
qb = Client(item["hostname"])
qb.login(username=item["username"], password=item["password"])
for torrent in qbt_client.torrents_info():
torrent_infohashes.append(torrent.hash)
for torrent in qb.torrents():
torrent_infohashes.append(torrent.get("hash")) # type: ignore
# Format the infohashes to have a \n at the end
console.log("Formatting infohashes to have a newline at the end.")
@ -104,16 +97,28 @@ if __name__ == "__main__":
]
)
# Ensure {tracker_domain}:6969/announce is added to each torrent's
# tracker list.
if tracker_domain:
console.log(
f"Ensuring {tracker_domain}:6969/announce is added to each torrent's tracker list."
)
for item in auth_data["instances"]:
qb = Client(item["hostname"])
qb.login(username=item["username"], password=item["password"])
for torrent in qb.torrents():
qb.add_trackers(
torrent.get("hash"), # type: ignore
f"http://{tracker_domain}:6969/announce\nudp://{tracker_domain}:6969/announce",
)
# Reannounce all torrents in each qBittorrent instance to their trackers
console.log("Reannouncing all torrents to their trackers.")
for item in auth_data["instances"]:
with qbittorrentapi.Client(
host=item["hostname"],
username=item["username"],
password=item["password"],
) as qbt_client:
for torrent in qbt_client.torrents_info():
torrent.reannounce(torrent.hash)
qb = Client(item["hostname"])
qb.login(username=item["username"], password=item["password"])
torrent_infohashes = [torrent.get("hash") for torrent in qb.torrents()] # type: ignore
qb.reannounce(torrent_infohashes)
console.log("Done!")