Skip to content

Commit

Permalink
feat: download speed control (#4)
Browse files Browse the repository at this point in the history
* added option to control download speeds

* fixed download dependencies

* fixed missing set download

* updated README.md

* overriding the upload should be fixed

* fixed issue with set_reduction

* added default values

* updated to plausible default values
  • Loading branch information
Floo33R authored Jan 25, 2025
1 parent b5a54c8 commit bd3def7
Show file tree
Hide file tree
Showing 6 changed files with 80 additions and 18 deletions.
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
<p align="center">
<img src="https://raw.githubusercontent.com/itschasa/speedrr/master/images/speedrr_text.png" alt="speedrr" width="336" height="84">
<br/>
<h1>speedrr - Dynamic Upload Speed Manager for Torrenting</h1>
<h1>speedrr - Dynamic Upload and Download Speed Manager for Torrenting</h1>
</p>

Change your torrent client's upload speed dynamically, on certain events such as:
- When a Plex/Jellyfin stream starts
- Time of day and day of the week
- <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!

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>


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!


## Features
Expand Down
8 changes: 8 additions & 0 deletions clients/qbittorrent.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,11 @@ def set_upload_speed(self, speed: Union[int, float]) -> None:
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."

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')))
)
15 changes: 14 additions & 1 deletion config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,16 @@ min_upload: 8

# The maximum upload speed allowed on your torrent client.
# This should be around 70-80% of your total upload speed.
max_upload: 75
max_upload: 15

# The minimum dowload 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

# The maximum dowload speed allowed on your torrent client.
# This should be around 80-100% of your total dowload speed.
max_download: 80

# 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 @@ -107,3 +115,8 @@ modules:
# Example: 50%, 10, 5, 80%, 20%
upload: 60%

# The dowload 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%

16 changes: 9 additions & 7 deletions helpers/config.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
from dataclasses import dataclass
from dataclasses import dataclass, field
from typing import List, Optional, Union, Literal
from dataclass_wizard import YAMLWizard # type: ignore



@dataclass(frozen=True)
class ClientConfig(YAMLWizard):
type: Literal['qbittorrent', 'deluge', 'transmission']
Expand Down Expand Up @@ -33,6 +32,7 @@ class ScheduleConfig(YAMLWizard):
end: str
days: tuple[Literal['all', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']]
upload: Union[int, str]
download: Union[int, str]

@dataclass(frozen=True)
class ModulesConfig(YAMLWizard):
Expand All @@ -41,11 +41,13 @@ class ModulesConfig(YAMLWizard):

@dataclass(frozen=True)
class SpeedrrConfig(YAMLWizard):
units: Literal['bit', 'b', 'kbit', 'kb', 'mbit', 'mb']
min_upload: int
max_upload: int
clients: List[ClientConfig]
modules: ModulesConfig
units: Literal['bit', 'b', 'kbit', 'kb', 'mbit', 'mb'] = 'mb'
min_upload: int = 0
max_upload: int = 50
min_download: int = 0
max_download: int = 100
clients: List[ClientConfig] = field(default_factory=list)
modules: ModulesConfig = field(default_factory=ModulesConfig)


def load_config(config_file: str) -> SpeedrrConfig:
Expand Down
16 changes: 16 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,22 @@

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


except Exception:
logger.error("An error occurred while updating clients:\n" + traceback.format_exc())
Expand Down
33 changes: 25 additions & 8 deletions modules/schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,16 @@ def __init__(self, config: ScheduleConfig, module: ScheduleModule) -> None:
break

self._days_as_int.append(['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'].index(day))

if isinstance(self._config.upload, str):
self._reduce_by = int(self._config.upload[:-1]) / 100 * self._module._config.max_upload
self._upload_reduce_by = int(self._config.upload[:-1]) / 100 * self._module._config.max_upload
else:
self._upload_reduce_by = self._config.upload

if isinstance(self._config.download, str):
self._download_reduce_by = int(self._config.download[:-1]) / 100 * self._module._config.max_upload
else:
self._reduce_by = self._config.upload
self._download_reduce_by = self._config.download

self.timezone = datetime.now(timezone.utc).astimezone().tzinfo
logger.info("<ScheduleThread> Using timezone: %s", self.timezone)
Expand All @@ -85,17 +90,28 @@ 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(self):
"Set the reduction value for the module, and dispatches an update event."
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._reduce_by:
if reduction_value == self._download_reduce_by:
return

self._module.reduction_value_dict[self._config] = self._reduce_by
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."

reduction_value = self._module.reduction_value_dict.get(self._config)
if reduction_value == self._upload_reduce_by:
return

self._module.reduction_value_dict[self._config] = self._upload_reduce_by
self._module._update_event.set()


def remove_reduction(self):
"Remove the reduction value for the module, and dispatches an update event."

Expand All @@ -116,7 +132,8 @@ def run(self) -> None:

if next_start_occurrence > next_end_occurrence:
# currently between the start and end time
self.set_reduction()
self.set_reduction_upload()
self.set_reduction_download()

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 bd3def7

Please sign in to comment.