Skip to content

Commit

Permalink
4.1.17 (#744)
Browse files Browse the repository at this point in the history
* 4.1.17-develop1

* Retry on ConnectionError (#740)

Add Retries for connection to qbit

* Adds !ENV constructor to read environment variables

* Update config sample to include ENV variable examples

* Fixes #702

* remove warning when remote_dir not defined

* [pre-commit.ci] pre-commit autoupdate (#742)

* [pre-commit.ci] pre-commit autoupdate

updates:
- [github.com/pycqa/isort: 5.13.2 → 6.0.0](PyCQA/isort@5.13.2...6.0.0)
- [github.com/psf/black: 24.10.0 → 25.1.0](psf/black@24.10.0...25.1.0)

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

* add more !ENV usage in config.yml.sample

* 4.1.17

* formatting

---------

Co-authored-by: Denys Kozhevnikov <[email protected]>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored Feb 10, 2025
1 parent 99cfac5 commit a09bdb5
Show file tree
Hide file tree
Showing 9 changed files with 99 additions and 29 deletions.
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ repos:
- id: yamlfix
exclude: ^.github/
- repo: https://github.com/pycqa/isort
rev: 5.13.2
rev: 6.0.0
hooks:
- id: isort
name: isort (python)
Expand All @@ -43,7 +43,7 @@ repos:
- id: pyupgrade
args: [--py3-plus]
- repo: https://github.com/psf/black
rev: 24.10.0
rev: 25.1.0
hooks:
- id: black
language_version: python3
Expand Down
11 changes: 6 additions & 5 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# Requirements Updated
ruamel.yaml==0.18.10

# New Updates
- Adds support for wlidcard matching in category (Adds #695)
- Adds support for environment variables in config file using (`!ENV VAR_NAME`)
- Add Retries on ConnectionError (#740)
- Fixes #702
- Removes warning when `remote_dir` is not defined

**Full Changelog**: https://github.com/StuffAnThings/qbit_manage/compare/v4.1.15...v4.1.16
Special thanks to @NooNameR for their contributions!
**Full Changelog**: https://github.com/StuffAnThings/qbit_manage/compare/v4.1.16...v4.1.17
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
4.1.16
4.1.17
7 changes: 4 additions & 3 deletions config/config.yml.sample
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ commands:

qbt:
# qBittorrent parameters
# Pass environment variables to the config via !ENV tag
host: "localhost:8080"
user: "username"
pass: "password"
user: !ENV QBIT_USER
pass: !ENV QBIT_PASS

settings:
force_auto_tmm: False # Will force qBittorrent to enable Automatic Torrent Management for each torrent.
Expand Down Expand Up @@ -295,7 +296,7 @@ notifiarr:
# Notifiarr integration with webhooks
# Leave Empty/Blank to disable
# Mandatory to fill out API Key
apikey: ####################################
apikey: !ENV NOTIFIARR_API
# <OPTIONAL> Set to a unique value (could be your username on notifiarr for example)
instance:

Expand Down
4 changes: 3 additions & 1 deletion docs/Config-Setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@

The script utilizes a YAML config file to load information to connect to the various APIs you can connect with.

By default, the script looks at /config/config.yml for the Configuration File unless otherwise specified.
By default, the script looks at `/config/config.yml` when running locally or `/app/config.yml` in docker for the Configuration File unless otherwise specified.

A template Configuration File can be found in the repo [config/config.yml.sample](https://github.com/StuffAnThings/qbit_manage/blob/master/config/config.yml.sample).

You can reference environment variables inside your `config.yml` by `!ENV VAR_NAME`

**WARNING**: As this software is constantly evolving and this wiki might not be up to date the sample shown here might not might not be current. Please refer to the repo for the most current version.

# Config File
Expand Down
51 changes: 39 additions & 12 deletions modules/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -657,16 +657,31 @@ def _sort_share_limits(share_limits):
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), ""
self.util.check_for_attribute(
self.data, "remote_dir", parent="directory", default=self.root_dir, do_print=False, save=False
),
"",
)
if self.commands["cross_seed"] or self.commands["tag_nohardlinks"] or self.commands["rem_orphaned"]:
self.remote_dir = self.util.check_for_attribute(
self.data, "remote_dir", parent="directory", var_type="path", default=self.root_dir
self.data,
"remote_dir",
parent="directory",
var_type="path",
default=self.root_dir,
do_print=False,
save=False,
)
else:
if self.recyclebin["enabled"]:
self.remote_dir = self.util.check_for_attribute(
self.data, "remote_dir", parent="directory", var_type="path", default=self.root_dir
self.data,
"remote_dir",
parent="directory",
var_type="path",
default=self.root_dir,
do_print=False,
save=False,
)
if not self.remote_dir:
self.remote_dir = self.root_dir
Expand Down Expand Up @@ -754,20 +769,32 @@ def _sort_share_limits(share_limits):
# Connect to Qbittorrent
self.qbt = None
if "qbt" in self.data:
logger.info("Connecting to Qbittorrent...")
self.qbt = Qbt(
self,
{
"host": self.util.check_for_attribute(self.data, "host", parent="qbt", throw=True),
"username": self.util.check_for_attribute(self.data, "user", parent="qbt", default_is_none=True),
"password": self.util.check_for_attribute(self.data, "pass", parent="qbt", default_is_none=True),
},
)
self.qbt = self.__connect()
else:
e = "Config Error: qbt attribute not found"
self.notify(e, "Config")
raise Failed(e)

def __retry_on_connect(exception):
return isinstance(exception.__cause__, ConnectionError)

@retry(
retry_on_exception=__retry_on_connect,
stop_max_attempt_number=5,
wait_exponential_multiplier=30000,
wait_exponential_max=120000,
)
def __connect(self):
logger.info("Connecting to Qbittorrent...")
return Qbt(
self,
{
"host": self.util.check_for_attribute(self.data, "host", parent="qbt", throw=True),
"username": self.util.check_for_attribute(self.data, "user", parent="qbt", default_is_none=True),
"password": self.util.check_for_attribute(self.data, "pass", parent="qbt", default_is_none=True),
},
)

# Empty old files from recycle bin or orphaned
def cleanup_dirs(self, location):
num_del = 0
Expand Down
2 changes: 1 addition & 1 deletion modules/core/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""
modules.core contains all the core functions of qbit_manage such as updating categories/tags etc..
modules.core contains all the core functions of qbit_manage such as updating categories/tags etc..
"""
4 changes: 4 additions & 0 deletions modules/qbittorrent.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from fnmatch import fnmatch
from functools import cache

from qbittorrentapi import APIConnectionError
from qbittorrentapi import Client
from qbittorrentapi import LoginFailed
from qbittorrentapi import NotFound404Error
Expand Down Expand Up @@ -77,6 +78,9 @@ def __init__(self, config, params):
ex = "Qbittorrent Error: Failed to login. Invalid username/password."
self.config.notify(ex, "Qbittorrent")
raise Failed(ex)
except APIConnectionError as exc:
self.config.notify(exc, "Qbittorrent")
raise Failed(exc) from ConnectionError(exc)
except Exception as exc:
self.config.notify(exc, "Qbittorrent")
raise Failed(exc)
Expand Down
43 changes: 39 additions & 4 deletions modules/util.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
""" Utility functions for qBit Manage. """
"""Utility functions for qBit Manage."""

import json
import logging
Expand All @@ -12,6 +12,7 @@
import requests
import ruamel.yaml
from pytimeparse2 import parse
from ruamel.yaml.constructor import ConstructorError

logger = logging.getLogger("qBit Manage")

Expand Down Expand Up @@ -756,13 +757,19 @@ def human_readable_size(size, decimal_places=3):


class YAML:
"""Class to load and save yaml files"""
"""Class to load and save yaml files with !ENV tag preservation and environment variable resolution"""

def __init__(self, path=None, input_data=None, check_empty=False, create=False):
self.path = path
self.input_data = input_data
self.yaml = ruamel.yaml.YAML()
self.yaml.indent(mapping=2, sequence=2)

# Add constructor for !ENV tag
self.yaml.Constructor.add_constructor("!ENV", self._env_constructor)
# Add representer for !ENV tag
self.yaml.Representer.add_representer(EnvStr, self._env_representer)

try:
if input_data:
self.data = self.yaml.load(input_data)
Expand All @@ -784,8 +791,36 @@ def __init__(self, path=None, input_data=None, check_empty=False, create=False):
raise Failed("YAML Error: File is empty")
self.data = {}

def _env_constructor(self, loader, node):
"""Constructor for !ENV tag"""
value = loader.construct_scalar(node)
# Resolve the environment variable at runtime
env_value = os.getenv(value)
if env_value is None:
raise ConstructorError(f"Environment variable '{value}' not found")
# Return a custom string subclass that preserves the !ENV tag
return EnvStr(value, env_value)

def _env_representer(self, dumper, data):
"""Representer for EnvStr class"""
return dumper.represent_scalar("!ENV", data.env_var)

def save(self):
"""Save yaml file"""
"""Save yaml file with !ENV tags preserved"""
if self.path:
with open(self.path, "w") as filepath:
with open(self.path, "w", encoding="utf-8") as filepath:
self.yaml.dump(self.data, filepath)


class EnvStr(str):
"""Custom string subclass to preserve !ENV tags"""

def __new__(cls, env_var, resolved_value):
# Create a new string instance with the resolved value
instance = super().__new__(cls, resolved_value)
instance.env_var = env_var # Store the environment variable name
return instance

def __repr__(self):
"""Return the resolved value as a string"""
return super().__repr__()

0 comments on commit a09bdb5

Please sign in to comment.