diff --git a/README.md b/README.md
index 614542e..58ba861 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@ Change your torrent client's upload speed dynamically, on certain events such as
Change your torrent client's download speed dynamically, on certain events such as:
- Time of day and day of the week
-- More comming soon!
+- More coming soon!
This script is ideal for users with limited upload speed, however anyone can use it to maximise their upload speed, whilst keeping their Plex/Jellyfin streams buffer-free! Also great to adjust the download rate during the day, in case the bandwidth is needed for something else!
diff --git a/clients/qbittorrent.py b/clients/qbittorrent.py
index 40697d2..0068954 100644
--- a/clients/qbittorrent.py
+++ b/clients/qbittorrent.py
@@ -45,18 +45,18 @@ def get_active_torrent_count(self) -> int:
def set_upload_speed(self, speed: Union[int, float]) -> None:
- "Set the upload speed limit for the client, in client units."
+ "Set the upload speed limit for the client, in config units."
logger.debug(f" Setting upload speed to {speed}{self._config.units}")
self._client.transfer_set_upload_limit(
- max(1, int(bit_conv(speed, self._config.units, 'b')))
+ max(1, int(bit_conv(speed, self._config.units, 'B')))
)
def set_download_speed(self, speed: Union[int, float]) -> None:
- "Set the download speed limit for the client, in client units."
+ "Set the download speed limit for the client, in config units."
logger.debug(f" Setting dowload speed to {speed}{self._config.units}")
self._client.transfer_set_download_limit(
- max(1, int(bit_conv(speed, self._config.units, 'b')))
+ max(1, int(bit_conv(speed, self._config.units, 'B')))
)
diff --git a/config.yaml b/config.yaml
index 3e57f0e..5dbbdbb 100644
--- a/config.yaml
+++ b/config.yaml
@@ -2,25 +2,28 @@
# https://github.com/itschasa/speedrr
+# Directory to store logs
+logs_path: ./logs/
+
# Units to be used for all speed values
# Options (smallest to largest):
-# - bit = bit/s, bits per second
-# - B = B/s, bytes per second
-# - Kbit = Kbit/s, kilobits per second
-# - Kibit = Kibit/s, kibibits per second
-# - KB = KB/s, kilobytes per second
-# - KiB = KiB/s, kibibytes per second
-# - Mbit = Mbit/s, megabits per second
-# - Mibit = Mibit/s, mebibits per second
-# - MB = MB/s, megabytes per second
-# - MiB = MiB/s, mebibytes per second (default)
-# - Gbit = Gbit/s, gigabits per second
-# - Gibit = Gibit/s, gibibits per second
-# - GB = GB/s, gigabytes per second
-# - GiB = GiB/s, gibibytes per second
+# - bit = bit/s, bits per second
+# - B = B/s, bytes per second
+# - Kbit = Kbit/s, kilobits per second
+# - Kibit = Kibit/s, kibibits per second
+# - KB = KB/s, kilobytes per second
+# - KiB = KiB/s, kibibytes per second
+# - Mbit = Mbit/s, megabits per second
+# - Mibit = Mibit/s, mebibits per second
+# - MB = MB/s, megabytes per second
+# - MiB = MiB/s, mebibytes per second
+# - Gbit = Gbit/s, gigabits per second
+# - Gibit = Gibit/s, gibibits per second
+# - GB = GB/s, gigabytes per second
+# - GiB = GiB/s, gibibytes per second
# Full unit names (e.g. kilobyte) can be used as well
# Note: Capitalization matters for the acronyms above.
-units: MiB
+units: Mbit
# The minimum upload speed allowed on your torrent client.
# Note: Most torrent clients won't allow you to set the upload speed to 0,
@@ -31,14 +34,14 @@ min_upload: 8
# This should be around 70-80% of your total upload speed.
max_upload: 15
-# The minimum dowload speed allowed on your torrent client.
+# The minimum download speed allowed on your torrent client.
# Note: Most torrent clients won't allow you to set the upload speed to 0,
# so the actual minimum upload speed will be 1 Byte/s.
-min_download: 8
+min_download: 10
-# The maximum dowload speed allowed on your torrent client.
-# This should be around 80-100% of your total dowload speed.
-max_download: 80
+# The maximum download speed allowed on your torrent client.
+# This should be around 70-100% of your total download speed.
+max_download: 100
# The torrent clients to be used by Speedrr
# Note: If you have multiple clients, Speedrr will split the upload speed between them, based on the number of seeding+downloading torrents.
@@ -105,7 +108,7 @@ modules:
ignore_paused_after: 300
- # Changes the upload speed based on the time of day, and what day of the week
+ # Changes the upload/download speed based on the time of day, and what day of the week
# Note: Recommended to use to set your upload speed to be lower during the day,
# when lots of users are using your internet.
# Note: Supports multiple schedules.
@@ -122,11 +125,11 @@ modules:
# The upload speed deducted in this time period.
# Note: This can be a percentage of the maximum or a fixed value (uses units specified at the top of config).
- # Example: 50%, 10, 5, 80%, 20%
+ # Example: 50%, 10, 5, 80%, 20%, 0
upload: 60%
- # The dowload speed deducted in this time period.
+ # The download speed deducted in this time period.
# Note: This can be a percentage of the maximum or a fixed value (uses units specified at the top of config).
- # Example: 50%, 10, 5, 80%, 20%
- download: 80%
+ # Example: 50%, 10, 5, 80%, 20%, 0
+ download: 40%
diff --git a/helpers/config.py b/helpers/config.py
index 9b21a7b..f2593cf 100644
--- a/helpers/config.py
+++ b/helpers/config.py
@@ -41,6 +41,7 @@ class ModulesConfig(YAMLWizard):
@dataclass(frozen=True)
class SpeedrrConfig(YAMLWizard):
+ logs_path: Optional[str]
units: Literal[
'bit',
'B',
@@ -72,9 +73,14 @@ class SpeedrrConfig(YAMLWizard):
]
min_upload: int
max_upload: int
+ min_download: int
+ max_download: int
clients: List[ClientConfig]
modules: ModulesConfig
def load_config(config_file: str) -> SpeedrrConfig:
- return SpeedrrConfig.from_yaml_file(config_file)
+ config = SpeedrrConfig.from_yaml_file(config_file)
+ if isinstance(config, list):
+ raise ValueError("Config can't be a list")
+ return config
diff --git a/helpers/log_loader.py b/helpers/log_loader.py
index 6f57fde..e84be5e 100644
--- a/helpers/log_loader.py
+++ b/helpers/log_loader.py
@@ -3,13 +3,13 @@
from colorama import Fore
import sys
import traceback
+import pathlib
logger_name = "speedrr"
default_stdout_log_level = logging.INFO
-default_file_log_level = logging.WARNING
-file_log_name = 'logs/{:%Y-%m-%d %H.%M.%S}.log'.format(datetime.datetime.now())
+file_log_name = '{:%Y-%m-%d %H.%M.%S}.log'.format(datetime.datetime.now())
log_format = '[%(asctime)s] [%(levelname)s] %(message)s (%(filename)s:%(lineno)d)'
@@ -36,10 +36,14 @@ def format(self, record):
stdout_handler.setFormatter(ColourFormatter())
logger.addHandler(stdout_handler)
-file_handler = logging.FileHandler(file_log_name, encoding="utf-8")
-file_handler.setLevel(default_file_log_level)
-file_handler.setFormatter(logging.Formatter(log_format))
-logger.addHandler(file_handler)
+def set_file_handler(folder: str, level: int) -> None:
+ path = pathlib.Path(folder)
+ path.mkdir(parents=True, exist_ok=True)
+
+ file_handler = logging.FileHandler(str(pathlib.Path(folder, file_log_name)), encoding="utf-8")
+ file_handler.setLevel(level)
+ file_handler.setFormatter(logging.Formatter(log_format))
+ logger.addHandler(file_handler)
def handle_exception(exc_type, exc_value, exc_traceback):
if issubclass(exc_type, KeyboardInterrupt):
diff --git a/logs/_logs_saved_here_ b/logs/.gitkeep
similarity index 100%
rename from logs/_logs_saved_here_
rename to logs/.gitkeep
diff --git a/main.py b/main.py
index dd9c1bf..70ca22c 100644
--- a/main.py
+++ b/main.py
@@ -12,16 +12,18 @@
if __name__ == '__main__':
args = arguments.load_args()
- log_loader.file_handler.setLevel(args.log_file_level)
- log_loader.stdout_handler.setLevel(args.log_level)
-
logger.debug("Loading config")
if not args.config:
- logger.error("No config file specified, use --config_path arg or SPEEDRR_CONFIG env var to specify a config file.")
+ logger.critical("No config file specified, use --config_path arg or SPEEDRR_CONFIG env var to specify a config file.")
exit()
cfg = config.load_config(args.config)
+
+ if cfg.logs_path:
+ log_loader.set_file_handler(cfg.logs_path, args.log_file_level)
+
+ log_loader.stdout_handler.setLevel(args.log_level)
logger.info("Starting Speedrr")
@@ -35,7 +37,7 @@
torrent_client = qbittorrent.qBittorrentClient(cfg, client)
else:
- logger.error(f"Unknown client type in config: {client.type}")
+ logger.critical(f"Unknown client type in config: {client.type}")
exit()
clients.append(torrent_client)
@@ -52,7 +54,7 @@
if not modules:
- logger.error("No modules enabled in config, exiting")
+ logger.critical("No modules enabled in config, exiting")
exit()
@@ -77,12 +79,24 @@
logger.info("Update event triggered")
try:
+ module_reduction_values = [
+ module.get_reduction_value()
+ for module in modules
+ ]
+
+ # These are in the config's units
new_upload_speed = max(
cfg.min_upload,
- (cfg.max_upload - sum(module.get_reduction_value() for module in modules))
- ) # This is in the config's units
+ (cfg.max_upload - sum(module[0] for module in module_reduction_values))
+ )
+
+ new_download_speed = max(
+ cfg.min_download,
+ (cfg.max_download - sum(module[1] for module in module_reduction_values))
+ )
logger.info(f"New calculated upload speed: {new_upload_speed}{cfg.units}")
+ logger.info(f"New calculated download speed: {new_download_speed}{cfg.units}")
logger.info("Getting active torrent counts")
@@ -94,37 +108,23 @@
sum_active_torrents = sum(client_active_torrent_dict.values())
for torrent_client, active_torrent_count in client_active_torrent_dict.items():
+ # If there are no active torrents, set the upload speed to the new speed
+ effective_upload_speed = (active_torrent_count / sum_active_torrents * new_upload_speed) if active_torrent_count > 0 else new_upload_speed
+ effective_download_speed = (active_torrent_count / sum_active_torrents * new_download_speed) if active_torrent_count > 0 else new_download_speed
+
try:
- if active_torrent_count == 0:
- # if there are no active torrents, set the upload speed to the new speed
- torrent_client.set_upload_speed(new_upload_speed)
- else:
- torrent_client.set_upload_speed((active_torrent_count / sum_active_torrents * new_upload_speed))
+ torrent_client.set_upload_speed(effective_upload_speed)
+ torrent_client.set_download_speed(effective_download_speed)
except Exception:
- logger.warn(f"An error occurred while updating {torrent_client._client_config.url}, skipping:\n" + traceback.format_exc())
+ logger.warning(f"An error occurred while updating {torrent_client._client_config.url}, skipping:\n" + traceback.format_exc())
else:
- logger.info(f"Set upload speed for {torrent_client._client_config.url} to {new_upload_speed}{cfg.units}")
+ logger.info(f"Set upload speed for {torrent_client._client_config.url} to {effective_upload_speed}{cfg.units}")
+ logger.info(f"Set download speed for {torrent_client._client_config.url} to {effective_download_speed}{cfg.units}")
- logger.info("Upload speeds updated")
-
- new_dowload_speed = max(
- cfg.min_download,
- (cfg.max_download - sum(module.get_reduction_value() for module in modules))
- ) # This is in the config's units
-
- logger.info(f"New calculated dowload speed: {new_dowload_speed}{cfg.units}")
-
- try:
- torrent_client.set_download_speed(new_dowload_speed)
-
- except Exception:
- logger.warn(f"An error occurred while updating {torrent_client._client_config.url}, skipping:\n" + traceback.format_exc())
-
- else:
- logger.info(f"Set dowload speed for {torrent_client._client_config.url} to {new_dowload_speed}{cfg.units}")
+ logger.info("Speeds updated")
except Exception:
diff --git a/modules/media_server.py b/modules/media_server.py
index e4a6e5c..81a221b 100644
--- a/modules/media_server.py
+++ b/modules/media_server.py
@@ -32,17 +32,17 @@ def __init__(self, config: SpeedrrConfig, module_config: List[MediaServerConfig]
self.servers.append(JellyfinServer(config, server, self))
else:
- logger.error(f"Unknown media server type in config: {server.type}")
+ logger.critical(f" Unknown media server type in config: {server.type}")
exit()
self.servers[-1].get_bandwidth()
- def get_reduction_value(self) -> float:
- "How much to reduce the upload speed by, in the config's units."
+ def get_reduction_value(self) -> tuple[float, float]:
+ "How much to reduce the speed by, in the config's units. Returns a tuple of `(upload, download)`."
- logger.info(f" Reduction values = {'; '.join(f'{server.url}: {reduction}' for server, reduction in self.reduction_value_dict.items())}")
- return sum(self.reduction_value_dict.values())
+ logger.info(f" Upload reduction values = {'; '.join(f'{server.url}: {reduction}' for server, reduction in self.reduction_value_dict.items())}")
+ return sum(self.reduction_value_dict.values()), 0
def run(self):
diff --git a/modules/schedule.py b/modules/schedule.py
index e3facb0..f77cf3d 100644
--- a/modules/schedule.py
+++ b/modules/schedule.py
@@ -12,24 +12,29 @@ class ScheduleModule:
"A module that manages schedules."
def __init__(self, config: SpeedrrConfig, module_configs: List[ScheduleConfig], update_event: threading.Event) -> None:
- self.reduction_value_dict: dict[ScheduleConfig, float] = {}
+ self.reduction_value_dict: dict[ScheduleConfig, tuple[float, float]] = {}
self._config = config
self._module_configs = module_configs
self._update_event = update_event
- def get_reduction_value(self) -> float:
- "How much to reduce the upload speed by, in the config's units."
+ def get_reduction_value(self) -> tuple[float, float]:
+ "How much to reduce the speed by, in the config's units. Returns a tuple of `(upload, download)`."
- logger.info(f" Reduction values = {'; '.join(f'{cfg.start}-{cfg.end}: {reduction}' for cfg, reduction in self.reduction_value_dict.items())}")
- return sum(self.reduction_value_dict.values())
+ logger.info(f" Upload reduction values = {'; '.join(f'{cfg.start}-{cfg.end}: {reduction[0]}' for cfg, reduction in self.reduction_value_dict.items())}")
+ logger.info(f" Download reduction values = {'; '.join(f'{cfg.start}-{cfg.end}: {reduction[1]}' for cfg, reduction in self.reduction_value_dict.items())}")
+
+ return (
+ sum([reduction[0] for reduction in self.reduction_value_dict.values()]),
+ sum([reduction[1] for reduction in self.reduction_value_dict.values()]),
+ )
def run(self) -> None:
"Start the schedule threads."
- logger.debug("Starting schedule module threads")
+ logger.debug(" Starting schedule module threads")
for module_config in self._module_configs:
thread = ScheduleThread(module_config, self)
thread.daemon = True
@@ -90,25 +95,14 @@ def calculate_next_occurrence(self, hour: int, minute: int) -> datetime:
return datetime(now.year, now.month, now.day + 7 - current_day + self._days_as_int[0], hour, minute, tzinfo=self.timezone)
- def set_reduction_download(self):
- "Set the reduction download value for the module, and dispatches an update event."
-
- reduction_value = self._module.reduction_value_dict.get(self._config)
- if reduction_value == self._download_reduce_by:
- return
-
- self._module.reduction_value_dict[self._config] = self._download_reduce_by
- self._module._update_event.set()
-
-
- def set_reduction_upload(self):
- "Set the reduction upload value for the module, and dispatches an update event."
+ def set_reduction(self):
+ "Set the reduction value for the module, and dispatches an update event."
reduction_value = self._module.reduction_value_dict.get(self._config)
- if reduction_value == self._upload_reduce_by:
+ if reduction_value == (self._upload_reduce_by, self._download_reduce_by):
return
- self._module.reduction_value_dict[self._config] = self._upload_reduce_by
+ self._module.reduction_value_dict[self._config] = (self._upload_reduce_by, self._download_reduce_by)
self._module._update_event.set()
@@ -132,8 +126,7 @@ def run(self) -> None:
if next_start_occurrence > next_end_occurrence:
# currently between the start and end time
- self.set_reduction_upload()
- self.set_reduction_download()
+ self.set_reduction()
sleeping_time = (next_end_occurrence - datetime.now(tz=self.timezone)).total_seconds()
logger.debug(f" start>end, Sleeping for {sleeping_time} seconds")