apps/recipes/services/scraper.py (Line 412:9 - Line 428:7), apps/recipes/services/scraper.py (Line 220:9 - Line 236:6)
from curl_cffi import CurlOpt
current_url = url
current_resolve = curl_resolve or []
for _ in range(MAX_REDIRECT_HOPS):
curl_opts = {CurlOpt.RESOLVE: current_resolve} if current_resolve else {}
async with AsyncSession(impersonate=profile, curl_options=curl_opts) as session:
response = await session.get(
current_url,
timeout=self.timeout,
allow_redirects=False,
)
if response.status_code in (301, 302, 303, 307, 308):
location = response.headers.get("location")
if not location:
return
apps/recipes/services/image_cache.py (Line 160:18 - Line 171:17), apps/recipes/services/scraper.py (Line 410:28 - Line 229:8)
(self, url, profile, curl_resolve=None):
"""Fetch image following redirects with per-hop SSRF validation and DNS pinning."""
from curl_cffi import CurlOpt
current_url = url
current_resolve = curl_resolve or []
for _ in range(MAX_REDIRECT_HOPS):
curl_opts = {CurlOpt.RESOLVE: current_resolve} if current_resolve else {}
async with AsyncSession(impersonate=profile, curl_options=curl_opts) as session:
response = await session.get(
current_url,
timeout=self.DOWNLOAD_TIMEOUT
apps/recipes/services/image_cache.py (Line 171:17 - Line 187:3), apps/recipes/services/scraper.py (Line 421:8 - Line 437:3)
,
allow_redirects=False,
)
if response.status_code in (301, 302, 303, 307, 308):
location = response.headers.get("location")
if not location:
return None
try:
resolved = validate_redirect_url(location)
except ValueError:
return None
current_url = location
current_resolve = resolved.curl_resolve
continue
if response.status_code in
apps/ai/services/validator.py (Line 78:20 - Line 92:15), apps/ai/services/validator.py (Line 64:21 - Line 78:20)
apps/ai/services/remix.py (Line 261:5 - Line 276:16), apps/ai/services/scaling.py (Line 28:5 - Line 43:13)
numbers = re.findall(r"\d+", time_str)
if not numbers:
return None
minutes = int(numbers[0])
# Convert hours to minutes if needed
if "hour" in time_str:
minutes *= 60
if len(numbers) > 1:
minutes += int(numbers[1])
return minutes
def _parse_servings
apps/ai/services/quota.py (Line 104:5 - Line 118:4), apps/ai/services/quota.py (Line 65:5 - Line 79:5)
if getattr(settings, "AUTH_MODE", "home") != "passkey":
return (True, {})
if profile.unlimited_ai:
return (True, {})
if feature not in FEATURE_LIMIT_FIELDS:
raise ValueError(f"Unknown quota feature: {feature}")
app = AppSettings.get()
limit_field = FEATURE_LIMIT_FIELDS[feature]
limit = getattr(app, limit_field)
key = _cache_key(profile.pk, feature)
ttl
apps/ai/services/openrouter.py (Line 133:11 - Line 159:2), apps/ai/services/openrouter.py (Line 89:5 - Line 115:6)
(
messages=messages,
model=model,
stream=False,
timeout_ms=timeout_ms,
)
if not response or not hasattr(response, "choices"):
raise AIResponseError("Invalid response structure from OpenRouter")
if not response.choices:
raise AIResponseError("No choices in OpenRouter response")
content = response.choices[0].message.content
if json_response:
return self._parse_json_response(content)
return {"content": content}
except AIServiceError:
raise
except Exception as e:
logger.exception("OpenRouter API error")
raise AIResponseError(f"OpenRouter API error: {e}")
@
apps/core/auth_api.py (Line 68:5 - Line 76:21), apps/core/auth_api.py (Line 46:5 - Line 56:30)
_require_auth_mode(request)
user = request.user
if not user or not getattr(user, "is_authenticated", False):
return Status(401, {"error": "Authentication required"})
try:
profile = user.profile
except Profile.DoesNotExist:
return Status(401, {"error": "Authentication required"})
return Status(200, get_deletion_preview
apps/core/auth_api.py (Line 99:5 - Line 108:9), apps/core/auth_api.py (Line 46:5 - Line 56:7)
_require_auth_mode(request)
user = request.user
if not user or not getattr(user, "is_authenticated", False):
return Status(401, {"error": "Authentication required"})
try:
profile = user.profile
except Profile.DoesNotExist:
return Status(401, {"error": "Authentication required"})
username
apps/core/api.py (Line 176:9 - Line 184:58), apps/core/management/commands/_cookie_admin_app.py (Line 64:9 - Line 71:7)
AIDiscoverySuggestion.objects.all().delete()
ServingAdjustment.objects.all().delete()
RecipeViewHistory.objects.all().delete()
RecipeCollectionItem.objects.all().delete()
RecipeCollection.objects.all().delete()
RecipeFavorite.objects.all().delete()
CachedSearchImage.objects.all().delete()
# Delete all recipes (this will cascade to related items)