Skip to content

Commit

Permalink
Merge pull request #9 from itschasa/chasa-feat
Browse files Browse the repository at this point in the history
Fixes issues from PRs, few chores
  • Loading branch information
itschasa authored Jan 25, 2025
2 parents 3b6aeb1 + 64e1d68 commit 61f4785
Show file tree
Hide file tree
Showing 9 changed files with 103 additions and 97 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
- <i>More comming soon!</i>
- <i>More coming soon!</i>


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!
Expand Down
8 changes: 4 additions & 4 deletions clients/qbittorrent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"<qbit|{self._client_config.url}> 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"<qbit|{self._client_config.url}> 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')))
)
53 changes: 28 additions & 25 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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%

8 changes: 7 additions & 1 deletion helpers/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class ModulesConfig(YAMLWizard):

@dataclass(frozen=True)
class SpeedrrConfig(YAMLWizard):
logs_path: Optional[str]
units: Literal[
'bit',
'B',
Expand Down Expand Up @@ -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
16 changes: 10 additions & 6 deletions helpers/log_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)'


Expand All @@ -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):
Expand Down
File renamed without changes.
64 changes: 32 additions & 32 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand All @@ -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)
Expand All @@ -52,7 +54,7 @@


if not modules:
logger.error("No modules enabled in config, exiting")
logger.critical("No modules enabled in config, exiting")
exit()


Expand All @@ -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")

Expand All @@ -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:
Expand Down
10 changes: 5 additions & 5 deletions modules/media_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"<media_servers> 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"<media_servers> 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"<media_servers> 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):
Expand Down
39 changes: 16 additions & 23 deletions modules/schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"<schedule> 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"<schedule> Upload reduction values = {'; '.join(f'{cfg.start}-{cfg.end}: {reduction[0]}' for cfg, reduction in self.reduction_value_dict.items())}")
logger.info(f"<schedule> 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("<schedule> Starting schedule module threads")
for module_config in self._module_configs:
thread = ScheduleThread(module_config, self)
thread.daemon = True
Expand Down Expand Up @@ -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()


Expand All @@ -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"<ScheduleThread> start>end, Sleeping for {sleeping_time} seconds")
Expand Down

0 comments on commit 61f4785

Please sign in to comment.