apps/recipes/services/scraper.py (Line 421:9 - Line 437:7), apps/recipes/services/scraper.py (Line 233:9 - Line 249: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 419:28 - Line 242: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 430:8 - Line 446: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 121:4), apps/ai/services/quota.py (Line 62:5 - Line 79:5)
if getattr(settings, "AUTH_MODE", "home") != "passkey":
return (True, {})
if profile.user and profile.user.is_staff:
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/api.py (Line 179:9 - Line 187:58), apps/core/management/commands/cookie_admin.py (Line 652:9 - Line 659: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)