Skip to content

Commit

Permalink
Merge pull request #84 from StuffAnThings/develop
Browse files Browse the repository at this point in the history
v3.1.4
  • Loading branch information
bobokun authored Jan 4, 2022
2 parents 19c4e62 + 25ae540 commit 0f2cb4f
Show file tree
Hide file tree
Showing 10 changed files with 307 additions and 133 deletions.
12 changes: 11 additions & 1 deletion .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,14 @@ updates:
directory: "/"
schedule:
interval: "daily"
target-branch: "develop"
target-branch: "develop"
assignees:
- "bobokun"
- package-ecosystem: github-actions
directory: '/'
schedule:
interval: daily
assignees:
- "bobokun"
ignore:
- dependency-name: "salsify/action-detect-and-tag-new-version"
16 changes: 10 additions & 6 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
FROM hotio/base:alpine
FROM python:3.9-alpine

# install packages
RUN apk add --no-cache gcc g++ libxml2-dev libxslt-dev shadow bash curl wget jq grep sed coreutils findutils unzip p7zip ca-certificates

COPY requirements.txt /

RUN apk add --no-cache py3-pip
COPY --chown=hotio:users requirements.txt /
RUN echo "**** install python packages ****" \
&& pip3 install --user --no-cache-dir --upgrade --requirement /requirements.txt \
&& pip3 install --no-cache-dir --upgrade --requirement /requirements.txt \
&& rm -rf /requirements.txt /tmp/* /var/tmp/*

COPY --chown=hotio:users . "${APP_DIR}"
WORKDIR ${APP_DIR}
COPY . /app
WORKDIR /app
VOLUME /config
ENTRYPOINT ["python3", "qbit_manage.py"]
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.1.3
3.1.4
22 changes: 19 additions & 3 deletions config/config.yml.sample
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# This is an example configuration file that documents all the options.
# It will need to be modified for your specific use case.
# Please refer to the link below for more details on how to set up the configuration file
# https://github.com/StuffAnThings/qbit_manage/wiki/Config-Setup

# qBittorrent parameters
qbt:
Expand All @@ -16,9 +18,14 @@ directory:
# root_dir var: </your/path/here/> # Root downloads directory used to check for orphaned files, noHL, and RecycleBin.
# <OPTIONAL> remote_dir var: </your/path/here/> # Path of docker host mapping of root_dir.
# Must be set if you're running qbit_manage locally and qBittorrent/cross_seed is in a docker
# <OPTIONAL> recycle_bin var: </your/path/here/> # Path of the RecycleBin folder. Default location is set to remote_dir/.RecycleBin
# <OPTIONAL> torrents_dir var: </your/path/here/> # Path of the your qbittorrent torrents directory. Required for `save_torrents` attribute in recyclebin

cross_seed: "/your/path/here/"
root_dir: "/data/torrents/"
remote_dir: "/mnt/user/data/torrents/"
recycle_bin: "/mnt/user/data/torrents/.RecycleBin"
torrents_dir: "/qbittorrent/data/BT_backup"

# Category & Path Parameters
cat:
Expand Down Expand Up @@ -123,10 +130,19 @@ nohardlinks:
# By default the Recycle Bin will be emptied on every run of the qbit_manage script if empty_after_x_days is defined.
recyclebin:
enabled: true
# <OPTIONAL> empty_after_x_days var: Will automatically remove all files and folders in recycle bin after x days. (Checks every script run)
# If this variable is not defined it, the RecycleBin will never be emptied.
# WARNING: Setting this variable to 0 will delete all files immediately upon script run!
# <OPTIONAL> empty_after_x_days var:
# Will automatically remove all files and folders in recycle bin after x days. (Checks every script run)
# If this variable is not defined it, the RecycleBin will never be emptied.
# WARNING: Setting this variable to 0 will delete all files immediately upon script run!
empty_after_x_days: 60
# <OPTIONAL> save_torrents var:
# If this option is set to true you MUST fill out the torrents_dir in the directory attribute.
# This will save a copy of your .torrent and .fastresume file in the recycle bin before deleting it from qbittorrent
save_torrents: true
# <OPTIONAL> split_by_category var:
# This will split the recycle bin folder by the save path defined in the `cat` attribute
# and add the base folder name of the recycle bin that was defined in the `recycle_bin` sub-attribute under directory.
split_by_category: false

# Orphaned files are those in the root_dir download directory that are not referenced by any active torrents.
orphaned:
Expand Down
72 changes: 50 additions & 22 deletions modules/config.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import logging, os, requests, stat, time
import logging, os, requests, stat, time, re
from modules import util
from modules.util import Failed, check
from modules.qbittorrent import Qbt
Expand Down Expand Up @@ -33,15 +33,13 @@ def __init__(self, default_dir, args):
yaml.YAML().allow_duplicate_keys = True
try:
new_config, _, _ = yaml.util.load_yaml_guess_indent(open(self.config_path, encoding="utf-8"))
if "settings" not in new_config: new_config["settings"] = {}
if "cat" not in new_config: new_config["cat"] = {}
if "tracker" not in new_config and "tags" not in new_config: new_config["tracker"] = {}
if "qbt" in new_config: new_config["qbt"] = new_config.pop("qbt")
if "settings" in new_config: new_config["settings"] = new_config.pop("settings")
new_config["settings"] = new_config.pop("settings") if "settings" in new_config else {}
if "directory" in new_config: new_config["directory"] = new_config.pop("directory")
if "cat" in new_config: new_config["cat"] = new_config.pop("cat")
new_config["cat"] = new_config.pop("cat") if "cat" in new_config else {}
if "tracker" in new_config: new_config["tracker"] = new_config.pop("tracker")
elif "tags" in new_config: new_config["tracker"] = new_config.pop("tags")
else: new_config["tracker"] = {}
if "nohardlinks" in new_config: new_config["nohardlinks"] = new_config.pop("nohardlinks")
if "recyclebin" in new_config: new_config["recyclebin"] = new_config.pop("recyclebin")
if "orphaned" in new_config: new_config["orphaned"] = new_config.pop("orphaned")
Expand Down Expand Up @@ -178,15 +176,13 @@ def hooks(attr):
self.recyclebin = {}
self.recyclebin['enabled'] = self.util.check_for_attribute(self.data, "enabled", parent="recyclebin", var_type="bool", default=True)
self.recyclebin['empty_after_x_days'] = self.util.check_for_attribute(self.data, "empty_after_x_days", parent="recyclebin", var_type="int", default_is_none=True)

# Add Orphaned
self.orphaned = {}
self.orphaned['exclude_patterns'] = self.util.check_for_attribute(self.data, "exclude_patterns", parent="orphaned", var_type="list", default_is_none=True, do_print=False)
self.recyclebin['save_torrents'] = self.util.check_for_attribute(self.data, "save_torrents", parent="recyclebin", var_type="bool", default=False)
self.recyclebin['split_by_category'] = self.util.check_for_attribute(self.data, "split_by_category", parent="recyclebin", var_type="bool", default=False)

# Assign directories
if "directory" in self.data:
self.root_dir = self.util.check_for_attribute(self.data, "root_dir", parent="directory", default_is_none=True)
self.remote_dir = self.util.check_for_attribute(self.data, "remote_dir", parent="directory", default=self.root_dir)
self.root_dir = os.path.join(self.util.check_for_attribute(self.data, "root_dir", parent="directory", default_is_none=True), '')
self.remote_dir = os.path.join(self.util.check_for_attribute(self.data, "remote_dir", parent="directory", default=self.root_dir), '')
if (self.args["cross_seed"] or self.args["tag_nohardlinks"] or self.args["rem_orphaned"]):
self.remote_dir = self.util.check_for_attribute(self.data, "remote_dir", parent="directory", var_type="path", default=self.root_dir)
else:
Expand All @@ -196,12 +192,26 @@ def hooks(attr):
self.cross_seed_dir = self.util.check_for_attribute(self.data, "cross_seed", parent="directory", var_type="path")
else:
self.cross_seed_dir = self.util.check_for_attribute(self.data, "cross_seed", parent="directory", default_is_none=True)
self.recycle_dir = os.path.join(self.remote_dir, '.RecycleBin')
self.recycle_dir = self.util.check_for_attribute(self.data, "recycle_bin", parent="directory", var_type="path", default=os.path.join(self.remote_dir, '.RecycleBin'), make_dirs=True)
if self.recyclebin['enabled'] and self.recyclebin['save_torrents']:
self.torrents_dir = self.util.check_for_attribute(self.data, "torrents_dir", parent="directory", var_type="path")
if not any(File.endswith(".torrent") for File in os.listdir(self.torrents_dir)):
e = f"Config Error: The location {self.torrents_dir} does not contain any .torrents"
self.notify(e, 'Config')
raise Failed(e)
else:
self.torrents_dir = self.util.check_for_attribute(self.data, "torrents_dir", parent="directory", default_is_none=True)
else:
e = "Config Error: directory attribute not found"
self.notify(e, 'Config')
raise Failed(e)

# Add Orphaned
exclude_recycle = f"**/{os.path.basename(self.recycle_dir.rstrip('/'))}/*"
self.orphaned = {}
self.orphaned['exclude_patterns'] = self.util.check_for_attribute(self.data, "exclude_patterns", parent="orphaned", var_type="list", default_is_none=True, do_print=False)
self.orphaned['exclude_patterns'].append(exclude_recycle) if exclude_recycle not in self.orphaned['exclude_patterns'] else self.orphaned['exclude_patterns']

# Connect to Qbittorrent
self.qbt = None
if "qbt" in self.data:
Expand Down Expand Up @@ -304,28 +314,46 @@ def empty_recycle(self):
files = []
size_bytes = 0
if not self.args["skip_recycle"]:
n_info = ''
if self.recyclebin['enabled'] and self.recyclebin['empty_after_x_days']:
recycle_files = [os.path.join(path, name) for path, subdirs, files in os.walk(self.recycle_dir) for name in files]
if self.recyclebin['split_by_category']:
if "cat" in self.data and self.data["cat"] is not None:
save_path = list(self.data["cat"].values())
cleaned_save_path = [os.path.join(s.replace(self.root_dir, self.remote_dir), os.path.basename(self.recycle_dir.rstrip('/'))) for s in save_path]
recycle_path = [self.recycle_dir]
for dir in cleaned_save_path:
if os.path.exists(dir): recycle_path.append(dir)
else:
e = (f'No categories defined. Checking Recycle Bin directory {self.recycle_dir}.')
self.notify(e, 'Empty Recycle Bin', False)
logger.warning(e)
recycle_path = [self.recycle_dir]
else:
recycle_path = [self.recycle_dir]
recycle_files = [os.path.join(path, name) for r_path in recycle_path for path, subdirs, files in os.walk(r_path) for name in files]
recycle_files = sorted(recycle_files)
if recycle_files:
util.separator(f"Emptying Recycle Bin (Files > {self.recyclebin['empty_after_x_days']} days)", space=False, border=False)
body = []
util.separator(f"Emptying Recycle Bin (Files > {self.recyclebin['empty_after_x_days']} days)", space=True, border=True)
prevfolder = ''
for file in recycle_files:
folder = re.search(f".*{os.path.basename(self.recycle_dir.rstrip('/'))}", file).group(0)
if folder != prevfolder: body += util.separator(f"Searching: {folder}", space=False, border=False)
fileStats = os.stat(file)
filename = file.replace(self.recycle_dir, '')
filename = os.path.basename(file)
last_modified = fileStats[stat.ST_MTIME] # in seconds (last modified time)
now = time.time() # in seconds
days = (now - last_modified) / (60 * 60 * 24)
if (self.recyclebin['empty_after_x_days'] <= days):
num_del += 1
n_info += (f"{'Did not delete' if dry_run else 'Deleted'} {filename} from the recycle bin. (Last modified {round(days)} days ago).\n")
body += util.print_line(f"{'Did not delete' if dry_run else 'Deleted'} {filename} from {folder} (Last modified {round(days)} days ago).", loglevel)
files += [str(filename)]
size_bytes += os.path.getsize(file)
if not dry_run: os.remove(file)
prevfolder = re.search(f".*{os.path.basename(self.recycle_dir.rstrip('/'))}", file).group(0)
if num_del > 0:
if not dry_run: util.remove_empty_directories(self.recycle_dir, "**/*")
body = []
body += util.print_multiline(n_info, loglevel)
if not dry_run:
for path in recycle_path:
util.remove_empty_directories(path, "**/*")
body += util.print_line(f"{'Did not delete' if dry_run else 'Deleted'} {num_del} files ({util.human_readable_size(size_bytes)}) from the Recycle Bin.", loglevel)
attr = {
"function": "empty_recyclebin",
Expand All @@ -337,7 +365,7 @@ def empty_recycle(self):
}
self.send_notifications(attr)
else:
logger.debug('No files found in "' + self.recycle_dir + '"')
logger.debug(f'No files found in "{(",".join(recycle_path))}"')
return num_del

def send_notifications(self, attr):
Expand Down
Loading

0 comments on commit 0f2cb4f

Please sign in to comment.