Skip to content

Commit

Permalink
feat: requests second pass (#848)
Browse files Browse the repository at this point in the history
* feat: add support for HTTP method and response type enums

- Introduced `HttpMethod` and `ResponseType` enums to standardize HTTP method usage and response handling.
- Updated `BaseRequestHandler` and `ResponseObject` to utilize the new enums for improved code clarity and flexibility.
- Modified `RealDebridRequestHandler` and `AllDebridRequestHandler` to use `HttpMethod` and `ResponseType` enums.
- Enhanced request handling in downloader classes to support the new enums, ensuring consistent API interactions.

* refactor: implement request handler classes for API communication

- Introduced new request handler classes for various APIs to streamline HTTP request handling.
- Replaced direct requests with handler executions across multiple modules, including TraktAPI, OverseerrAPI, and others.
- Enhanced error handling by defining custom exceptions for each API.
- Updated session creation and rate limiting configurations for improved performance and reliability.
- Improved code readability and maintainability by encapsulating request logic within dedicated classes.

* fix: handle missing 'streams' attribute in Torrentio response

- Added a check for the presence of the 'streams' attribute in the Torrentio scraper response to prevent errors when the attribute is missing.
- Updated the MdblistRequestHandler to include the API key in the request parameters by default, simplifying API calls.
- Improved error handling in the ListrrAPI to break on specific HTTP errors.
- Enhanced logging for better debugging and error tracking across various modules.

* fix: jackett exception handling ordering

* feat: add support for overriding response type in request handlers

- Introduced an optional `overriden_response_type` parameter in the `_request` method of `BaseRequestHandler` and `execute` method of `PlexRequestHandler`.
- Updated `get_items_from_rss` in `PlexAPI` to use `ResponseType.DICT` for RSS feed requests.
- Enhanced flexibility in handling different response formats dynamically.

* fix: remove retry_if_failed parameter from request handler calls in Orionoid scraper

* fix: correct parameter name in Plex RSS feed request handler call
  • Loading branch information
iPromKnight authored Nov 4, 2024
1 parent 6d01407 commit d41c2ff
Show file tree
Hide file tree
Showing 21 changed files with 320 additions and 282 deletions.
24 changes: 17 additions & 7 deletions src/program/apis/listrr_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,17 @@

from program.apis.trakt_api import TraktAPI
from program.media.item import MediaItem
from program.utils.request import get, ping, create_service_session
from program.utils.request import create_service_session, BaseRequestHandler, Session, ResponseType, ResponseObject, HttpMethod

class ListrrAPIError(Exception):
"""Base exception for ListrrAPI related errors"""

class ListrrRequestHandler(BaseRequestHandler):
def __init__(self, session: Session, base_url: str, request_logging: bool = False):
super().__init__(session, base_url=base_url, response_type=ResponseType.SIMPLE_NAMESPACE, custom_exception=ListrrAPIError, request_logging=request_logging)

def execute(self, method: HttpMethod, endpoint: str, **kwargs) -> ResponseObject:
return super()._request(method, endpoint, **kwargs)

class ListrrAPI:
"""Handles Listrr API communication"""
Expand All @@ -13,12 +22,13 @@ def __init__(self, api_key: str):
self.BASE_URL = "https://listrr.pro"
self.api_key = api_key
self.headers = {"X-Api-Key": self.api_key}
self.session = create_service_session()
self.session.headers.update(self.headers)
session = create_service_session()
session.headers.update(self.headers)
self.request_handler = ListrrRequestHandler(session, base_url=self.BASE_URL)
self.trakt_api = TraktAPI(rate_limit=False)

def validate(self):
return ping(session=self.session, url=self.BASE_URL)
return self.request_handler.execute(HttpMethod.GET, self.BASE_URL)

def get_items_from_Listrr(self, content_type, content_lists) -> list[MediaItem] | list[str]: # noqa: C901, PLR0912
"""Fetch unique IMDb IDs from Listrr for a given type and list of content."""
Expand All @@ -33,9 +43,9 @@ def get_items_from_Listrr(self, content_type, content_lists) -> list[MediaItem]
page, total_pages = 1, 1
while page <= total_pages:
try:
url = f"{self.BASE_URL}/api/List/{content_type}/{list_id}/ReleaseDate/Descending/{page}"
response = get(session=self.session, url=url).response
data = response.json()
url = f"api/List/{content_type}/{list_id}/ReleaseDate/Descending/{page}"
response = self.request_handler.execute(HttpMethod.GET, url)
data = response.data
total_pages = data.get("pages", 1)
for item in data.get("items", []):
imdb_id = item.get("imDbId")
Expand Down
33 changes: 19 additions & 14 deletions src/program/apis/mdblist_api.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,42 @@
from program.utils.request import get_rate_limit_params, create_service_session, get, ping
from program.utils.request import get_rate_limit_params, create_service_session, BaseRequestHandler, Session, ResponseType, ResponseObject, HttpMethod


class MdblistAPIError(Exception):
"""Base exception for MdblistAPI related errors"""

class MdblistRequestHandler(BaseRequestHandler):
def __init__(self, session: Session, base_url: str, api_key: str, request_logging: bool = False):
self.api_key = api_key
super().__init__(session, base_url=base_url, response_type=ResponseType.SIMPLE_NAMESPACE, custom_exception=MdblistAPIError, request_logging=request_logging)

def execute(self, method: HttpMethod, endpoint: str, ignore_base_url: bool = False, **kwargs) -> ResponseObject:
return super()._request(method, endpoint, ignore_base_url=ignore_base_url, params={"apikey": self.api_key}, **kwargs)


class MdblistAPI:
"""Handles Mdblist API communication"""
BASE_URL = "https://mdblist.com"

def __init__(self, api_key: str):
self.api_key = api_key

rate_limit_params = get_rate_limit_params(per_minute=60)

self.session = create_service_session(
rate_limit_params=rate_limit_params,
use_cache=False
)
session = create_service_session(rate_limit_params=rate_limit_params)
self.request_handler = MdblistRequestHandler(session, base_url=self.BASE_URL, api_key=api_key)

def validate(self):
return ping(session=self.session, url=f"{self.BASE_URL}/api/user?apikey={self.api_key}")
return self.request_handler.execute(HttpMethod.GET, f"api/user")

def my_limits(self):
"""Wrapper for mdblist api method 'My limits'"""
response = get(session=self.session, url=f"{self.BASE_URL}/api/user?apikey={self.api_key}")
response = self.request_handler.execute(HttpMethod.GET,f"api/user")
return response.data

def list_items_by_id(self, list_id: int):
"""Wrapper for mdblist api method 'List items'"""
response = get(session=self.session,
url=f"{self.BASE_URL}/api/lists/{str(list_id)}/items?apikey={self.api_key}"
)
response = self.request_handler.execute(HttpMethod.GET,f"api/lists/{str(list_id)}/items")
return response.data

def list_items_by_url(self, url: str):
url = url if url.endswith("/") else f"{url}/"
url = url if url.endswith("json/") else f"{url}json/"
response = get(session=self.session, url=url, params={"apikey": self.api_key})
response = self.request_handler.execute(HttpMethod.GET, url, ignore_base_url=True)
return response.data
55 changes: 21 additions & 34 deletions src/program/apis/overseerr_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,32 +7,38 @@
from program.apis.trakt_api import TraktAPI
from program.media.item import MediaItem
from program.settings.manager import settings_manager
from program.utils.request import delete, get, ping, post, get_rate_limit_params, create_service_session
from program.utils.request import BaseRequestHandler, Session, ResponseType, HttpMethod, ResponseObject, get_rate_limit_params, create_service_session

class OverseerrAPIError(Exception):
"""Base exception for OverseerrAPI related errors"""

class OverseerrRequestHandler(BaseRequestHandler):
def __init__(self, session: Session, base_url: str, request_logging: bool = False):
super().__init__(session, base_url=base_url, response_type=ResponseType.SIMPLE_NAMESPACE, custom_exception=OverseerrAPIError, request_logging=request_logging)

def execute(self, method: HttpMethod, endpoint: str, **kwargs) -> ResponseObject:
return super()._request(method, endpoint, **kwargs)


class OverseerrAPI:
"""Handles Overseerr API communication"""

def __init__(self, api_key: str, base_url: str):
self.api_key = api_key
self.BASE_URL = base_url
rate_limit_params = get_rate_limit_params(max_calls=1000, period=300)
self.session = create_service_session(rate_limit_params=rate_limit_params, use_cache=False)
session = create_service_session(rate_limit_params=rate_limit_params)
self.trakt_api = TraktAPI(rate_limit=False)
self.headers = {"X-Api-Key": self.api_key}
self.session.headers.update(self.headers)
session.headers.update(self.headers)
self.request_handler = OverseerrRequestHandler(session, base_url=base_url)

def validate(self):
return ping(
session=self.session,
url=self.BASE_URL + "/api/v1/auth/me",
timeout=30,
)
return self.request_handler.execute(HttpMethod.GET, "api/v1/auth/me", timeout=30)

def get_media_requests(self, service_key: str) -> list[MediaItem]:
"""Get media requests from `Overseerr`"""
try:
response = get(session=self.session, url=self.BASE_URL + f"/api/v1/request?take={10000}&filter=approved&sort=added")
response = self.request_handler.execute(HttpMethod.GET, f"api/v1/request?take={10000}&filter=approved&sort=added")
if not response.is_ok:
logger.error(f"Failed to fetch requests from overseerr: {response.data}")
return []
Expand Down Expand Up @@ -82,10 +88,7 @@ def get_imdb_id(self, data) -> str | None:
external_id = data.tmdbId

try:
response = get(
session=self.session,
url=self.BASE_URL + f"/api/v1/{data.mediaType}/{external_id}?language=en",
)
response = self.request_handler.execute(HttpMethod.GET, f"api/v1/{data.mediaType}/{external_id}?language=en")
except (ConnectionError, RetryError, MaxRetryError) as e:
logger.error(f"Failed to fetch media details from overseerr: {str(e)}")
return None
Expand Down Expand Up @@ -122,11 +125,7 @@ def delete_request(self, mediaId: int) -> bool:
settings = settings_manager.settings.content.overseerr
headers = {"X-Api-Key": settings.api_key}
try:
response = delete(
session=self.session,
url=self.BASE_URL + f"/api/v1/request/{mediaId}",
additional_headers=headers,
)
response = self.request_handler.execute(HttpMethod.DELETE, f"api/v1/request/{mediaId}", additional_headers=headers)
logger.debug(f"Deleted request {mediaId} from overseerr")
return response.is_ok == True
except Exception as e:
Expand All @@ -136,11 +135,7 @@ def delete_request(self, mediaId: int) -> bool:
def mark_processing(self, mediaId: int) -> bool:
"""Mark item as processing in overseerr"""
try:
response = post(
session=self.session,
url=self.BASE_URL + f"/api/v1/media/{mediaId}/pending",
data={"is4k": False},
)
response = self.request_handler.execute(HttpMethod.POST, f"api/v1/media/{mediaId}/pending", data={"is4k": False})
logger.info(f"Marked media {mediaId} as processing in overseerr")
return response.is_ok
except Exception as e:
Expand All @@ -150,11 +145,7 @@ def mark_processing(self, mediaId: int) -> bool:
def mark_partially_available(self, mediaId: int) -> bool:
"""Mark item as partially available in overseerr"""
try:
response = post(
session=self.session,
url=self.BASE_URL + f"/api/v1/media/{mediaId}/partial",
data={"is4k": False},
)
response = self.request_handler.execute(HttpMethod.POST, f"api/v1/media/{mediaId}/partial", data={"is4k": False})
logger.info(f"Marked media {mediaId} as partially available in overseerr")
return response.is_ok
except Exception as e:
Expand All @@ -164,11 +155,7 @@ def mark_partially_available(self, mediaId: int) -> bool:
def mark_completed(self, mediaId: int) -> bool:
"""Mark item as completed in overseerr"""
try:
response = post(
session=self.session,
url=self.BASE_URL + f"/api/v1/media/{mediaId}/available",
data={"is4k": False},
)
response = self.request_handler.execute(HttpMethod.POST, f"api/v1/media/{mediaId}/available", data={"is4k": False})
logger.info(f"Marked media {mediaId} as completed in overseerr")
return response.is_ok
except Exception as e:
Expand Down
31 changes: 21 additions & 10 deletions src/program/apis/plex_api.py
Original file line number Diff line number Diff line change
@@ -1,46 +1,57 @@
from typing import List, Optional, Dict, Union
from loguru import logger
from requests import Session
from plexapi.myplex import MyPlexAccount
from plexapi.server import PlexServer
from plexapi.library import LibrarySection

from program.media import Movie, Episode
from program.settings.manager import settings_manager
from program.utils.request import get, ping, create_service_session
from program.utils.request import create_service_session, BaseRequestHandler, HttpMethod, ResponseType, ResponseObject


class PlexAPIError(Exception):
"""Base exception for PlexApi related errors"""

class PlexRequestHandler(BaseRequestHandler):
def __init__(self, session: Session, request_logging: bool = False):
super().__init__(session, response_type=ResponseType.SIMPLE_NAMESPACE, custom_exception=PlexAPIError, request_logging=request_logging)

def execute(self, method: HttpMethod, endpoint: str, overriden_response_type: ResponseType = None, **kwargs) -> ResponseObject:
return super()._request(method, endpoint, overriden_response_type=overriden_response_type, **kwargs)

class PlexAPI:
"""Handles Plex API communication"""

def __init__(self, token: str, base_url: str, rss_urls: Optional[List[str]]):
self.BASE_URL = base_url
self.rss_urls = rss_urls
self.token = token
self.session = create_service_session()
self.BASE_URL = base_url
session = create_service_session()
self.request_handler = PlexRequestHandler(session)
self.account = None
self.plex_server = None
self.rss_enabled = False

def validate_account(self):
try:
self.account = MyPlexAccount(session=self.session, token=self.token)
self.account = MyPlexAccount(session=self.request_handler.session, token=self.token)
except Exception as e:
logger.error(f"Failed to authenticate Plex account: {e}")
return False
return True

def validate_server(self):
self.plex_server = PlexServer(self.BASE_URL, token=self.token, session=self.session, timeout=60)
self.plex_server = PlexServer(self.BASE_URL, token=self.token, session=self.request_handler.session, timeout=60)

def validate_rss(self, url: str):
return ping(session=self.session, url=url)
return self.request_handler.execute(HttpMethod.GET, url)

def ratingkey_to_imdbid(self, ratingKey: str) -> str | None:
"""Convert Plex rating key to IMDb ID"""
token = settings_manager.settings.updaters.plex.token
filter_params = "includeGuids=1&includeFields=guid,title,year&includeElements=Guid"
url = f"https://metadata.provider.plex.tv/library/metadata/{ratingKey}?X-Plex-Token={token}&{filter_params}"
response = get(session=self.session, url=url)
response = self.request_handler.execute(HttpMethod.GET, url)
if response.is_ok and hasattr(response.data, "MediaContainer"):
metadata = response.data.MediaContainer.Metadata[0]
return next((guid.id.split("//")[-1] for guid in metadata.Guid if "imdb://" in guid.id), None)
Expand All @@ -52,8 +63,8 @@ def get_items_from_rss(self) -> list[str]:
rss_items: list[str] = []
for rss_url in self.rss_urls:
try:
response = self.session.get(rss_url + "?format=json", timeout=60)
for _item in response.json().get("items", []):
response = self.request_handler.execute(HttpMethod.GET, rss_url + "?format=json", overriden_response_type=ResponseType.DICT, timeout=60)
for _item in response.data.get("items", []):
imdb_id = self.extract_imdb_ids(_item.get("guids", []))
if imdb_id and imdb_id.startswith("tt"):
rss_items.append(imdb_id)
Expand Down
Loading

0 comments on commit d41c2ff

Please sign in to comment.