mirror of
https://github.com/StuffAnThings/qbit_manage.git
synced 2025-10-03 10:24:31 +08:00
4.1.17 (#744)
* 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](https://github.com/pycqa/isort/compare/5.13.2...6.0.0) - [github.com/psf/black: 24.10.0 → 25.1.0](https://github.com/psf/black/compare/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 <github@mail.noonamer.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
99cfac58aa
commit
a09bdb5f0b
9 changed files with 99 additions and 29 deletions
|
@ -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)
|
||||
|
@ -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
|
||||
|
|
11
CHANGELOG
11
CHANGELOG
|
@ -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
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
4.1.16
|
||||
4.1.17
|
||||
|
|
|
@ -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.
|
||||
|
@ -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:
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -657,16 +657,31 @@ class Config:
|
|||
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
|
||||
|
@ -754,20 +769,32 @@ class Config:
|
|||
# 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
|
||||
|
|
|
@ -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..
|
||||
"""
|
||||
|
|
|
@ -5,6 +5,7 @@ import sys
|
|||
from fnmatch import fnmatch
|
||||
from functools import cache
|
||||
|
||||
from qbittorrentapi import APIConnectionError
|
||||
from qbittorrentapi import Client
|
||||
from qbittorrentapi import LoginFailed
|
||||
from qbittorrentapi import NotFound404Error
|
||||
|
@ -77,6 +78,9 @@ class Qbt:
|
|||
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)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
""" Utility functions for qBit Manage. """
|
||||
"""Utility functions for qBit Manage."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
@ -12,6 +12,7 @@ from pathlib import Path
|
|||
import requests
|
||||
import ruamel.yaml
|
||||
from pytimeparse2 import parse
|
||||
from ruamel.yaml.constructor import ConstructorError
|
||||
|
||||
logger = logging.getLogger("qBit Manage")
|
||||
|
||||
|
@ -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)
|
||||
|
@ -784,8 +791,36 @@ class YAML:
|
|||
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__()
|
||||
|
|
Loading…
Add table
Reference in a new issue