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

Torrentio rate limiter improvements, realdebrid downloading improvements #62

Merged
merged 1 commit into from
Dec 18, 2023
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
3 changes: 2 additions & 1 deletion backend/program/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ class Program:
"""Program class"""

def __init__(self):
logger.info("Iceberg initializing...")
self.settings = settings_manager.get_all()
self.media_items = MediaItemContainer(items=[])
self.data_path = get_data_path()
Expand All @@ -67,7 +68,7 @@ def __init__(self):
Symlinker(self.media_items),
Scraping(self.media_items),
]
logger.info("Iceberg initialized")
logger.info("Iceberg initialized!")

def start(self):
for thread in self.threads:
Expand Down
68 changes: 54 additions & 14 deletions backend/program/debrid/realdebrid.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import threading
import time
import requests
import PTN
from requests import ConnectTimeout
from utils.logger import logger
from utils.request import get, post, ping
Expand Down Expand Up @@ -40,7 +41,7 @@ def __init__(self, media_items: MediaItemContainer):
if self._validate_settings():
self._torrents = {}
break
logger.error("Realdebrid settings incorrect, retrying in 2...")
logger.error("Realdebrid settings incorrect or not premium, retrying in 2...")
time.sleep(2)

def _validate_settings(self):
Expand All @@ -49,7 +50,9 @@ def _validate_settings(self):
"https://api.real-debrid.com/rest/1.0/user",
additional_headers=self.auth_headers,
)
return response.ok
if response.ok:
json = response.json()
return json["premium"] > 0
except ConnectTimeout:
return False

Expand Down Expand Up @@ -93,11 +96,21 @@ def download(self):

def _download(self, item):
"""Download movie from real-debrid.com"""
self.check_stream_availability(item)
self._check_stream_availability(item)
self._determine_best_stream(item)
self._download_item(item)
# item.change_state(MediaItemState.DOWNLOAD)
return 1
if not self._is_downloaded(item):
self._download_item(item)
return 1
return 0

def _is_downloaded(self, item):
if not item.get("active_stream", None):
return False
torrents = self.get_torrents()
if any(torrent.hash == item.active_stream.get("hash") for torrent in torrents):
logger.debug("Torrent already downloaded")
return True
return False

def _download_item(self, item):
if not item.get("active_stream", None):
Expand Down Expand Up @@ -167,12 +180,12 @@ def _determine_best_stream(self, item) -> bool:
item.streams = {}
return False

def check_stream_availability(self, item: MediaItem):
def _check_stream_availability(self, item: MediaItem):
if len(item.streams) == 0:
return
streams = "/".join(
list(item.streams)
) # THIS IT TO SLOW, LETS CHECK ONE STREAM AT A TIME
)
response = get(
f"https://api.real-debrid.com/rest/1.0/torrents/instantAvailability/{streams}/",
additional_headers=self.auth_headers,
Expand All @@ -184,12 +197,25 @@ def check_stream_availability(self, item: MediaItem):
continue
for containers in provider_list.values():
for container in containers:
wanted_files = {
file_id: file
for file_id, file in container.items()
if os.path.splitext(file["filename"])[1] in WANTED_FORMATS
and file["filesize"] > 50000000
}
wanted_files = None
if item.type in ["movie", "season"]:
wanted_files = {
file_id: file
for file_id, file in container.items()
if os.path.splitext(file["filename"])[1] in WANTED_FORMATS
and file["filesize"] > 50000000
}
if item.type == "episode":
for file_id, file in container.items():
parse = PTN.parse(file["filename"])
episode = parse.get("episode")
if type(episode) == list:
if item.number in episode:
wanted_files = {file_id: file}
break
elif item.number == episode:
wanted_files = {file_id: file}
break
if wanted_files:
cached = False
if item.type == "season":
Expand Down Expand Up @@ -246,6 +272,20 @@ def add_magnet(self, item: MediaItem) -> str:
return response.data.id
return None

def get_torrents(self) -> str:
"""Add magnet link to real-debrid.com"""
response = get(
"https://api.real-debrid.com/rest/1.0/torrents/",
data = {
"offset": 0,
"limit": 2500
},
additional_headers=self.auth_headers,
)
if response.is_ok:
return response.data
return None

def select_files(self, request_id, item) -> bool:
"""Select files from real-debrid.com"""
files = item.active_stream.get("files")
Expand Down
10 changes: 5 additions & 5 deletions backend/program/scrapers/torrentio.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def __init__(self, media_items: MediaItemContainer):
self.last_scrape = 0
self.filters = self.class_settings["filter"]
self.minute_limiter = RateLimiter(
max_calls=140, period=60 * 5, raise_on_limit=True
max_calls=60, period=60, raise_on_limit=True
)
self.second_limiter = RateLimiter(max_calls=1, period=1)
self.initialized = True
Expand All @@ -37,11 +37,11 @@ def run(self):
scraped_amount += self._scrape_items([item])
else:
scraped_amount += self._scrape_show(item)
except RequestException as exception:
logger.error("%s, trying again next cycle", exception)
except RequestException:
self.minute_limiter.limit_hit()
break
except RateLimitExceeded as exception:
logger.error("%s, trying again next cycle", exception)
except RateLimitExceeded:
self.minute_limiter.limit_hit()
break

if scraped_amount > 0:
Expand Down
43 changes: 43 additions & 0 deletions backend/utils/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ def ping(url: str, timeout=10, additional_headers=None):
def get(
url: str,
timeout=10,
data=None,
additional_headers=None,
retry_if_failed=True,
response_type=SimpleNamespace,
Expand All @@ -103,6 +104,7 @@ def get(
return _make_request(
"GET",
url,
data=data,
timeout=timeout,
additional_headers=additional_headers,
retry_if_failed=retry_if_failed,
Expand Down Expand Up @@ -160,7 +162,36 @@ class RateLimitExceeded(Exception):
pass


import time
from threading import Lock

class RateLimiter:
"""
A rate limiter class that limits the number of calls within a specified period.

Args:
max_calls (int): The maximum number of calls allowed within the specified period.
period (float): The time period (in seconds) within which the calls are limited.
raise_on_limit (bool, optional): Whether to raise an exception when the rate limit is exceeded.
Defaults to False.

Attributes:
max_calls (int): The maximum number of calls allowed within the specified period.
period (float): The time period (in seconds) within which the calls are limited.
tokens (int): The number of available tokens for making calls.
last_call (float): The timestamp of the last call made.
lock (threading.Lock): A lock used for thread-safety.
raise_on_limit (bool): Whether to raise an exception when the rate limit is exceeded.

Methods:
limit_hit(): Resets the token count to 0, indicating that the rate limit has been hit.
__enter__(): Enters the rate limiter context and checks if a call can be made.
__exit__(): Exits the rate limiter context.

Raises:
RateLimitExceeded: If the rate limit is exceeded and `raise_on_limit` is set to True.
"""

def __init__(self, max_calls, period, raise_on_limit=False):
self.max_calls = max_calls
self.period = period
Expand All @@ -169,7 +200,16 @@ def __init__(self, max_calls, period, raise_on_limit=False):
self.lock = Lock()
self.raise_on_limit = raise_on_limit

def limit_hit(self):
"""
Resets the token count to 0, indicating that the rate limit has been hit.
"""
self.tokens = 0

def __enter__(self):
"""
Enters the rate limiter context and checks if a call can be made.
"""
with self.lock:
current_time = time.time()
time_since_last_call = current_time - self.last_call
Expand All @@ -190,4 +230,7 @@ def __enter__(self):
return self

def __exit__(self, exc_type, exc_value, traceback):
"""
Exits the rate limiter context.
"""
pass
Loading