/ qbth.py
qbth.py
1 # /// script 2 # dependencies = [ 3 # "qbittorrent-api", 4 # "requests", 5 # "bs4", 6 # "docopt", 7 # ] 8 # /// 9 10 """qbth.py - qbittorrent helper 11 12 Usage: 13 qbth.py (HOSTNAME) (USERNAME) (PASSWORD) 14 qbth.py -h 15 16 Examples: 17 qbth.py "http://localhost:8080" "admin" "adminadmin" 18 qbth.py "https://cat.seedhost.eu/lol/qbittorrent" "lol" "meow" 19 20 Options: 21 -h, --help show this help message and exit 22 """ 23 24 import json 25 import os 26 import subprocess 27 from shutil import which 28 29 import qbittorrentapi 30 import requests 31 from bs4 import BeautifulSoup 32 from docopt import docopt 33 34 args = docopt(__doc__) 35 conn_info = dict( 36 host=args["HOSTNAME"], 37 username=args["USERNAME"], 38 password=args["PASSWORD"], 39 ) 40 41 try: 42 with qbittorrentapi.Client(**conn_info) as qbt_client: 43 qbt_client.auth_log_in() 44 except qbittorrentapi.LoginFailed as e: 45 print(e) 46 47 48 def add_torrents(urls: list[str]): 49 """ 50 Add torrents from their URLs. 51 52 Params: 53 urls: list of strings that are URLs. 54 """ 55 with qbittorrentapi.Client(**conn_info) as qbt_client: 56 for url in urls: 57 response = requests.get(url) 58 if response.status_code == 200: 59 if qbt_client.torrents_add(url, category="distro") != "Ok.": 60 raise Exception("Failed to add torrent: " + os.path.basename(url)) 61 else: 62 print(f"Added {os.path.basename(url)}") 63 else: 64 print(f"{response.status_code}: {url}") 65 66 67 def add_torrents_from_html(webpage_url: str, torrent_substring: str): 68 """ 69 Add torrents from an HTML web page. 70 71 Params: 72 webpage_url: a string that is the URL for the desired webpage. 73 torrent_substring: a string that is a substring of the URLs in 74 the webpage that you want to extract. It serves as a 75 selector. 76 """ 77 reqs = requests.get(webpage_url, timeout=60) 78 soup = BeautifulSoup(reqs.text, "html.parser") 79 with qbittorrentapi.Client(**conn_info) as qbt_client: 80 for link in soup.find_all("a"): 81 if torrent_substring in link.get("href"): 82 url = f"{webpage_url}/{link.get('href')}" 83 response = requests.get(url) 84 if response.status_code == 200: 85 if qbt_client.torrents_add(url, category="distro") != "Ok.": 86 raise Exception( 87 "Failed to add torrent: " + os.path.basename(url) 88 ) 89 else: 90 print(f"Added {os.path.basename(url)}") 91 92 else: 93 print(f"{response.status_code}: {url}") 94 95 96 def remove_torrents(distro_substring: str): 97 """ 98 Remove torrents by selecting a substring that corresponds to the 99 distro's torrent file name. When the substring is found, the 100 torrent is removed by passing the corresponding hash to the method. 101 102 Params: 103 distro_substring: a string that is a substring of the distro 104 torrent's file name. 105 """ 106 with qbittorrentapi.Client(**conn_info) as qbt_client: 107 for torrent in qbt_client.torrents_info(): 108 if distro_substring in torrent.name: 109 qbt_client.torrents_delete( 110 torrent_hashes=torrent.hash, delete_files=True 111 ) 112 print(f"Removed {torrent.name}") 113 114 115 def add_almalinux(rel_ver: str): 116 """ 117 Add AlmaLinux torrents from a list of URLs. These URLs are partially 118 hardcoded for convenience and aren't expected to change frequently. 119 120 Params: 121 relver: the AlmaLinux release version. 122 """ 123 urls = [ 124 f"https://almalinux-mirror.dal1.hivelocity.net/{rel_ver}/isos/aarch64/AlmaLinux-{rel_ver}-aarch64.torrent", 125 f"https://almalinux-mirror.dal1.hivelocity.net/{rel_ver}/isos/ppc64le/AlmaLinux-{rel_ver}-ppc64le.torrent", 126 f"https://almalinux-mirror.dal1.hivelocity.net/{rel_ver}/isos/s390x/AlmaLinux-{rel_ver}-s390x.torrent", 127 f"https://almalinux-mirror.dal1.hivelocity.net/{rel_ver}/isos/x86_64/AlmaLinux-{rel_ver}-x86_64.torrent", 128 ] 129 130 add_torrents(urls) 131 132 133 def remove_almalinux(rel_ver: str): 134 """ 135 Remove AlmaLinux torrents given their release version. 136 137 Params: 138 relver: the AlmaLinux release version. 139 """ 140 remove_torrents(f"AlmaLinux-{rel_ver}") 141 142 143 def add_debian(rel_ver: str): 144 """ 145 Add Debian torrents from a list of URLs. 146 147 Params: 148 relver: the Debian release version. 149 """ 150 urls = [ 151 f"https://cdimage.debian.org/debian-cd/current/amd64/bt-dvd/debian-{rel_ver}-amd64-DVD-1.iso.torrent", 152 f"https://cdimage.debian.org/debian-cd/current/arm64/bt-dvd/debian-{rel_ver}-arm64-DVD-1.iso.torrent", 153 f"https://cdimage.debian.org/debian-cd/current/armel/bt-dvd/debian-{rel_ver}-armel-DVD-1.iso.torrent", 154 f"https://cdimage.debian.org/debian-cd/current/armhf/bt-dvd/debian-{rel_ver}-armhf-DVD-1.iso.torrent", 155 f"https://cdimage.debian.org/debian-cd/current/mips64el/bt-dvd/debian-{rel_ver}-mips64el-DVD-1.iso.torrent", 156 f"https://cdimage.debian.org/debian-cd/current/mipsel/bt-dvd/debian-{rel_ver}-mipsel-DVD-1.iso.torrent", 157 f"https://cdimage.debian.org/debian-cd/current/ppc64el/bt-dvd/debian-{rel_ver}-ppc64el-DVD-1.iso.torrent", 158 f"https://cdimage.debian.org/debian-cd/current/s390x/bt-dvd/debian-{rel_ver}-s390x-DVD-1.iso.torrent", 159 ] 160 161 add_torrents(urls) 162 163 164 def remove_debian(rel_ver: str): 165 """ 166 Remove Debian torrents given their release version. 167 168 Params: 169 relver: the Debian release version. 170 """ 171 remove_torrents(f"debian-{rel_ver}") 172 173 174 def add_devuan(rel_ver: str): 175 """ 176 Add Devuan torrents from a URL. 177 178 Params: 179 relver: the Devuan release version. 180 """ 181 url = f"https://files.devuan.org/devuan_{rel_ver}.torrent" 182 add_torrents([url]) 183 184 185 def remove_devuan(rel_ver: str): 186 """ 187 Remove Devuan torrents given their release version. 188 189 Params: 190 relver: the Devuan release version. 191 """ 192 remove_torrents(f"devuan_{rel_ver}") 193 194 195 def add_fedora(rel_ver: str): 196 """ 197 Add Fedora torrents from URLs extracted from a webpage. 198 199 Params: 200 relver: the Fedora release version. 201 """ 202 webpage_url = "https://torrent.fedoraproject.org/torrents" 203 torrent_substring = f"{rel_ver}.torrent" 204 add_torrents_from_html(webpage_url, torrent_substring) 205 206 207 def remove_fedora(rel_ver: str): 208 """ 209 Remove Fedora torrents given their release version. 210 211 Params: 212 relver: the Fedora release version. 213 """ 214 with qbittorrentapi.Client(**conn_info) as qbt_client: 215 for torrent in qbt_client.torrents_info(): 216 if torrent.name.startswith("Fedora") and torrent.name.endswith(rel_ver): 217 qbt_client.torrents_delete( 218 torrent_hashes=torrent.hash, delete_files=True 219 ) 220 print(f"Removed {torrent.name}") 221 222 223 def add_freebsd(rel_ver: str): 224 """ 225 Add FreeBSD torrents via a text file on the web that contains their 226 magnet links. 227 228 Params: 229 relver: the FreeBSD release version. 230 """ 231 url = f"https://people.freebsd.org/~jmg/FreeBSD-{rel_ver}-R-magnet.txt" 232 reqs = requests.get(url, timeout=60) 233 data = reqs.text.split("\n") 234 235 with qbittorrentapi.Client(**conn_info) as qbt_client: 236 for line in data: 237 if line.startswith("magnet:"): 238 if qbt_client.torrents_add(line) != "Ok.": 239 raise Exception("Failed to add torrent: " + line.split("=")[2]) 240 241 print(f"Added {line.split('=')[2]}") 242 243 244 def remove_freebsd(rel_ver: str): 245 """ 246 Remove FreeBSD torrents given their release version. 247 248 Params: 249 relver: the FreeBSD release version. 250 """ 251 remove_torrents(f"FreeBSD-{rel_ver}") 252 253 254 def add_kali(): 255 """ 256 Add Kali Linux torrents from their URLs extracted from a webpage. 257 This method does not accept any parameters. The latest Kali Linux 258 version is automatically selected. 259 260 Params: none 261 """ 262 webpage_url = "https://kali.download/base-images/current" 263 torrent_substring = ".torrent" 264 add_torrents_from_html(webpage_url, torrent_substring) 265 266 267 def remove_kali(): 268 """ 269 Remove Kali Linux torrents. This method does not accept any parameters. 270 All Kali Linux torrents in the qBittorrent instance will be removed. 271 272 Params: none 273 """ 274 remove_torrents("kali-linux") 275 276 277 def add_netbsd(rel_ver: str): 278 """ 279 Add NetBSD torrents from their URLs extracted from a webpage. 280 281 Params: 282 relver: the NetBSD release version. 283 """ 284 webpage_url = f"https://cdn.netbsd.org/pub/NetBSD/NetBSD-{rel_ver}/images/" 285 torrent_substring = ".torrent" 286 add_torrents_from_html(webpage_url, torrent_substring) 287 288 289 def remove_netbsd(rel_ver: str): 290 """ 291 Remove NetBSD torrents given their release version. 292 293 Params: 294 relver: the NetBSD release version. 295 """ 296 remove_torrents(f"NetBSD-{rel_ver}") 297 298 299 def add_nixos(): 300 """ 301 Add NixOS torrents from their GitHub release at 302 https://github.com/AninMouse/NixOS-ISO-Torrents. This method does not 303 accept any paramters. The latest NixOS torrent is automatically selected. 304 305 Params: none 306 """ 307 url = "https://api.github.com/repos/AnimMouse/NixOS-ISO-Torrents/releases/latest" 308 reqs = requests.get(url, timeout=60) 309 json_data = json.loads(reqs.text) 310 311 with qbittorrentapi.Client(**conn_info) as qbt_client: 312 for item in json_data["assets"]: 313 response = requests.get(item["browser_download_url"]) 314 if response.status_code == 200: 315 if ( 316 qbt_client.torrents_add( 317 item["browser_download_url"], category="distro" 318 ) 319 != "Ok." 320 ): 321 raise Exception( 322 "Failed to add torrent: " 323 + os.path.basename(item["browser_download_url"]) 324 ) 325 else: 326 print(f"Added {os.path.basename(item['browser_download_url'])}") 327 else: 328 print(f"{response.status_code}: {item['browser_download_url']}") 329 330 331 def remove_nixos(): 332 """ 333 Remove NixOS torrents. This method does not accept any parameters. All 334 NixOS torrents in the qBittorrent instance will be removed. 335 336 Params: none 337 """ 338 remove_torrents("nixos") 339 340 341 def add_qubes(rel_ver: str): 342 """ 343 Add QubesOS torrents from their URLs. 344 345 Params: 346 relver: the QubesOS release version. 347 """ 348 url = f"https://mirrors.edge.kernel.org/qubes/iso/Qubes-R{rel_ver}-x86_64.torrent" 349 350 response = requests.get(url) 351 if response.status_code == 200: 352 with qbittorrentapi.Client(**conn_info) as qbt_client: 353 if qbt_client.torrents_add(url, category="distro") != "Ok.": 354 raise Exception("Failed to add torrent: " + os.path.basename(url)) 355 else: 356 print(f"Added {os.path.basename(url)}") 357 else: 358 print(f"{response.status_code}: {url}") 359 360 361 def remove_qubes(rel_ver: str): 362 """ 363 Remove QubesOS torrents given their release version. 364 365 Params: 366 relver: the Qubes OS release version. 367 """ 368 remove_torrents(f"Qubes-R{rel_ver}") 369 370 371 def add_rockylinux(rel_ver: str): 372 """ 373 Add Rocky Linux torrents from their URLs. 374 375 Params: 376 relver: the Rocky Linux release version. 377 """ 378 urls = [ 379 f"https://download.rockylinux.org/pub/rocky/{rel_ver}/isos/aarch64/Rocky-{rel_ver}-aarch64-dvd.torrent", 380 f"https://download.rockylinux.org/pub/rocky/{rel_ver}/isos/ppc64le/Rocky-{rel_ver}-ppc64le-dvd.torrent", 381 f"https://download.rockylinux.org/pub/rocky/{rel_ver}/isos/s390x/Rocky-{rel_ver}-s390x-dvd.torrent", 382 f"https://download.rockylinux.org/pub/rocky/{rel_ver}/isos/x86_64/Rocky-{rel_ver}-x86_64-dvd.torrent", 383 ] 384 385 add_torrents(urls) 386 387 388 def remove_rockylinux(rel_ver: str): 389 """ 390 Remove Rocky Linux torrents given their release version. 391 392 Params: 393 relver: the Rocky Linux release version. 394 """ 395 remove_torrents(f"Rocky-{rel_ver}") 396 397 398 def add_tails(rel_ver: str): 399 """ 400 Add Tails torrents from their URLs. 401 402 Params: 403 relver: the Tails release version. 404 """ 405 urls = [ 406 f"https://tails.net/torrents/files/tails-amd64-{rel_ver}.img.torrent", 407 f"https://tails.net/torrents/files/tails-amd64-{rel_ver}.iso.torrent", 408 ] 409 410 add_torrents(urls) 411 412 413 def remove_tails(rel_ver: str): 414 """ 415 Remove Tails torrents given their release version. 416 417 Params: 418 relver: the Tails release version. 419 """ 420 remove_torrents(f"tails-amd64-{rel_ver}") 421 422 423 if __name__ == "__main__": 424 # Check if gum is installed. 425 if which("gum") is None: 426 exit("Please install gum first. https://github.com/charmbracelet/gum") 427 428 # Run the gum program in a subprocess to allow easy selecting of distro 429 # torrents. 430 distro_selection = subprocess.run( 431 [ 432 "gum", 433 "choose", 434 "--limit=1", 435 "--header=Available torrents", 436 "--height=13", 437 "AlmaLinux", 438 "Debian", 439 "Devuan", 440 "Fedora", 441 "FreeBSD", 442 "Kali Linux", 443 "NetBSD", 444 "NixOS", 445 "Qubes", 446 "Rocky Linux", 447 "Tails", 448 ], 449 stdout=subprocess.PIPE, 450 text=True, 451 check=True, 452 ).stdout.strip() 453 454 # After the distro is selected and stored in the distro_selection variable, 455 # choose an action to take on the selected distro. 456 # Add: add the distro torrents to qBittorrent 457 # Remove: remove the distro torrents from qBittorrent 458 action_selection = subprocess.run( 459 ["gum", "choose", "--limit=1", "--header='Choose:'", "Add", "Remove"], 460 stdout=subprocess.PIPE, 461 text=True, 462 check=True, 463 ).stdout.strip() 464 465 # After the distro is selected and stored in the distro_selection variable, 466 # and after the action is selected and store in the action_selection 467 # variable, enter the release version of the selected distro to execute the 468 # selected action on. 469 relver = subprocess.run( 470 [ 471 "gum", 472 "input", 473 f"--placeholder='Enter {distro_selection} release version'", 474 ], 475 stdout=subprocess.PIPE, 476 text=True, 477 check=True, 478 ).stdout.strip() 479 480 # Match the distro_selection to execute the action_selection on. 481 match distro_selection: 482 case "AlmaLinux": 483 if action_selection == "Add": 484 add_almalinux(relver) 485 486 if action_selection == "Remove": 487 remove_almalinux(relver) 488 489 case "Debian": 490 if action_selection == "Add": 491 add_debian(relver) 492 493 if action_selection == "Remove": 494 remove_debian(relver) 495 496 case "Devuan": 497 if action_selection == "Add": 498 add_devuan(relver) 499 500 if action_selection == "Remove": 501 remove_devuan(relver) 502 503 case "Fedora": 504 if action_selection == "Add": 505 add_fedora(relver) 506 507 if action_selection == "Remove": 508 remove_fedora(relver) 509 510 case "FreeBSD": 511 if action_selection == "Add": 512 add_freebsd(relver) 513 514 if action_selection == "Remove": 515 remove_freebsd(relver) 516 517 case "Kali Linux": 518 if action_selection == "Add": 519 add_kali() 520 521 if action_selection == "Remove": 522 remove_kali() 523 524 case "NetBSD": 525 if action_selection == "Add": 526 add_netbsd(relver) 527 528 if action_selection == "Remove": 529 remove_netbsd(relver) 530 531 case "NixOS": 532 if action_selection == "Add": 533 add_nixos() 534 535 if action_selection == "Remove": 536 remove_nixos() 537 538 case "Qubes": 539 if action_selection == "Add": 540 add_qubes(relver) 541 542 if action_selection == "Remove": 543 remove_qubes(relver) 544 545 case "Rocky Linux": 546 if action_selection == "Add": 547 add_rockylinux(relver) 548 549 if action_selection == "Remove": 550 remove_rockylinux(relver) 551 552 case "Tails": 553 if action_selection == "Add": 554 add_tails(relver) 555 556 if action_selection == "Remove": 557 remove_tails(relver) 558 559 case _: 560 print("Nothing to do.")