Files
holy-flavor-rater/backend/api/management/commands/sync_flavors.py
T
2026-04-11 10:37:49 +02:00

212 lines
7.6 KiB
Python

import os
import re
import requests
from django.conf import settings
from django.core.management.base import BaseCommand
from django.utils.text import slugify
from api.models import Category, Flavor
class Command(BaseCommand):
help = "Syncs flavors from Holy Energy Shopify API"
def clean_html(self, raw_html):
if not raw_html:
return ""
cleanr = re.compile("<.*?>")
cleantext = re.sub(cleanr, "", raw_html)
return cleantext.strip()
def download_image(self, url, flavor_name):
if not url:
return None
try:
safe_name = slugify(flavor_name)
ext = url.split(".")[-1].split("?")[0].lower()
if ext not in ["jpg", "jpeg", "png", "webp", "gif"]:
ext = "png"
filename = f"{safe_name}.{ext}"
filepath = os.path.join(settings.MEDIA_ROOT, "flavors", filename)
os.makedirs(os.path.dirname(filepath), exist_ok=True)
response = requests.get(url, headers={"User-Agent": "Mozilla/5.0"}, timeout=10)
response.raise_for_status()
if os.path.exists(filepath):
os.remove(filepath)
with open(filepath, "wb") as f:
f.write(response.content)
return f"flavors/{filename}"
except Exception as e:
self.stdout.write(
self.style.WARNING(f"Failed to download image for {flavor_name}: {e}")
)
return None
def handle(self, *args, **options):
url = "https://weareholy.com/products.json?limit=250"
self.stdout.write(f"Fetching from {url}...")
try:
response = requests.get(url, headers={"User-Agent": "Mozilla/5.0"}, timeout=15)
response.raise_for_status()
data = response.json()
except Exception as e:
self.stdout.write(self.style.ERROR(f"Failed to fetch data: {e}"))
return
products = data.get("products", [])
self.stdout.write(f"Found {len(products)} products.")
cat_map = {
"energy": Category.objects.get_or_create(name="Energy", defaults={"slug": "energy"})[0],
"hydration": Category.objects.get_or_create(
name="Hydration", defaults={"slug": "hydration"}
)[0],
"iced-tea": Category.objects.get_or_create(
name="Iced Tea", defaults={"slug": "iced-tea"}
)[0],
"milkshake": Category.objects.get_or_create(
name="Milkshake", defaults={"slug": "milkshake"}
)[0],
"packs": Category.objects.get_or_create(
name="Packs and other", defaults={"slug": "packs-and-other"}
)[0],
}
created_count = 0
updated_count = 0
synced_external_ids = []
for p in products:
title = p.get("title", "").strip()
tags = p.get("tags", [])
if isinstance(tags, str):
tags = [t.strip() for t in tags.split(",")]
tags_lower = [t.lower() for t in tags]
title_lower = title.lower()
is_pack = False
pack_keywords = [
"bundle",
"set",
"box",
"probe",
"sample",
"taster",
"starter",
"collection",
"probier",
]
if any(k in title_lower for k in pack_keywords):
is_pack = True
if "shaker" in title_lower or "merch" in tags_lower:
is_pack = True
category = None
if is_pack:
category = cat_map["packs"]
elif "holy energy" in tags_lower or "energy" in tags_lower:
category = cat_map["energy"]
elif "holy hydration" in tags_lower or "hydration" in tags_lower:
category = cat_map["hydration"]
elif "holy iced tea" in tags_lower or "iced tea" in tags_lower:
category = cat_map["iced-tea"]
elif "milkshake" in tags_lower:
category = cat_map["milkshake"]
if not category:
if "shaker" in tags_lower:
category = cat_map["packs"]
else:
continue
variants = p.get("variants", [])
is_available = any(v.get("available") for v in variants)
images = p.get("images", [])
image_url = images[0].get("src") if images else None
description = self.clean_html(p.get("body_html", ""))
handle = p.get("handle")
shop_url = f"https://weareholy.com/products/{handle}" if handle else None
# Find or create - be very specific to avoid duplicates
# 1. Try external_id
flavor = Flavor.objects.filter(external_id=p["id"]).first()
if not flavor:
# 2. Try exact name match in category
# REMOVED: is_legacy=False
flavor = Flavor.objects.filter(name=title, category=category).first()
if not flavor:
# 3. Try case-insensitive name match in category (safety for whitespace/case)
# REMOVED: is_legacy=False
flavor = Flavor.objects.filter(name__iexact=title, category=category).first()
if not flavor:
flavor = Flavor(external_id=p["id"], name=title, category=category, is_legacy=False)
created_count += 1
else:
updated_count += 1
# Ensure external_id is updated if it was missing
if not flavor.external_id:
flavor.external_id = p["id"]
# Reactivate the flavor if it was previously marked as legacy
if flavor.is_legacy:
flavor.is_legacy = False
# Ensure all synced fields are accurately reflected
if (
flavor.name != title
and not Flavor.objects.filter(name=title, category=category)
.exclude(pk=flavor.pk)
.exists()
):
flavor.name = title
flavor.description = description
flavor.shop_url = shop_url
flavor.is_available = is_available
if image_url:
# Check if local file exists
file_exists = False
if flavor.image:
abs_path = os.path.join(settings.MEDIA_ROOT, flavor.image.name)
if os.path.exists(abs_path):
file_exists = True
# Download if missing or URL changed
if not file_exists or flavor.image_url != image_url:
rel_path = self.download_image(image_url, title)
if rel_path:
flavor.image = rel_path
self.stdout.write(f"Downloaded image for: {title}")
else:
self.stdout.write(
self.style.WARNING(f"Failed to ensure image for: {title}")
)
flavor.image_url = image_url
flavor.save()
synced_external_ids.append(p["id"])
discontinued_count = (
Flavor.objects.filter(external_id__isnull=False)
.exclude(external_id__in=synced_external_ids)
.update(is_available=False)
)
self.stdout.write(
self.style.SUCCESS(
f"Finished! Created: {created_count}, Updated: {updated_count}, Marked Unavailable: {discontinued_count}"
)
)