Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes issues from PRs, few chores #9

Merged
merged 3 commits into from
Jan 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading