Files
holy-flavor-rater/backend/api/management/commands/sync_flavors.py
T
2026-03-19 15:53:54 +01:00

168 lines
6.9 KiB
Python

import json
import requests
import re
import os
from django.core.management.base import BaseCommand
from django.core.files import File
from django.conf import settings
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
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}"))