qBitrr2 3.7.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
qBitrr/bundled_data.py ADDED
@@ -0,0 +1,6 @@
1
+ version = "3.7.1"
2
+ git_hash = "da63e3c"
3
+ license_text = (
4
+ "Licence can be found on:\n\nhttps://github.com/Feramance/qBitrr/blob/master/LICENSE"
5
+ )
6
+ patched_version = f"{version}-{git_hash}"
qBitrr/config.py ADDED
@@ -0,0 +1,172 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import contextlib
5
+ import os
6
+ import pathlib
7
+ import shutil
8
+ import signal
9
+ import sys
10
+
11
+ from qBitrr.bundled_data import license_text, patched_version
12
+ from qBitrr.env_config import ENVIRO_CONFIG
13
+ from qBitrr.gen_config import MyConfig, _write_config_file, generate_doc
14
+ from qBitrr.home_path import APPDATA_FOLDER, HOME_PATH, ON_DOCKER
15
+
16
+
17
+ def process_flags() -> argparse.Namespace | bool:
18
+ parser = argparse.ArgumentParser(description="An interface to interact with qBit and *arrs.")
19
+ parser.add_argument(
20
+ "--gen-config",
21
+ "-gc",
22
+ dest="gen_config",
23
+ help="Generate a config file in the current working directory",
24
+ action="store_true",
25
+ )
26
+ parser.add_argument(
27
+ "-v", "--version", action="version", version=f"qBitrr version: {patched_version}"
28
+ )
29
+
30
+ parser.add_argument(
31
+ "-l",
32
+ "--license",
33
+ dest="license",
34
+ action="store_const",
35
+ const=license_text,
36
+ help="Show the qBitrr's licence",
37
+ )
38
+ parser.add_argument(
39
+ "-s",
40
+ "--source",
41
+ action="store_const",
42
+ dest="source",
43
+ const="Source code can be found on: https://github.com/Feramance/qBitrr",
44
+ help="Shows a link to qBitrr's source",
45
+ )
46
+
47
+ args = parser.parse_args()
48
+
49
+ if args.gen_config:
50
+ from qBitrr.gen_config import _write_config_file
51
+
52
+ _write_config_file()
53
+ return True
54
+ elif args.license:
55
+ print(args.license)
56
+ return True
57
+ elif args.source:
58
+ print(args.source)
59
+ return True
60
+ return args
61
+
62
+
63
+ COPIED_TO_NEW_DIR = False
64
+ file = "config.toml"
65
+ CONFIG_FILE = HOME_PATH.joinpath(file)
66
+ CONFIG_PATH = pathlib.Path(f"./{file}")
67
+ if any(
68
+ a in sys.argv
69
+ for a in [
70
+ "--gen-config",
71
+ "-gc",
72
+ "--version",
73
+ "-v",
74
+ "--license",
75
+ "-l",
76
+ "--source",
77
+ "-s",
78
+ "-h",
79
+ "--help",
80
+ ]
81
+ ):
82
+ CONFIG = MyConfig(CONFIG_FILE, config=generate_doc())
83
+ COPIED_TO_NEW_DIR = None
84
+ elif (not CONFIG_FILE.exists()) and (not CONFIG_PATH.exists()):
85
+ if ON_DOCKER:
86
+ print(f"{file} has not been found")
87
+
88
+ CONFIG_FILE = _write_config_file(docker=True)
89
+ print(f"'{CONFIG_FILE.name}' has been generated")
90
+ print('Rename it to "config.toml" then edit it and restart the container')
91
+
92
+ os.kill(os.getppid(), signal.SIGTERM)
93
+
94
+ else:
95
+ print(f"{file} has not been found")
96
+
97
+ CONFIG_FILE = _write_config_file(docker=True)
98
+ print(f"'{CONFIG_FILE.name}' has been generated")
99
+ print('Rename it to "config.toml" then edit it and restart the container')
100
+
101
+ os.kill(os.getppid(), signal.SIGTERM)
102
+
103
+ elif CONFIG_FILE.exists():
104
+ CONFIG = MyConfig(CONFIG_FILE)
105
+ else:
106
+ with contextlib.suppress(
107
+ Exception
108
+ ): # If file already exist or can't copy to APPDATA_FOLDER ignore the exception
109
+ shutil.copy(CONFIG_PATH, CONFIG_FILE)
110
+ COPIED_TO_NEW_DIR = True
111
+ CONFIG = MyConfig("./config.toml")
112
+
113
+ if COPIED_TO_NEW_DIR is not None:
114
+ # print(f"STARTUP | {CONFIG.path} |\n{CONFIG}")
115
+ print(f"STARTUP")
116
+ else:
117
+ print(f"STARTUP | CONFIG_FILE={CONFIG_FILE} | CONFIG_PATH={CONFIG_PATH}")
118
+
119
+ FFPROBE_AUTO_UPDATE = (
120
+ CONFIG.get("Settings.FFprobeAutoUpdate", fallback=True)
121
+ if ENVIRO_CONFIG.settings.ffprobe_auto_update is None
122
+ else ENVIRO_CONFIG.settings.ffprobe_auto_update
123
+ )
124
+ FAILED_CATEGORY = ENVIRO_CONFIG.settings.failed_category or CONFIG.get(
125
+ "Settings.FailedCategory", fallback="failed"
126
+ )
127
+ RECHECK_CATEGORY = ENVIRO_CONFIG.settings.recheck_category or CONFIG.get(
128
+ "Settings.RecheckCategory", fallback="recheck"
129
+ )
130
+ CONSOLE_LOGGING_LEVEL_STRING = ENVIRO_CONFIG.settings.console_level or CONFIG.get_or_raise(
131
+ "Settings.ConsoleLevel"
132
+ )
133
+ COMPLETED_DOWNLOAD_FOLDER = (
134
+ ENVIRO_CONFIG.settings.completed_download_folder
135
+ or CONFIG.get_or_raise("Settings.CompletedDownloadFolder")
136
+ )
137
+ NO_INTERNET_SLEEP_TIMER = ENVIRO_CONFIG.settings.no_internet_sleep_timer or CONFIG.get(
138
+ "Settings.NoInternetSleepTimer", fallback=60
139
+ )
140
+ LOOP_SLEEP_TIMER = ENVIRO_CONFIG.settings.loop_sleep_timer or CONFIG.get(
141
+ "Settings.LoopSleepTimer", fallback=5
142
+ )
143
+ PING_URLS = ENVIRO_CONFIG.settings.ping_urls or CONFIG.get(
144
+ "Settings.PingURLS", fallback=["one.one.one.one", "dns.google.com"]
145
+ )
146
+ IGNORE_TORRENTS_YOUNGER_THAN = ENVIRO_CONFIG.settings.ignore_torrents_younger_than or CONFIG.get(
147
+ "Settings.IgnoreTorrentsYoungerThan", fallback=600
148
+ )
149
+ QBIT_DISABLED = (
150
+ CONFIG.get("qBit.Disabled", fallback=False)
151
+ if ENVIRO_CONFIG.qbit.disabled is None
152
+ else ENVIRO_CONFIG.qbit.disabled
153
+ )
154
+ SEARCH_ONLY = ENVIRO_CONFIG.overrides.search_only
155
+ PROCESS_ONLY = ENVIRO_CONFIG.overrides.processing_only
156
+
157
+ if QBIT_DISABLED and PROCESS_ONLY:
158
+ print("qBittorrent is disabled yet QBITRR_OVERRIDES_PROCESSING_ONLY is enabled")
159
+ print(
160
+ "Processing monitors qBitTorrents downloads "
161
+ "therefore it depends on a health qBitTorrent connection"
162
+ )
163
+ print("Exiting...")
164
+ sys.exit(1)
165
+
166
+ if SEARCH_ONLY and QBIT_DISABLED is False:
167
+ QBIT_DISABLED = True
168
+ print("QBITRR_OVERRIDES_SEARCH_ONLY is enabled, forcing qBitTorrent setting off")
169
+
170
+ # Settings Config Values
171
+ FF_VERSION = APPDATA_FOLDER.joinpath("ffprobe_info.json")
172
+ FF_PROBE = APPDATA_FOLDER.joinpath("ffprobe")
qBitrr/env_config.py ADDED
@@ -0,0 +1,60 @@
1
+ from distutils.util import strtobool
2
+ from typing import Optional
3
+
4
+ import environ
5
+
6
+
7
+ class Converter:
8
+ @staticmethod
9
+ def int(value: Optional[str]) -> Optional[int]:
10
+ if value is None:
11
+ return None
12
+ return int(value)
13
+
14
+ @staticmethod
15
+ def list(value: Optional[str], delimiter=",", converter=str) -> Optional[list]:
16
+ if value is None:
17
+ return None
18
+ return list(map(converter, value.split(delimiter)))
19
+
20
+ @staticmethod
21
+ def bool(value: Optional[str]) -> Optional[bool]:
22
+ if value is None:
23
+ return None
24
+ return strtobool(value) == 1
25
+
26
+
27
+ @environ.config(prefix="QBITRR", frozen=True)
28
+ class AppConfig:
29
+ @environ.config(prefix="OVERRIDES", frozen=True)
30
+ class Overrides:
31
+ search_only = environ.var(None, converter=Converter.bool)
32
+ processing_only = environ.var(None, converter=Converter.bool)
33
+ data_path = environ.var(None)
34
+
35
+ @environ.config(prefix="SETTINGS", frozen=True)
36
+ class Settings:
37
+ console_level = environ.var(None)
38
+ completed_download_folder = environ.var(None)
39
+ no_internet_sleep_timer = environ.var(None, converter=Converter.int)
40
+ loop_sleep_timer = environ.var(None, converter=Converter.int)
41
+ failed_category = environ.var(None)
42
+ recheck_category = environ.var(None)
43
+ ignore_torrents_younger_than = environ.var(None, converter=Converter.int)
44
+ ping_urls = environ.var(None, converter=Converter.list)
45
+ ffprobe_auto_update = environ.var(None, converter=Converter.bool)
46
+
47
+ @environ.config(prefix="QBIT", frozen=True)
48
+ class qBit:
49
+ disabled = environ.var(None, converter=Converter.bool)
50
+ host = environ.var(None)
51
+ port = environ.var(None, converter=Converter.int)
52
+ username = environ.var(None)
53
+ password = environ.var(None)
54
+
55
+ overrides: Overrides = environ.group(Overrides)
56
+ settings: Settings = environ.group(Settings)
57
+ qbit: qBit = environ.group(qBit)
58
+
59
+
60
+ ENVIRO_CONFIG: AppConfig = environ.to_config(AppConfig)
qBitrr/errors.py ADDED
@@ -0,0 +1,41 @@
1
+ class qBitManagerExceptions(Exception):
2
+ """Base Exception"""
3
+
4
+
5
+ class UnhandledError(qBitManagerExceptions):
6
+ """Use to raise when there an unhandled edge case"""
7
+
8
+
9
+ class ConfigException(qBitManagerExceptions):
10
+ """Base Exception for Config related exceptions"""
11
+
12
+
13
+ class ArrManagerException(qBitManagerExceptions):
14
+ """Base Exception for Arr related Exceptions"""
15
+
16
+
17
+ class SkipException(qBitManagerExceptions):
18
+ """Dummy error to skip actions"""
19
+
20
+
21
+ class RequireConfigValue(qBitManagerExceptions):
22
+ """Exception raised when a config value requires a value."""
23
+
24
+ def __init__(self, config_class: str, config_key: str):
25
+ self.message = f"Config key '{config_key}' in '{config_class}' requires a value."
26
+
27
+
28
+ class NoConnectionrException(qBitManagerExceptions):
29
+ def __init__(self, message: str, type: str = "delay"):
30
+ self.message = message
31
+ self.type = type
32
+
33
+
34
+ class DelayLoopException(qBitManagerExceptions):
35
+ def __init__(self, length: int, type: str):
36
+ self.type = type
37
+ self.length = length
38
+
39
+
40
+ class RestartLoopException(ArrManagerException):
41
+ """Exception to trigger a loop restart"""
qBitrr/ffprobe.py ADDED
@@ -0,0 +1,105 @@
1
+ import io
2
+ import json
3
+ import logging
4
+ import os
5
+ import platform
6
+ import sys
7
+ import zipfile
8
+
9
+ import requests
10
+
11
+ from qBitrr.config import FF_PROBE, FF_VERSION, FFPROBE_AUTO_UPDATE
12
+ from qBitrr.logger import run_logs
13
+
14
+
15
+ class FFprobeDownloader:
16
+ def __init__(self):
17
+ self.api = "https://ffbinaries.com/api/v1/version/latest"
18
+ self.version_file = FF_VERSION
19
+ self.logger = logging.getLogger("qBitrr.FFprobe")
20
+ run_logs(self.logger)
21
+ self.platform = platform.system()
22
+ if self.platform == "Windows":
23
+ self.probe_path = FF_PROBE.with_suffix(".exe")
24
+ else:
25
+ self.probe_path = FF_PROBE
26
+
27
+ def get_upstream_version(self) -> dict:
28
+ with requests.Session() as session:
29
+ with session.get(self.api) as response:
30
+ if response.status_code != 200:
31
+ self.logger.warning("Failed to retrieve ffprobe version from API.")
32
+ return {}
33
+ return response.json()
34
+
35
+ def get_current_version(self):
36
+ try:
37
+ with self.version_file.open(mode="r") as file:
38
+ data = json.load(file)
39
+ return data.get("version")
40
+ except Exception: # If file can't be found or read or parsed
41
+ self.logger.warning("Failed to retrieve current ffprobe version.")
42
+ return ""
43
+
44
+ def update(self):
45
+ if not FFPROBE_AUTO_UPDATE:
46
+ return
47
+ current_version = self.get_current_version()
48
+ upstream_data = self.get_upstream_version()
49
+ upstream_version = upstream_data.get("version")
50
+ if upstream_version is None:
51
+ self.logger.debug(
52
+ "Failed to retrieve ffprobe version from API.'upstream_version' is None"
53
+ )
54
+ return
55
+ probe_file_exists = self.probe_path.exists()
56
+ if current_version == upstream_version and probe_file_exists:
57
+ self.logger.debug("Current FFprobe is up to date.")
58
+ return
59
+ arch_key = self.get_arch()
60
+ urls = upstream_data.get("bin", {}).get(arch_key)
61
+ if urls is None:
62
+ self.logger.debug("Failed to retrieve ffprobe version from API.'urls' is None")
63
+ return
64
+ ffprobe_url = urls.get("ffprobe")
65
+ self.logger.debug("Downloading newer FFprobe: %s", ffprobe_url)
66
+ self.download_and_extract(ffprobe_url)
67
+ self.logger.debug("Updating local version of FFprobe: %s", upstream_version)
68
+ self.version_file.write_text(json.dumps({"version": upstream_version}))
69
+ try:
70
+ os.chmod(self.probe_path, 0o777)
71
+ self.logger.debug("Successfully changed permissions for ffprobe")
72
+ except Exception as e:
73
+ self.logger.debug("Failed to change permissions for ffprobe, %s", e)
74
+
75
+ def download_and_extract(self, ffprobe_url):
76
+ r = requests.get(ffprobe_url)
77
+ z = zipfile.ZipFile(io.BytesIO(r.content))
78
+ self.logger.debug("Extracting downloaded FFprobe to: %s", FF_PROBE.parent)
79
+ z.extract(member=self.probe_path.name, path=FF_PROBE.parent)
80
+
81
+ def get_arch(self):
82
+ part1 = None
83
+ is_64bits = sys.maxsize > 2**32
84
+ part2 = "64" if is_64bits else "32"
85
+ if self.platform == "Windows":
86
+ part1 = "windows-"
87
+ elif self.platform == "Linux":
88
+ part1 = "linux-"
89
+ machine = platform.machine()
90
+ if machine == "armv6l":
91
+ part2 = "armhf"
92
+ elif ("arm" in machine and is_64bits) or machine == "aarch64":
93
+ part2 = "arm64"
94
+ # Else just 32/64, Not armel - because just no
95
+ elif self.platform == "Darwin":
96
+ part1 = "osx-"
97
+ part2 = "64"
98
+ if part1 is None:
99
+ raise RuntimeError(
100
+ "You are running in an unsupported platform, "
101
+ "if you expect this to be supported please open an issue on GitHub "
102
+ "https://github.com/Feramance/qBitrr."
103
+ )
104
+
105
+ return part1 + part2