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")