/ 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.")