Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
itschasa committed Jun 14, 2024
0 parents commit 15d2fdb
Show file tree
Hide file tree
Showing 13 changed files with 865 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
__pycache__
*.log
9 changes: 9 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
FROM python:3.10

ADD . /home

WORKDIR /home

RUN pip install -r requirements.txt

CMD ["python", "./main.py"]
54 changes: 54 additions & 0 deletions clients/qbittorrent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import qbittorrentapi
from typing import Union

from helpers.config import SpeedrrConfig, ClientConfig
from helpers.log_loader import logger
from helpers.bit_convert import bit_conv



class qBittorrentClient:
def __init__(self, config: SpeedrrConfig, config_client: ClientConfig) -> None:
self._client = qbittorrentapi.Client(
host = config_client.url,
username = config_client.username,
password = config_client.password,
FORCE_SCHEME_FROM_HOST = True,
VERIFY_WEBUI_CERTIFICATE = config_client.https_verify
)
self._client_config = config_client
self._config = config

logger.debug(f"<qbit|{self._client_config.url}> Connecting to qBittorrent at {config_client.url}")

try:
self._client.auth_log_in()

except qbittorrentapi.LoginFailed:
raise Exception(f"<qbit|{self._client_config.url}> Failed to login to qBittorrent, check your credentials")

except qbittorrentapi.Forbidden403Error:
raise Exception(f"<qbit|{self._client_config.url}> Failed to login to qBittorrent, temporarily banned, try again later")

logger.debug(f"<qbit|{self._client_config.url}> Connected to qBittorrent")


def get_active_torrent_count(self) -> int:
"Get the number of torrents that are currently downloading or uploading."

logger.debug(f"<qbit|{self._client_config.url}> Getting active torrent count")

return sum(
1 for torrent in self._client.torrents_info()
if torrent.state_enum.is_downloading or torrent.state_enum.is_uploading
)


def set_upload_speed(self, speed: Union[int, float]) -> None:
"Set the upload speed limit for the client, in client 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')))
)

109 changes: 109 additions & 0 deletions config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# Speedrr Configuration File
# https://github.com/itschasa/speedrr


# 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
# - kb = KB/s, Kilobytes per second
# - mbit = Mbit/s, Megabits per second (default)
# - mb = MB/s, Megabytes per second
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,
# so the actual minimum upload speed will be 1 Byte/s.
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


# 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.
clients:

# The type of torrent client
# Options: qbittorrent
- type: qbittorrent

# The URL to your torrent client
url: <webui_url>

# The username and password to access your torrent client
username: <username>
password: <password>

# Whether to verify the SSL certificate of the torrent client
# If you are unsure what this means, leave it as is.
https_verify: true


# These are the modules that Speedrr will use to determine what upload speed to set.
modules:

# For monitoring Plex/Jellyfin streams, via Plex, Jellyfin, or Tautulli
# Uses the bandwidth of the streams to determine how much upload speed to deduct.
media_servers:
# Supports multiple servers
# Note: You should only use either plex or tautulli for every Plex Media Server you have.

# The type of server to get data from
# Options: plex, tautulli, jellyfin
- type: <server_type>

# The URL to your Plex/Tautulli/Jellyfin server
url: <server_url>

# PLEX ONLY, the token to access your Plex server
# Help: https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/
token: <plex_x_token>

# TAUTULLI AND JELLYFIN ONLY, the API key to access your Tautulli/Jellyfin server
# Tautulli: Settings > Web Interface > API > API key
# Jellyfin: Dashboard > Advanced > API Keys > +
api_key: <api_key>

# Whether to verify the SSL certificate of the Plex/Tautulli server
# If you are unsure what this means, leave it as is.
https_verify: true

# Bandwidth reported by Plex is multiplied by this value
# Plex will reserve a higher bandwidth than the actual stream requires all the time,
# so this value is used to reduce the reported bandwidth, if you want to.
bandwidth_multiplier: 1.0

# The interval in seconds to update the Plex stream data
update_interval: 5

# Checks if a stream is local, and if it is, it will ignore it from calculations
ignore_local_streams: true

# After a stream has been paused for this amount of seconds, it will be ignored from calculations
# Note: To disable this feature, set to -1.
ignore_paused_after: 300


# Changes the upload 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.
schedule:
# The start and end time of the schedule, in 24-hour format
# Note: Uses your machine's local timezone.
- start: "05:00"
end: "23:30"

# The days of the week to apply the schedule to
# Options: all, mon, tue, wed, thu, fri, sat, sun
# Note: If your end time goes past midnight, you should add the next day as well.
days: [all]

# 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%
upload: 60%

37 changes: 37 additions & 0 deletions helpers/arguments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import argparse
import os
import logging



def is_valid_file(parser: argparse.ArgumentParser, arg) -> str:
if not os.path.exists(arg):
parser.error(f"invalid path {arg}")
else:
return str(arg)


def load_args() -> argparse.Namespace:
argparser = argparse.ArgumentParser()
argparser.add_argument(
'--config_path',
dest='config',
help='Path to the config file',
type=lambda x: is_valid_file(argparser, x),
default=os.environ.get('SPEEDRR_CONFIG')
)
argparser.add_argument(
'--log_level',
dest='log_level',
help='Python logging level to stdout, use 10, 20, 30, 40, 50. Default is 20 (INFO)',
type=int,
default=os.environ.get('SPEEDRR_LOG_LEVEL', logging.INFO)
)
argparser.add_argument(
'--log_file_level',
dest='log_file_level',
help='Python logging level to file, use 10, 20, 30, 40, 50. Default is 30 (WARNING)',
type=int,
default=os.environ.get('SPEEDRR_LOG_FILE_LEVEL', logging.WARNING)
)
return argparser.parse_args()
17 changes: 17 additions & 0 deletions helpers/bit_convert.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from typing import Union



bit_convertion_dict = {
"bit": 1,
"b": 8,
"kbit": 1000,
"kb": 8000,
"mbit": 1000000,
"mb": 8000000,
}


def bit_conv(inp: Union[int, float], inp_type: str, out_type: str) -> float:
"Convert an input in one type to an output in another type, to 3dp."
return round(inp * bit_convertion_dict[inp_type] / bit_convertion_dict[out_type], 3)
52 changes: 52 additions & 0 deletions helpers/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from dataclasses import dataclass
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']
url: str
username: str
password: str
https_verify: bool

@dataclass(frozen=True)
class MediaServerConfig(YAMLWizard):
type: Literal['plex', 'tautulli', 'jellyfin']
url: str
https_verify: bool
bandwidth_multiplier: float
update_interval: int
ignore_local_streams: bool
ignore_paused_after: int
token: Optional[str] = None
api_key: Optional[str] = None

def __hash__(self) -> int:
return super().__hash__()

@dataclass(frozen=True)
class ScheduleConfig(YAMLWizard):
start: str
end: str
days: tuple[Literal['all', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']]
upload: Union[int, str]

@dataclass(frozen=True)
class ModulesConfig(YAMLWizard):
media_servers: Optional[List[MediaServerConfig]]
schedule: Optional[List[ScheduleConfig]]

@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


def load_config(config_file: str) -> SpeedrrConfig:
return SpeedrrConfig.from_yaml_file(config_file)
51 changes: 51 additions & 0 deletions helpers/log_loader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import logging
import datetime
from colorama import Fore
import sys
import traceback



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())
log_format = '[%(asctime)s] [%(levelname)s] %(message)s (%(filename)s:%(lineno)d)'


class ColourFormatter(logging.Formatter):
FORMATS = {
logging.DEBUG: Fore.LIGHTBLACK_EX + log_format + Fore.RESET,
logging.INFO: log_format + Fore.RESET,
logging.WARNING: Fore.YELLOW + log_format + Fore.RESET,
logging.ERROR: Fore.LIGHTRED_EX + log_format + Fore.RESET,
logging.CRITICAL: Fore.RED + log_format + Fore.RESET
}

def format(self, record):
log_fmt = self.FORMATS.get(record.levelno)
formatter = logging.Formatter(log_fmt)
return formatter.format(record)


logger = logging.getLogger(logger_name)
logger.setLevel(logging.DEBUG)

stdout_handler = logging.StreamHandler()
stdout_handler.setLevel(default_stdout_log_level)
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 handle_exception(exc_type, exc_value, exc_traceback):
if issubclass(exc_type, KeyboardInterrupt):
sys.__excepthook__(exc_type, exc_value, exc_traceback)
return

logger.error("Uncaught exception: " + ' '.join(traceback.format_exception(exc_type, exc_value, exc_traceback)))

sys.excepthook = handle_exception
Empty file added logs/_logs_saved_here_
Empty file.
Loading

0 comments on commit 15d2fdb

Please sign in to comment.