#!/usr/bin/env python3 """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 . Usage: scihub_knapsack.py [--smaller] [--dry-run] -H -U -P -S -s 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 of the server where the qBittorrent instance is running. -U Username of the user to login to the qBittorrent instance. -P Password of the user to login to the qBittorrent instance. -S 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 Select torrents with less than or equal to 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 torrents = [t for t in get_torrent_health_data() if t["seeders"] <= max_seeders] # Sorted list of torrents with . 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("----------------")