Script to delete torrents on low disk space (#169)

* Addes new script for deleting torrents when low on space
This commit is contained in:
StoneColeQ 2022-10-20 22:30:39 -04:00 committed by GitHub
parent 3bb3f092e1
commit 33d9286eff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -0,0 +1,177 @@
"""This script deletes torrents once your drive space drops below a certain threshold.
You can set a min torrent age and share ratio for a torrent to be deleted.
You can also allow incomplete torrents to be deleted.
Torrents will be deleted starting with the ones with the most seeds, only torrents with a single hardlink will be deleted.
"""
import os
import qbittorrentapi
from datetime import datetime, timedelta
import shutil
import time
"""===Config==="""
# qBittorrent WebUi Login
qbt_login = {"host": "localhost", "port": 8080, "username": "???", "password": "???"}
PATH = "M:" # Path of drive to monitor
MIN_FREE_SPACE = 10 # In GB. Min free space on drive.
MIN_FREE_USAGE = 0 # In decimal percentage, 0 to 1. Min % free space on drive.
MIN_TORRENT_SHARE_RATIO = 0 # In decimal percentage, 0 to inf. Min seeding ratio of torrent to delete.
MIN_TORRENT_AGE = 30 # In days, min age of torrent to delete. Uses seeding time.
ALLOW_INCOMPLETE_TORRENT_DELETIONS = False # Also delete torrents that haven't finished downloading. MIN_TORRENT_AGE now based on time torrent was added.
PREFER_PRIVATE_TORRENTS = True # Will delete public torrents before private ones regardless of seed difference. See is_torrent_public().
"""===End Config==="""
# Services
qbt_client: qbittorrentapi.Client = None
def quit_program(code=0) -> None:
"""Quits program with info"""
print("Exiting...")
import sys
sys.exit(code)
def setup_services(qbt=False) -> None:
"""Setup required services"""
global qbt_client
if qbt:
qbt_client = qbittorrentapi.Client(host=qbt_login["host"], port=qbt_login["port"], username=qbt_login["username"], password=qbt_login["password"])
try:
qbt_client.auth_log_in()
print("Succesfully connected to qBittorrent!")
except:
print("Error: Could not log into qBittorrent. Please verify login details are correct and Web Ui is available.")
quit_program(1)
def bytes_to_gb(data) -> float:
"""Converts bytes to GB."""
return data / 1024**3
def seconds_to_days(seconds) -> float:
"""Converts seconds to days."""
return seconds / 60 / 60 / 24
def get_disk_usage() -> tuple[float, float]:
"""Gets the free space and free usage of disk."""
stat = shutil.disk_usage(PATH)
free_space = bytes_to_gb(stat.free)
free_usage = stat.free / stat.total
return free_space, free_usage
def is_storage_full() -> bool:
"""Checks if free space are below user threshold."""
free_space, free_usage = get_disk_usage()
if free_space < MIN_FREE_SPACE or free_usage < MIN_FREE_USAGE:
return True
return False
def print_free_space() -> None:
"""Prints free space and user threshold."""
free_space, free_usage = get_disk_usage()
print(f"Free space: {free_space:.2f} GB ({free_usage:.2%}) - Thresholds: {MIN_FREE_SPACE:.2f} GB ({MIN_FREE_USAGE:.2%}) ")
def is_torrent_public(torrent_hash, setup=True) -> bool:
"""Checks if torrent is public or private by word 'private' in tracker messages."""
setup_services(qbt=setup)
torrent_trackers = qbt_client.torrents_trackers(torrent_hash)
for tracker in torrent_trackers:
if "private" in tracker["msg"].lower():
return False
return True
def has_single_hard_link(path) -> bool:
"""Check if file has a single hard link. False if any file in directory has multiple."""
# Check all files if path is directory
if os.path.isfile(path):
if os.stat(path).st_nlink > 1:
return False
else:
for dirpath, _, filenames in os.walk(path):
for file in filenames:
file_path = os.path.join(dirpath, file)
if os.stat(file_path).st_nlink > 1:
return False
return True
def torrent_age_satisfied(torrent) -> bool:
"""Gets the age of the torrent based on config"""
if ALLOW_INCOMPLETE_TORRENT_DELETIONS:
return datetime.now() >= datetime.fromtimestamp(torrent["added_on"]) + timedelta(days=MIN_TORRENT_AGE)
else:
return seconds_to_days(torrent["seeding_time"]) >= MIN_TORRENT_AGE
def main() -> None:
# If free space above requirements, terminate
print_free_space()
if is_storage_full():
print("Drive space low, will be deleting torrents...")
else:
print("Free space already above threshold, no torrents were deleted!")
quit_program(0)
setup_services(qbt=True)
# Get all torrents older than threshold
print("Getting all torrents above age and seeding threshold...")
torrent_hashes_raw = []
torrent_privacy_raw = []
torrent_num_seeds_raw = []
for torrent in qbt_client.torrents_info():
torrent_share_ratio = qbt_client.torrents_properties(torrent["hash"])["share_ratio"]
if torrent_age_satisfied(torrent) and torrent_share_ratio >= MIN_TORRENT_SHARE_RATIO:
torrent_hashes_raw.append(torrent["hash"])
torrent_privacy_raw.append(is_torrent_public(torrent["hash"], setup=False) if PREFER_PRIVATE_TORRENTS else True)
torrent_num_seeds_raw.append(torrent["num_complete"])
# Sort so most available torrent is last.
torrent_hashes = []
for *_, torrent_hash in sorted(zip(torrent_privacy_raw, torrent_num_seeds_raw, torrent_hashes_raw)):
torrent_hashes.append(torrent_hash)
# Delete torrents until storage is above threshold
deleted_torrents = []
if torrent_hashes:
print("Deleting torrents with a single hard link...")
while is_storage_full() and torrent_hashes:
torrent_hash = torrent_hashes.pop()
torrent_info = qbt_client.torrents_info(torrent_hashes=torrent_hash)[0]
torrent_name = torrent_info["name"]
torrent_path = torrent_info["content_path"]
# Only delete torrents with a single hard link as ones with multiple won't free any space
if has_single_hard_link(torrent_path):
qbt_client.torrents_delete(torrent_hashes=torrent_hash, delete_files=True)
deleted_torrents.append(torrent_name)
print(f"--- {torrent_name}")
time.sleep(1) # Sleep a bit after each deletion to make sure disk usage is updated.
# Print results
print_free_space()
if not is_storage_full():
print(f"Free space now above threshold, {len(deleted_torrents)} torrents were deleted!")
else: # No more torrents to delete but still low on space
print(f"WARNING... Free space still below threshold after deleting all {len(deleted_torrents)} eligible torrents! Either:")
print(f"--- Torrent ages are below threshold of '{MIN_TORRENT_AGE} days'\n"
f"--- Torrent seed ratios are below threshold of '{MIN_TORRENT_SHARE_RATIO}'\n"
f"--- Torrents have multiple hard links\n"
f"--- No torrents exists!")
quit_program(0)
if __name__ == "__main__":
main()