Assorted shell and Python scripts
1#!/usr/bin/env -S uv run --script 2# /// script 3# dependencies = [ 4# "qbittorrent-api", 5# "requests", 6# "docopt", 7# ] 8# /// 9 10"""scihub_knapsack.py 11 12Description: 13This script will add torrents to a qBittorrent instance until a specified size 14limit is reached. 15 16By default, the larger torrents are prioritized in descending order, but the 17script can be run with the --smaller flag to prioritize smaller torrents in 18ascending order. 19 20The script will select only torrents with less than or equal to <max_seeders>. 21 22Usage: 23 scihub_knapsack.py [--smaller] [--dry-run] -H <hostname> -U <username> -P <password> -S <size> -s <max_seeders> 24 scihub_knapsack.py -h 25 26Examples: 27 scihub_knapsack.py -H http://localhost:8080 -U admin -P adminadmin -S 42T 28 scihub_knapsack.py --smaller -H https://qbt.hello.world -U admin -P adminadmin -S 2.2T 29 30Options: 31 --smaller Prioritize from the smallest torrent sizes and work upward 32 to larger sizes. Default is to prioritize larger sizes. 33 --dry-run Only print the torrent names, total number of torrents, and 34 their total combined size instead of adding them to the 35 qBittorrent instance. 36 -H <hostname> Hostname of the server where the qBittorrent instance is 37 running. 38 -U <username> Username of the user to login to the qBittorrent instance. 39 -P <password> Password of the user to login to the qBittorrent instance. 40 -S <size> The maximum size, in GiB or TiB, of the knapsack to add Sci 41 Hub torrents to. Must be a positive integer or float. Must 42 have either G or T on the end, which represents GiB or TiB. 43 -s <max_seeders> Select torrents with less than or equal to <max_seeders> 44 seeders. <max_seeders> is a positive integer. 45""" 46 47import json 48 49import qbittorrentapi 50import requests 51from docopt import docopt 52 53 54def get_torrent_health_data() -> list[dict]: 55 """ 56 Fetch Sci Hub torrent health checker data from the given URL. The URL 57 should refer to a JSON-formatted file. 58 """ 59 TORRENT_HEALTH_URL = ( 60 "https://zrthstr.github.io/libgen_torrent_cardiography/torrent.json" 61 ) 62 response = requests.get(TORRENT_HEALTH_URL, timeout=60) 63 return json.loads(response.text) 64 65 66def convert_size_to_bytes(size: str) -> int: 67 """ 68 Convert the given size string to bytes. 69 70 Example: 42G --> 45097156608 bytes 71 """ 72 total_bytes = int() 73 74 if size.endswith("T"): 75 total_bytes = int(size.split("T")[0]) * (1024**4) 76 77 if size.endswith("G"): 78 total_bytes = int(size.split("G")[0]) * (1024**3) 79 80 return total_bytes 81 82 83def human_bytes(bites: int) -> str: 84 """ 85 Convert bytes to KiB, MiB, GiB, or TiB. 86 87 Example: 45097156608 bytes -> 42 GiB 88 """ 89 B = float(bites) 90 KiB = float(1024) 91 MiB = float(KiB**2) 92 GiB = float(KiB**3) 93 TiB = float(KiB**4) 94 95 match B: 96 case B if B < KiB: 97 return "{0} {1}".format(B, "bytes" if 0 == B > 1 else "byte") 98 case B if KiB <= B < MiB: 99 return "{0:.2f} KiB".format(B / KiB) 100 case B if MiB <= B < GiB: 101 return "{0:.2f} MiB".format(B / MiB) 102 case B if GiB <= B < TiB: 103 return "{0:.2f} GiB".format(B / GiB) 104 case B if TiB <= B: 105 return "{0:.2f} TiB".format(B / TiB) 106 case _: 107 return "" 108 109 110def get_knapsack_weight(knapsack: list[dict]) -> str: 111 """ 112 Get the weight of the given knapsack in GiB or TiB. 113 """ 114 return human_bytes(sum([torrent["size_bytes"] for torrent in knapsack])) 115 116 117def fill_knapsack( 118 max_seeders: int, knapsack_size: int, smaller: bool = False 119) -> list[dict]: 120 """ 121 Fill the knapsack. 122 123 Arguments: 124 max_seeders: int -- Select only torrents with less than or equal to 125 this number of seeders 126 knapsack_size: int -- The size in bytes of the knapsack 127 smaller: bool -- Prioritize smaller sized torrents (Default = False) 128 129 Return value: 130 A list of dictionaries that represent the torrents. 131 """ 132 133 # List of torrents with less than or equal to <max_seeders> 134 torrents = [t for t in get_torrent_health_data() if t["seeders"] <= max_seeders] 135 136 # Sorted list of torrents with <max_seeders>. If smaller == True, sort them 137 # in ascending order by size_bytes. Else sort them in descending order by 138 # size_bytes. 139 sorted_torrents = ( 140 sorted(torrents, key=lambda d: d["size_bytes"]) 141 if smaller == True 142 else sorted(torrents, key=lambda d: d["size_bytes"], reverse=True) 143 ) 144 145 # Sum the sizes of each torrent in sorted_torrents and add them to the 146 # knapsack until it is filled, then return the knapsack. 147 sum = 0 148 knapsack = [] 149 for torrent in sorted_torrents: 150 if sum + torrent["size_bytes"] >= knapsack_size: 151 break 152 sum += torrent["size_bytes"] 153 knapsack.append(torrent) 154 155 return knapsack 156 157 158if __name__ == "__main__": 159 args = docopt(__doc__) # type: ignore 160 hostname = args["-H"] 161 username = args["-U"] 162 password = args["-P"] 163 max_seeders = int(args["-s"]) 164 knapsack_size = convert_size_to_bytes(args["-S"]) 165 smaller = args["--smaller"] 166 dry_run = args["--dry-run"] 167 168 # Initialize client and login 169 qbt_client = qbittorrentapi.Client( 170 host=hostname, username=username, password=password 171 ) 172 173 try: 174 qbt_client.auth_log_in() 175 except qbittorrentapi.LoginFailed as e: 176 print(e) 177 178 # Fill the knapsack 179 knapsack = fill_knapsack(max_seeders, knapsack_size, smaller) 180 181 # If it's a dry run, only print the knapsack's contents. Otherwise, 182 # add the knapsack's contents to the qBittorrent instance. 183 # When finished, print the number of items and the combined weight of all 184 # items in the knapsack. Before attempting to add items to the qBittorrent 185 # instance, check to see if libgen.rs is even working. If libgen.rs is down 186 # no torrents can be added to the qBittorrent instance, so exit with an 187 # notice. 188 if dry_run: 189 for torrent in knapsack: 190 print(torrent["link"]) 191 else: 192 response = requests.get("https://libgen.is/") 193 if not response.ok: 194 exit( 195 "It appears https://libgen.is is currently down. Please try again later." 196 ) 197 for torrent in knapsack: 198 for torrent in knapsack: 199 if "gen.lib.rus.ec" in torrent["link"]: 200 new_torrent = torrent["link"].replace("gen.lib.rus.ec", "libgen.is") 201 qbt_client.torrents_add(new_torrent, category="scihub") 202 203 if "libgen.rs" in torrent["link"]: 204 new_torrent = torrent["link"].replace("libgen.rs", "libgen.is") 205 qbt_client.torrents_add(new_torrent, category="scihub") 206 # print(f"Added {torrent['name']}") 207 208 qbt_client.auth_log_out() 209 210 print("----------------") 211 print(f"Count: {len(knapsack)} torrents") 212 print(f"Total combined size: {get_knapsack_weight(knapsack)}") 213 print("----------------")