mirror of
https://github.com/StuffAnThings/qbit_manage.git
synced 2024-09-20 15:26:02 +08:00
Script to delete torrents on low disk space (#169)
* Addes new script for deleting torrents when low on space
This commit is contained in:
parent
3bb3f092e1
commit
33d9286eff
177
scripts/delete_torrents_on_low_disk_space.py
Normal file
177
scripts/delete_torrents_on_low_disk_space.py
Normal 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()
|
Loading…
Reference in a new issue