Initial infrastructure documentation - comprehensive homelab reference
This commit is contained in:
286
import-cocktaildb-to-bar-assistant-v2.py
Normal file
286
import-cocktaildb-to-bar-assistant-v2.py
Normal file
@@ -0,0 +1,286 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Import all cocktails from TheCocktailDB into Bar Assistant
|
||||
|
||||
Requirements:
|
||||
pip install requests
|
||||
|
||||
Usage:
|
||||
python import-cocktaildb-to-bar-assistant-v2.py
|
||||
"""
|
||||
|
||||
import requests
|
||||
import time
|
||||
import json
|
||||
import re
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
# Configuration
|
||||
BAR_ASSISTANT_URL = "https://cocktails.nianticbooks.com"
|
||||
BAR_ASSISTANT_API = f"{BAR_ASSISTANT_URL}/bar/api"
|
||||
COCKTAILDB_API = "https://www.thecocktaildb.com/api/json/v1/1"
|
||||
|
||||
BAR_ASSISTANT_TOKEN = "3|dqtZPWZoKdU59fFRLkGrL83I9HKm9EqmLCgO4kkI96199a82"
|
||||
BAR_ID = 1
|
||||
|
||||
# Cache for ingredients
|
||||
ingredient_cache = {}
|
||||
|
||||
|
||||
def parse_amount(measure_str: str) -> Tuple[Optional[float], str]:
|
||||
"""Parse amount string like '2 oz' into (2.0, 'oz')"""
|
||||
if not measure_str or not measure_str.strip():
|
||||
return (None, "")
|
||||
|
||||
measure_str = measure_str.strip()
|
||||
|
||||
# Try to extract number and unit
|
||||
# Handles: "2 oz", "1/2 oz", "1.5 oz", "2", etc.
|
||||
match = re.match(r'^([\d./\s]+)\s*(.*)$', measure_str)
|
||||
|
||||
if match:
|
||||
amount_str = match.group(1).strip()
|
||||
unit = match.group(2).strip()
|
||||
|
||||
# Handle fractions like "1/2" or "1 1/2"
|
||||
try:
|
||||
# Split on spaces to handle mixed fractions
|
||||
parts = amount_str.split()
|
||||
total = 0.0
|
||||
|
||||
for part in parts:
|
||||
if '/' in part:
|
||||
# Handle fraction
|
||||
num, denom = part.split('/')
|
||||
total += float(num) / float(denom)
|
||||
else:
|
||||
# Handle whole number or decimal
|
||||
total += float(part)
|
||||
|
||||
return (total, unit if unit else "oz")
|
||||
except:
|
||||
pass
|
||||
|
||||
# If we can't parse, return None
|
||||
return (None, "")
|
||||
|
||||
|
||||
def get_or_create_ingredient(name: str, headers: Dict) -> Optional[int]:
|
||||
"""Get ingredient ID by name, or create if it doesn't exist"""
|
||||
|
||||
# Check cache first
|
||||
name_lower = name.lower().strip()
|
||||
if name_lower in ingredient_cache:
|
||||
return ingredient_cache[name_lower]
|
||||
|
||||
# Search for ingredient
|
||||
url = f"{BAR_ASSISTANT_API}/ingredients"
|
||||
params = {"filter[name]": name}
|
||||
|
||||
try:
|
||||
response = requests.get(url, params=params, headers=headers, timeout=10)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
# Check if we found a match
|
||||
if data.get('data') and len(data['data']) > 0:
|
||||
for ingredient in data['data']:
|
||||
if ingredient['name'].lower() == name_lower:
|
||||
ingredient_id = ingredient['id']
|
||||
ingredient_cache[name_lower] = ingredient_id
|
||||
return ingredient_id
|
||||
|
||||
# Ingredient not found, create it
|
||||
print(f" Creating new ingredient: {name}")
|
||||
create_data = {
|
||||
"name": name,
|
||||
"strength": 0,
|
||||
"description": f"Imported from TheCocktailDB"
|
||||
}
|
||||
|
||||
response = requests.post(url, json=create_data, headers=headers, timeout=10)
|
||||
response.raise_for_status()
|
||||
ingredient_id = response.json()['data']['id']
|
||||
ingredient_cache[name_lower] = ingredient_id
|
||||
return ingredient_id
|
||||
|
||||
except Exception as e:
|
||||
print(f" [!] Error with ingredient '{name}': {e}")
|
||||
return None
|
||||
|
||||
|
||||
def get_all_cocktails_from_cocktaildb() -> List[Dict]:
|
||||
"""Fetch all cocktails from TheCocktailDB"""
|
||||
print("Fetching all cocktails from TheCocktailDB...")
|
||||
all_cocktails = []
|
||||
|
||||
letters = 'abcdefghijklmnopqrstuvwxyz0123456789'
|
||||
|
||||
for letter in letters:
|
||||
print(f" Fetching cocktails starting with '{letter}'...")
|
||||
url = f"{COCKTAILDB_API}/search.php?f={letter}"
|
||||
|
||||
try:
|
||||
response = requests.get(url, timeout=10)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
if data.get('drinks'):
|
||||
cocktails = data['drinks']
|
||||
all_cocktails.extend(cocktails)
|
||||
print(f" Found {len(cocktails)} cocktails")
|
||||
|
||||
time.sleep(0.5)
|
||||
|
||||
except Exception as e:
|
||||
print(f" Error fetching '{letter}': {e}")
|
||||
continue
|
||||
|
||||
print(f"\n[OK] Total cocktails found: {len(all_cocktails)}")
|
||||
return all_cocktails
|
||||
|
||||
|
||||
def convert_cocktail_ingredients(cocktail: Dict, headers: Dict) -> List[Dict]:
|
||||
"""Extract and convert ingredients from TheCocktailDB format"""
|
||||
ingredients = []
|
||||
|
||||
for i in range(1, 16):
|
||||
ingredient_key = f"strIngredient{i}"
|
||||
measure_key = f"strMeasure{i}"
|
||||
|
||||
ingredient_name = cocktail.get(ingredient_key)
|
||||
measure = cocktail.get(measure_key, "")
|
||||
|
||||
if ingredient_name and ingredient_name.strip():
|
||||
# Get or create the ingredient
|
||||
ingredient_id = get_or_create_ingredient(ingredient_name.strip(), headers)
|
||||
|
||||
if ingredient_id:
|
||||
# Parse the amount
|
||||
amount, units = parse_amount(measure)
|
||||
|
||||
ingredients.append({
|
||||
"sort": i, # Add sort field for ingredient order
|
||||
"ingredient_id": ingredient_id,
|
||||
"amount": amount if amount is not None else 0,
|
||||
"units": units if units else "oz", # Default to "oz" if no unit specified
|
||||
"optional": False
|
||||
})
|
||||
|
||||
return ingredients
|
||||
|
||||
|
||||
def import_cocktail(cocktail: Dict, headers: Dict) -> bool:
|
||||
"""Import a single cocktail into Bar Assistant"""
|
||||
|
||||
# Convert ingredients first
|
||||
ingredients = convert_cocktail_ingredients(cocktail, headers)
|
||||
|
||||
if not ingredients:
|
||||
print(f" [!] No valid ingredients found")
|
||||
return False
|
||||
|
||||
# Build cocktail data
|
||||
cocktail_data = {
|
||||
"name": cocktail.get('strDrink', 'Unknown'),
|
||||
"instructions": cocktail.get('strInstructions', ''),
|
||||
"description": f"Imported from TheCocktailDB. Category: {cocktail.get('strCategory', 'N/A')}. {cocktail.get('strAlcoholic', '')}",
|
||||
"source": f"TheCocktailDB (ID: {cocktail.get('idDrink')})",
|
||||
"garnish": cocktail.get('strGarnish', ''),
|
||||
"glass": cocktail.get('strGlass', ''),
|
||||
"ingredients": ingredients
|
||||
# Note: Images are not imported - Bar Assistant expects image IDs, not URLs
|
||||
}
|
||||
|
||||
# Add tags
|
||||
tags = []
|
||||
if cocktail.get('strCategory'):
|
||||
tags.append(cocktail['strCategory'])
|
||||
if cocktail.get('strAlcoholic'):
|
||||
tags.append(cocktail['strAlcoholic'])
|
||||
if cocktail.get('strIBA'):
|
||||
tags.append(cocktail['strIBA'])
|
||||
|
||||
if tags:
|
||||
cocktail_data['tags'] = tags
|
||||
|
||||
# Import
|
||||
url = f"{BAR_ASSISTANT_API}/cocktails"
|
||||
|
||||
try:
|
||||
response = requests.post(url, json=cocktail_data, headers=headers, timeout=30)
|
||||
response.raise_for_status()
|
||||
return True
|
||||
except requests.exceptions.HTTPError as e:
|
||||
if e.response.status_code == 409:
|
||||
print(f" [!] Already exists")
|
||||
return False
|
||||
elif e.response.status_code == 422:
|
||||
print(f" [X] Validation error: {e.response.text[:200]}")
|
||||
return False
|
||||
else:
|
||||
print(f" [X] HTTP Error {e.response.status_code}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f" [X] Error: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
"""Main execution"""
|
||||
print("="*60)
|
||||
print("TheCocktailDB -> Bar Assistant Bulk Import")
|
||||
print("="*60)
|
||||
|
||||
# Set up headers
|
||||
headers = {
|
||||
"Authorization": f"Bearer {BAR_ASSISTANT_TOKEN}",
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
"Bar-Assistant-Bar-Id": str(BAR_ID)
|
||||
}
|
||||
|
||||
# Step 1: Fetch all cocktails from TheCocktailDB
|
||||
cocktails = get_all_cocktails_from_cocktaildb()
|
||||
|
||||
if not cocktails:
|
||||
print("\n[ERROR] No cocktails found from TheCocktailDB")
|
||||
return
|
||||
|
||||
# Step 2: Import
|
||||
print(f"\nImporting {len(cocktails)} cocktails to Bar Assistant...")
|
||||
print("This may take a while...\n")
|
||||
|
||||
success_count = 0
|
||||
skip_count = 0
|
||||
error_count = 0
|
||||
|
||||
for idx, cocktail in enumerate(cocktails, 1):
|
||||
cocktail_name = cocktail.get('strDrink', 'Unknown')
|
||||
print(f"[{idx}/{len(cocktails)}] {cocktail_name}...")
|
||||
|
||||
if import_cocktail(cocktail, headers):
|
||||
success_count += 1
|
||||
print(f" [OK] Imported successfully")
|
||||
else:
|
||||
skip_count += 1
|
||||
|
||||
time.sleep(0.5)
|
||||
|
||||
# Summary
|
||||
print("\n" + "="*60)
|
||||
print("Import Complete!")
|
||||
print("="*60)
|
||||
print(f"[OK] Successfully imported: {success_count}")
|
||||
print(f"[!] Skipped (duplicates/errors): {skip_count}")
|
||||
print(f"\nTotal processed: {len(cocktails)}")
|
||||
print(f"\nView your cocktails at: {BAR_ASSISTANT_URL}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n[!] Import cancelled by user")
|
||||
except Exception as e:
|
||||
print(f"\n\n[ERROR] Error: {e}")
|
||||
Reference in New Issue
Block a user