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