Refactor: add caching for FlareSolverr solutions.
This commit is contained in:
parent
6c1421b3b4
commit
47be76fd73
4 changed files with 43 additions and 16 deletions
1
FAQ.md
1
FAQ.md
|
|
@ -41,6 +41,7 @@ or the `environment:` section in `compose.yaml` file.
|
||||||
| YTP_FLARESOLVERR_URL | FlareSolverr endpoint URL. | `(not_set)` |
|
| YTP_FLARESOLVERR_URL | FlareSolverr endpoint URL. | `(not_set)` |
|
||||||
| YTP_FLARESOLVERR_MAX_TIMEOUT | Max FlareSolverr challenge timeout in seconds | `120` |
|
| YTP_FLARESOLVERR_MAX_TIMEOUT | Max FlareSolverr challenge timeout in seconds | `120` |
|
||||||
| YTP_FLARESOLVERR_CLIENT_TIMEOUT | HTTP client timeout (seconds) when calling FlareSolverr | `120` |
|
| YTP_FLARESOLVERR_CLIENT_TIMEOUT | HTTP client timeout (seconds) when calling FlareSolverr | `120` |
|
||||||
|
| YTP_FLARESOLVERR_CACHE_TTL | The cache TTL (in seconds) for FlareSolverr solutions | `600` |
|
||||||
| YTP_BASE_PATH | Set this if you are serving YTPTube from sub-folder | `/` |
|
| YTP_BASE_PATH | Set this if you are serving YTPTube from sub-folder | `/` |
|
||||||
| YTP_PREVENT_LIVE_PREMIERE | Prevents the initial youtube premiere stream from being downloaded | `false` |
|
| YTP_PREVENT_LIVE_PREMIERE | Prevents the initial youtube premiere stream from being downloaded | `false` |
|
||||||
| YTP_LIVE_PREMIERE_BUFFER | buffer time in minutes to add to video duration | `5` |
|
| YTP_LIVE_PREMIERE_BUFFER | buffer time in minutes to add to video duration | `5` |
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,9 @@ from __future__ import annotations
|
||||||
import http.cookiejar
|
import http.cookiejar
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import time
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
from abc import ABC
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from typing import Any, ClassVar
|
from typing import Any, ClassVar
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
@ -25,6 +27,7 @@ from yt_dlp.utils.networking import clean_headers
|
||||||
LOG: logging.Logger = logging.getLogger(__name__)
|
LOG: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
SolverFn = Callable[[Request, Response, RequestHandler], Request | None]
|
SolverFn = Callable[[Request, Response, RequestHandler], Request | None]
|
||||||
|
CacheEntry = dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
def cf_solver(request: Request, _response: Response, handler: RequestHandler) -> Request | None:
|
def cf_solver(request: Request, _response: Response, handler: RequestHandler) -> Request | None:
|
||||||
|
|
@ -106,10 +109,15 @@ def cf_solver(request: Request, _response: Response, handler: RequestHandler) ->
|
||||||
solution = result.get("solution") or {}
|
solution = result.get("solution") or {}
|
||||||
_cookiejar_from_solution(solution.get("cookies"), request, handler)
|
_cookiejar_from_solution(solution.get("cookies"), request, handler)
|
||||||
|
|
||||||
ua = solution.get("userAgent")
|
if ua := solution.get("userAgent"):
|
||||||
if ua:
|
|
||||||
request.headers["User-Agent"] = ua
|
request.headers["User-Agent"] = ua
|
||||||
|
|
||||||
|
CFSolverRH.cache[urlparse(request.url).netloc] = {
|
||||||
|
"cookies": solution.get("cookies") or [],
|
||||||
|
"userAgent": ua,
|
||||||
|
"expires_at": time.time() + config.flaresolverr_cache_ttl,
|
||||||
|
}
|
||||||
|
|
||||||
return request
|
return request
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -163,25 +171,14 @@ def _cookiejar_from_solution(cookies, request: Request, handler: RequestHandler)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@register_preference()
|
|
||||||
def _prefer_cf_handler(handler: RequestHandler, _request: Request) -> int:
|
|
||||||
"""Prefer Cloudflare handler when configured with endpoint."""
|
|
||||||
from app.library.config import Config
|
|
||||||
|
|
||||||
if not Config.get_instance().flaresolverr_url:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
hand = getattr(handler, "RH_KEY", "")
|
|
||||||
return 1000 if hand == "CFSolver" else 0
|
|
||||||
|
|
||||||
|
|
||||||
@register_rh
|
@register_rh
|
||||||
class CFSolverRH(RequestHandler):
|
class CFSolverRH(RequestHandler, ABC):
|
||||||
"""Request handler that intercepts Cloudflare challenges"""
|
"""Request handler that intercepts Cloudflare challenges"""
|
||||||
|
|
||||||
_SUPPORTED_URL_SCHEMES = ("http", "https")
|
_SUPPORTED_URL_SCHEMES = ("http", "https")
|
||||||
_SUPPORTED_PROXY_SCHEMES = ("http", "https", "socks4", "socks4a", "socks5", "socks5h")
|
_SUPPORTED_PROXY_SCHEMES = ("http", "https", "socks4", "socks4a", "socks5", "socks5h")
|
||||||
solver: ClassVar[SolverFn | None] = None
|
solver: ClassVar[SolverFn | None] = None
|
||||||
|
cache: ClassVar[dict[str, CacheEntry]] = {}
|
||||||
|
|
||||||
def __init__(self, *, solver: SolverFn | None = None, **kwargs) -> None:
|
def __init__(self, *, solver: SolverFn | None = None, **kwargs) -> None:
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
@ -271,7 +268,22 @@ class CFSolverRH(RequestHandler):
|
||||||
response.close()
|
response.close()
|
||||||
return director.send(solved_request)
|
return director.send(solved_request)
|
||||||
|
|
||||||
|
def _inject_cookie(self, request: Request) -> None:
|
||||||
|
cache_key: str = urlparse(request.url).netloc
|
||||||
|
if not (cached := self.cache.get(cache_key)):
|
||||||
|
return
|
||||||
|
|
||||||
|
if cached.get("expires_at", 0) <= time.time():
|
||||||
|
self.cache.pop(cache_key, None)
|
||||||
|
return
|
||||||
|
|
||||||
|
LOG.info(f"Injecting cached Cloudflare cookies for '{cache_key}'.")
|
||||||
|
_cookiejar_from_solution(cached.get("cookies"), request, self)
|
||||||
|
if ua := cached.get("userAgent"):
|
||||||
|
request.headers["User-Agent"] = ua
|
||||||
|
|
||||||
def _send(self, request: Request) -> Response:
|
def _send(self, request: Request) -> Response:
|
||||||
|
self._inject_cookie(request)
|
||||||
director: RequestDirector = self._build_fallback()
|
director: RequestDirector = self._build_fallback()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -285,3 +297,13 @@ class CFSolverRH(RequestHandler):
|
||||||
return self._retry_with_clearance(request, response, director)
|
return self._retry_with_clearance(request, response, director)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@register_preference(CFSolverRH)
|
||||||
|
def cf_solver_preference(_handler: RequestHandler, _request: Request) -> int:
|
||||||
|
from app.library.config import Config
|
||||||
|
|
||||||
|
if not Config.get_instance().flaresolverr_url:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
return 1000
|
||||||
|
|
|
||||||
|
|
@ -192,6 +192,9 @@ class Config(metaclass=Singleton):
|
||||||
flaresolverr_client_timeout: int = 120
|
flaresolverr_client_timeout: int = 120
|
||||||
"""HTTP client timeout (seconds) when calling FlareSolverr."""
|
"""HTTP client timeout (seconds) when calling FlareSolverr."""
|
||||||
|
|
||||||
|
flaresolverr_cache_ttl: int = 600
|
||||||
|
"""The cache TTL (in seconds) for FlareSolverr solutions."""
|
||||||
|
|
||||||
is_native: bool = False
|
is_native: bool = False
|
||||||
"Is the application running in natively."
|
"Is the application running in natively."
|
||||||
|
|
||||||
|
|
@ -255,6 +258,7 @@ class Config(metaclass=Singleton):
|
||||||
"default_pagination",
|
"default_pagination",
|
||||||
"flaresolverr_max_timeout",
|
"flaresolverr_max_timeout",
|
||||||
"flaresolverr_client_timeout",
|
"flaresolverr_client_timeout",
|
||||||
|
"flaresolverr_cache_ttl",
|
||||||
)
|
)
|
||||||
"The variables that are integers."
|
"The variables that are integers."
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ from typing import Any
|
||||||
import yt_dlp
|
import yt_dlp
|
||||||
from yt_dlp.utils import make_archive_id
|
from yt_dlp.utils import make_archive_id
|
||||||
|
|
||||||
|
# imported to register the handler with yt-dlp.
|
||||||
from app.library.CFSolverRH import set_cf_handler
|
from app.library.CFSolverRH import set_cf_handler
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -51,7 +52,6 @@ class YTDLP(yt_dlp.YoutubeDL):
|
||||||
|
|
||||||
def __init__(self, params=None, auto_init=True):
|
def __init__(self, params=None, auto_init=True):
|
||||||
# Avoid yt-dlp preloading the archive file by stripping the param first
|
# Avoid yt-dlp preloading the archive file by stripping the param first
|
||||||
set_cf_handler(None)
|
|
||||||
orig_file = None
|
orig_file = None
|
||||||
patched_params = None
|
patched_params = None
|
||||||
if params is not None:
|
if params is not None:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue