admin-scripts/scihub_knapsack.py

213 lines
7.0 KiB
Python
Executable File

# /// script
# dependencies = [
# "qbittorrent-api",
# "requests",
# "docopt",
# ]
# ///
"""scihub_knapsack.py
Description:
This script will add torrents to a qBittorrent instance until a specified size
limit is reached.
By default, the larger torrents are prioritized in descending order, but the
script can be run with the --smaller flag to prioritize smaller torrents in
ascending order.
The script will select only torrents with less than or equal to <max_seeders>.
Usage:
scihub_knapsack.py [--smaller] [--dry-run] -H <hostname> -U <username> -P <password> -S <size> -s <max_seeders>
scihub_knapsack.py -h
Examples:
scihub_knapsack.py -H http://localhost:8080 -U admin -P adminadmin -S 42T
scihub_knapsack.py --smaller -H https://qbt.hello.world -U admin -P adminadmin -S 2.2T
Options:
--smaller Prioritize from the smallest torrent sizes and work upward
to larger sizes. Default is to prioritize larger sizes.
--dry-run Only print the torrent names, total number of torrents, and
their total combined size instead of adding them to the
qBittorrent instance.
-H <hostname> Hostname of the server where the qBittorrent instance is
running.
-U <username> Username of the user to login to the qBittorrent instance.
-P <password> Password of the user to login to the qBittorrent instance.
-S <size> The maximum size, in GiB or TiB, of the knapsack to add Sci
Hub torrents to. Must be a positive integer or float. Must
have either G or T on the end, which represents GiB or TiB.
-s <max_seeders> Select torrents with less than or equal to <max_seeders>
seeders. <max_seeders> is a positive integer.
"""
import json
import qbittorrentapi
import requests
from docopt import docopt
def get_torrent_health_data() -> list[dict]:
"""
Fetch Sci Hub torrent health checker data from the given URL. The URL
should refer to a JSON-formatted file.
"""
TORRENT_HEALTH_URL = (
"https://zrthstr.github.io/libgen_torrent_cardiography/torrent.json"
)
response = requests.get(TORRENT_HEALTH_URL, timeout=60)
return json.loads(response.text)
def convert_size_to_bytes(size: str) -> int:
"""
Convert the given size string to bytes.
Example: 42G --> 45097156608 bytes
"""
total_bytes = int()
if size.endswith("T"):
total_bytes = int(size.split("T")[0]) * (1024**4)
if size.endswith("G"):
total_bytes = int(size.split("G")[0]) * (1024**3)
return total_bytes
def human_bytes(bites: int) -> str:
"""
Convert bytes to KiB, MiB, GiB, or TiB.
Example: 45097156608 bytes -> 42 GiB
"""
B = float(bites)
KiB = float(1024)
MiB = float(KiB**2)
GiB = float(KiB**3)
TiB = float(KiB**4)
match B:
case B if B < KiB:
return "{0} {1}".format(B, "bytes" if 0 == B > 1 else "byte")
case B if KiB <= B < MiB:
return "{0:.2f} KiB".format(B / KiB)
case B if MiB <= B < GiB:
return "{0:.2f} MiB".format(B / MiB)
case B if GiB <= B < TiB:
return "{0:.2f} GiB".format(B / GiB)
case B if TiB <= B:
return "{0:.2f} TiB".format(B / TiB)
case _:
return ""
def get_knapsack_weight(knapsack: list[dict]) -> str:
"""
Get the weight of the given knapsack in GiB or TiB.
"""
return human_bytes(sum([torrent["size_bytes"] for torrent in knapsack]))
def fill_knapsack(
max_seeders: int, knapsack_size: int, smaller: bool = False
) -> list[dict]:
"""
Fill the knapsack.
Arguments:
max_seeders: int -- Select only torrents with less than or equal to
this number of seeders
knapsack_size: int -- The size in bytes of the knapsack
smaller: bool -- Prioritize smaller sized torrents (Default = False)
Return value:
A list of dictionaries that represent the torrents.
"""
# List of torrents with less than or equal to <max_seeders>
torrents = [t for t in get_torrent_health_data() if t["seeders"] <= max_seeders]
# Sorted list of torrents with <max_seeders>. If smaller == True, sort them
# in ascending order by size_bytes. Else sort them in descending order by
# size_bytes.
sorted_torrents = (
sorted(torrents, key=lambda d: d["size_bytes"])
if smaller == True
else sorted(torrents, key=lambda d: d["size_bytes"], reverse=True)
)
# Sum the sizes of each torrent in sorted_torrents and add them to the
# knapsack until it is filled, then return the knapsack.
sum = 0
knapsack = []
for torrent in sorted_torrents:
if sum + torrent["size_bytes"] >= knapsack_size:
break
sum += torrent["size_bytes"]
knapsack.append(torrent)
return knapsack
if __name__ == "__main__":
args = docopt(__doc__) # type: ignore
hostname = args["-H"]
username = args["-U"]
password = args["-P"]
max_seeders = int(args["-s"])
knapsack_size = convert_size_to_bytes(args["-S"])
smaller = args["--smaller"]
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)
# Fill the knapsack
knapsack = fill_knapsack(max_seeders, knapsack_size, smaller)
# If it's a dry run, only print the knapsack's contents. Otherwise,
# add the knapsack's contents to the qBittorrent instance.
# When finished, print the number of items and the combined weight of all
# items in the knapsack. Before attempting to add items to the qBittorrent
# instance, check to see if libgen.rs is even working. If libgen.rs is down
# no torrents can be added to the qBittorrent instance, so exit with an
# notice.
if dry_run:
for torrent in knapsack:
print(torrent["link"])
else:
response = requests.get("https://libgen.is/")
if not response.ok:
exit(
"It appears https://libgen.is is currently down. Please try again later."
)
for torrent in knapsack:
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")
if "libgen.rs" in torrent["link"]:
new_torrent = torrent["link"].replace("libgen.rs", "libgen.is")
qbt_client.torrents_add(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)}")
print("----------------")