# -*- coding: utf-8 -*- from flask import Flask, render_template_string, request, redirect, url_for, send_file import json import os import logging import threading import time from datetime import datetime from huggingface_hub import HfApi, hf_hub_download from huggingface_hub.utils import RepositoryNotFoundError from werkzeug.utils import secure_filename from urllib.parse import quote app = Flask(__name__) DATA_FILE = 'data.json' REPO_ID = "Kgshop/Mebelhause" HF_TOKEN_WRITE = os.getenv("HF_TOKEN") HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") LOGO_URL = "https://huggingface.co/spaces/Mebelhause/Kg/resolve/main/Screenshot_20250411-112027.png" WHATSAPP_NUMBER = "+996700253966" logging.basicConfig(level=logging.INFO) def load_data(): try: download_db_from_hf() with open(DATA_FILE, 'r', encoding='utf-8') as file: data = json.load(file) logging.info("Данные успешно загружены из JSON") if not isinstance(data, dict) or 'products' not in data or 'categories' not in data: logging.warning("Структура данных некорректна, используется структура по умолчанию.") return {'products': [], 'categories': []} # Ensure products and categories are lists if not isinstance(data.get('products'), list): data['products'] = [] if not isinstance(data.get('categories'), list): data['categories'] = [] return data except FileNotFoundError: logging.warning("Локальный файл базы данных не найден. Создание пустой базы.") return {'products': [], 'categories': []} except json.JSONDecodeError: logging.error("Ошибка: Невозможно декодировать JSON файл. Возврат пустой базы.") return {'products': [], 'categories': []} except RepositoryNotFoundError: logging.error("Репозиторий Hugging Face не найден. Создание локальной базы данных.") # Create an empty file if it doesn't exist locally after repo not found if not os.path.exists(DATA_FILE): with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'products': [], 'categories': []}, f) return {'products': [], 'categories': []} except Exception as e: logging.error(f"Произошла ошибка при загрузке данных: {e}. Возврат пустой базы.") return {'products': [], 'categories': []} def save_data(data): try: # Ensure structure is correct before saving if 'products' not in data or not isinstance(data['products'], list): data['products'] = [] if 'categories' not in data or not isinstance(data['categories'], list): data['categories'] = [] with open(DATA_FILE, 'w', encoding='utf-8') as file: json.dump(data, file, ensure_ascii=False, indent=4) logging.info("Данные успешно сохранены в JSON") upload_db_to_hf() except Exception as e: logging.error(f"Ошибка при сохранении данных: {e}") # Optionally re-raise or handle differently # raise def upload_db_to_hf(): if not HF_TOKEN_WRITE: logging.warning("HF_TOKEN (write) не установлен. Загрузка на Hugging Face пропущена.") return if not os.path.exists(DATA_FILE): logging.warning(f"Файл {DATA_FILE} не найден для загрузки.") return try: api = HfApi() api.upload_file( path_or_fileobj=DATA_FILE, path_in_repo=DATA_FILE, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, commit_message=f"Автоматическое резервное копирование базы данных {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" ) logging.info("Резервная копия JSON базы успешно загружена на Hugging Face.") except RepositoryNotFoundError: logging.error(f"Репозиторий {REPO_ID} не найден на Hugging Face. Не удалось загрузить.") except Exception as e: logging.error(f"Ошибка при загрузке резервной копии на Hugging Face: {e}") def download_db_from_hf(): if not HF_TOKEN_READ: logging.warning("HF_TOKEN_READ не установлен. Скачивание с Hugging Face пропущено.") # Attempt to create an empty file if none exists if not os.path.exists(DATA_FILE): save_data({'products': [], 'categories': []}) # Save an empty structure return try: hf_hub_download( repo_id=REPO_ID, filename=DATA_FILE, repo_type="dataset", token=HF_TOKEN_READ, local_dir=".", local_dir_use_symlinks=False, force_download=True, # Ensure we get the latest version resume_download=False # Avoid issues with partial downloads ) logging.info("JSON база успешно скачана из Hugging Face.") # Verify file integrity after download try: with open(DATA_FILE, 'r', encoding='utf-8') as f: json.load(f) logging.info("Проверка целостности JSON после скачивания: OK") except (json.JSONDecodeError, FileNotFoundError) as e: logging.error(f"Ошибка проверки JSON после скачивания: {e}. Файл может быть поврежден или пуст.") # Attempt to create a default empty file if it's broken save_data({'products': [], 'categories': []}) except RepositoryNotFoundError as e: logging.error(f"Репозиторий {REPO_ID} не найден на Hugging Face: {e}") if not os.path.exists(DATA_FILE): save_data({'products': [], 'categories': []}) # Save an empty structure # Do not raise here, allow fallback to local/empty data except Exception as e: # Catch specific hf_hub exceptions if needed, e.g., HTTPError logging.error(f"Ошибка при скачивании JSON базы из Hugging Face: {e}") if not os.path.exists(DATA_FILE): save_data({'products': [], 'categories': []}) # Save an empty structure # Do not raise here, allow fallback to local/empty data def periodic_backup(): while True: time.sleep(800) # Sleep first to avoid immediate backup on start logging.info("Запуск периодического резервного копирования...") # Ensure data is loaded before backup attempt try: load_data() # Reload potentially updated data except Exception as e: logging.error(f"Ошибка загрузки данных перед бэкапом: {e}") # Now attempt upload upload_db_to_hf() # --- Global Styles --- GLOBAL_STYLES = """ """ # --- Landing Page --- @app.route('/') def landing(): landing_html = ''' Mebel Hause KG - Изготовление мебели на заказ ''' + GLOBAL_STYLES + '''

Mebel Hause KG

Создаем мебель вашей мечты с любовью и мастерством

Смотреть готовые изделия

Почему выбирают нас?

Индивидуальный дизайн

Разработаем уникальный проект под ваш интерьер и пожелания.

Точные замеры

Гарантируем идеальное соответствие мебели вашему пространству.

Качественные материалы

Используем только проверенные и долговечные материалы.

Своевременная доставка

Доставим и соберем вашу новую мебель точно в срок.

Рассчитать стоимость мебели

Заполните форму ниже, и мы свяжемся с вами для уточнения деталей и расчета.

Свяжитесь с нами

Готовы обсудить ваш проект? Напишите нам в WhatsApp или позвоните!

Телефон / WhatsApp: {{ whatsapp_number_display }}

Мы находимся в Бишкеке.

''' # Format number for display whatsapp_display = f"+{WHATSAPP_NUMBER[0]} ({WHATSAPP_NUMBER[1:4]}) {WHATSAPP_NUMBER[4:7]}-{WHATSAPP_NUMBER[7:10]}-{WHATSAPP_NUMBER[10:]}" return render_template_string( landing_html, logo_url=LOGO_URL, whatsapp_number=WHATSAPP_NUMBER, whatsapp_number_display=whatsapp_display, current_year=datetime.now().year ) # --- Catalog Page --- @app.route('/catalog') def catalog(): data = load_data() products = data.get('products', []) categories = data.get('categories', []) catalog_html = ''' Каталог готовой мебели - Mebel Hause KG ''' + GLOBAL_STYLES + '''

Каталог готовых изделий

Ознакомьтесь с нашими работами. Возможно, что-то из этого подойдет именно вам!

{% for category in categories %} {% endfor %}
{% for product in products %}
{% if product.get('photos') and product['photos']|length > 0 %} {{ product['name'] }} {% else %} Нет фото {% endif %}

{{ product['name'] }}

{{ product['price'] }} сом

{{ product['description'] }}

{% else %}

В каталоге пока нет товаров.

{% endfor %}
''' return render_template_string( catalog_html, products=products, categories=categories, repo_id=REPO_ID, logo_url=LOGO_URL, whatsapp_number=WHATSAPP_NUMBER, current_year=datetime.now().year ) # --- Product Detail Partial --- @app.route('/product/') def product_detail(index): data = load_data() products = data.get('products', []) try: product = products[index] except IndexError: logging.warning(f"Попытка доступа к несуществующему продукту с индексом {index}") return "Товар не найден", 404 detail_html = '''

{{ product['name'] }}

{% if product.get('photos') and product['photos']|length > 0 %} {% for photo in product['photos'] %}
{{ product['name'] }} - Фото {{ loop.index }}
{% endfor %} {% else %}
Фото отсутствует
{% endif %}

Категория: {{ product.get('category', 'Без категории') }}

Цена: {{ product['price'] }} сом

Описание:

{{ product['description'] }}

{% if product.get('colors') and product['colors']|length > 0 %}

Доступные цвета: {{ product['colors']|join(', ') }}

{% endif %}
''' return render_template_string(detail_html, product=product, repo_id=REPO_ID) # --- Admin Panel --- @app.route('/admin', methods=['GET', 'POST']) def admin(): data = load_data() products = data.get('products', []) categories = data.get('categories', []) message = None # To display success/error messages if request.method == 'POST': action = request.form.get('action') logging.debug(f"Admin action received: {action}") logging.debug(f"Form data: {request.form}") logging.debug(f"Files data: {request.files}") try: if action == 'add_category': category_name = request.form.get('category_name', '').strip() if category_name and category_name not in categories: categories.append(category_name) save_data(data) message = ("Категория добавлена", "success") elif not category_name: message = ("Название категории не может быть пустым", "error") else: message = ("Категория уже существует", "error") return redirect(url_for('admin', msg=message[0], type=message[1])) elif action == 'delete_category': category_index_str = request.form.get('category_index') if category_index_str is not None: category_index = int(category_index_str) if 0 <= category_index < len(categories): deleted_category = categories.pop(category_index) # Update products using this category for product in products: if product.get('category') == deleted_category: product['category'] = 'Без категории' # Assign to default save_data(data) message = ("Категория удалена", "success") else: message = ("Неверный индекс категории", "error") else: message = ("Индекс категории не указан", "error") return redirect(url_for('admin', msg=message[0], type=message[1])) elif action == 'add' or action == 'edit': name = request.form.get('name', '').strip() price_str = request.form.get('price', '0').replace(',', '.') description = request.form.get('description', '').strip() category = request.form.get('category') photos_files = request.files.getlist('photos') # Get colors - handle empty strings and potential duplicates colors = [c.strip() for c in request.form.getlist('colors') if c.strip()] colors = sorted(list(set(colors))) # Remove duplicates and sort logging.debug(f"Received colors: {request.form.getlist('colors')}") logging.debug(f"Processed colors: {colors}") if not name or not description: message = ("Название и описание товара обязательны", "error") # Need to pass current state back if redirecting return redirect(url_for('admin', msg=message[0], type=message[1])) try: price = float(price_str) if price < 0: raise ValueError("Price cannot be negative") except ValueError: message = ("Некорректное значение цены", "error") return redirect(url_for('admin', msg=message[0], type=message[1])) photos_list = [] # Upload new photos if provided if photos_files and any(f.filename for f in photos_files): if not HF_TOKEN_WRITE: message = ("HF_TOKEN (write) не установлен. Невозможно загрузить фото.", "warning") # Decide if you want to proceed without photos or stop # return redirect(url_for('admin', msg=message[0], type=message[1])) else: api = HfApi() uploads_dir = 'uploads_temp' # Temporary local storage os.makedirs(uploads_dir, exist_ok=True) for photo in photos_files[:10]: # Limit photos if photo and photo.filename: try: photo_filename = secure_filename(f"{datetime.now().strftime('%Y%m%d%H%M%S')}_{photo.filename}") temp_path = os.path.join(uploads_dir, photo_filename) photo.save(temp_path) logging.info(f"Uploading photo {photo_filename} to HF...") api.upload_file( path_or_fileobj=temp_path, path_in_repo=f"photos/{photo_filename}", repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, commit_message=f"Фото для товара {name}" ) photos_list.append(photo_filename) logging.info(f"Photo {photo_filename} uploaded successfully.") # Clean up local temp file if os.path.exists(temp_path): os.remove(temp_path) except Exception as e: logging.error(f"Ошибка загрузки фото {photo.filename}: {e}") message = (f"Ошибка загрузки фото {photo.filename}", "error") # Decide whether to stop or continue # Clean up temp directory if empty if os.path.exists(uploads_dir) and not os.listdir(uploads_dir): os.rmdir(uploads_dir) if action == 'add': new_product = { 'name': name, 'price': price, 'description': description, 'category': category if category in categories else 'Без категории', 'photos': photos_list, # Only new photos for 'add' 'colors': colors } products.append(new_product) message = ("Товар успешно добавлен", "success") elif action == 'edit': index_str = request.form.get('index') if index_str is not None: index = int(index_str) if 0 <= index < len(products): # Update fields products[index]['name'] = name products[index]['price'] = price products[index]['description'] = description products[index]['category'] = category if category in categories else 'Без категории' products[index]['colors'] = colors # If new photos were uploaded, replace the old list. Otherwise, keep existing photos. if photos_list: # Optionally delete old photos from HF here if needed (more complex) products[index]['photos'] = photos_list # If no new photos uploaded, 'photos_list' is empty, # and we don't touch products[index]['photos'] message = ("Товар успешно обновлен", "success") else: message = ("Неверный индекс товара для редактирования", "error") else: message = ("Индекс товара для редактирования не указан", "error") elif action == 'delete': index_str = request.form.get('index') if index_str is not None: index = int(index_str) if 0 <= index < len(products): # Optionally delete photos from HF here (more complex) del products[index] message = ("Товар удален", "success") else: message = ("Неверный индекс товара для удаления", "error") else: message = ("Индекс товара для удаления не указан", "error") # Save changes if any action potentially modified data (except delete errors) if message and message[1] != "error": # Save on success or warning save_data(data) # Redirect only after processing and potential save return redirect(url_for('admin', msg=message[0] if message else None, type=message[1] if message else None)) except Exception as e: logging.error(f"Ошибка в админ панели ({action}): {e}", exc_info=True) message = (f"Произошла внутренняя ошибка: {e}", "error") return redirect(url_for('admin', msg=message[0], type=message[1])) # --- Admin HTML --- flash_message = request.args.get('msg') flash_type = request.args.get('type', 'info') # Default type admin_html = ''' Админ-панель - Mebel Hause KG ''' + GLOBAL_STYLES + '''

Админ-панель

{% if flash_message %}
{{ flash_message }}
{% endif %}

Добавить новый товар

Управление категориями

Список категорий

{% if categories %}
{% for category in categories %}

{{ category }}

{% if category != 'Без категории' %}
{% else %}

(Системная категория, не удаляется)

{% endif %}
{% endfor %}
{% else %}

Категорий пока нет.

{% endif %}

Управление базой данных

Список товаров

{% if products %}
{% for product in products %}

{{ product['name'] }}

Категория: {{ product.get('category', 'Без категории') }}

Цена: {{ product['price'] }} сом

Описание: {{ product['description'][:100] }}{% if product['description']|length > 100 %}...{% endif %}

Цвета: {{ product.get('colors', [])|join(', ') if product.get('colors') else 'Не указаны' }}

{% if product.get('photos') and product['photos']|length > 0 %}

Фотографии:

{% for photo in product['photos'] %} {{ product['name'] }} - Фото {{ loop.index }} {% endfor %}
{% else %}

Фотографии: Нет

{% endif %}
Редактировать товар
{% if product.get('colors') %} {% for color in product.get('colors', []) %}
{% endfor %} {% else %}
{% endif %} {% if not product.get('colors') %} {# Ensure at least one input exists even if no colors are set #} {% endif %}
{% endfor %}
{% else %}

В базе данных пока нет товаров.

{% endif %}
''' return render_template_string( admin_html, products=products, categories=categories, repo_id=REPO_ID, logo_url=LOGO_URL, flash_message=flash_message, flash_type=flash_type, current_year=datetime.now().year ) @app.route('/backup', methods=['POST']) def backup(): try: upload_db_to_hf() # Redirect back with success message return redirect(url_for('admin', msg="Резервное копирование на Hugging Face запущено.", type="success")) except Exception as e: logging.error(f"Ошибка при ручном резервном копировании: {e}") return redirect(url_for('admin', msg=f"Ошибка резервного копирования: {e}", type="error")) @app.route('/download', methods=['GET']) def download(): try: # Optionally trigger a download from HF first to ensure local is up-to-date # download_db_from_hf() # Uncomment if you want to force sync before download if os.path.exists(DATA_FILE): return send_file(DATA_FILE, as_attachment=True, download_name='mebelhause_database.json') else: return redirect(url_for('admin', msg="Локальный файл базы данных не найден.", type="error")) except Exception as e: logging.error(f"Ошибка при скачивании файла базы данных: {e}") return redirect(url_for('admin', msg=f"Ошибка скачивания файла: {e}", type="error")) if __name__ == '__main__': # Ensure data file exists on start, try loading/downloading try: initial_data = load_data() # If load_data created an empty structure due to errors, save it. if not os.path.exists(DATA_FILE) or os.path.getsize(DATA_FILE) == 0: save_data(initial_data) except Exception as e: logging.error(f"Критическая ошибка при инициализации базы данных: {e}") # Consider exiting if the DB is crucial and cannot be loaded/created # exit(1) # Start periodic backup in a separate thread if HF_TOKEN_WRITE and HF_TOKEN_READ: # Only run backup if tokens are set backup_thread = threading.Thread(target=periodic_backup, daemon=True) backup_thread.start() else: logging.warning("Токены Hugging Face (WRITE или READ) не установлены. Периодическое резервное копирование отключено.") # Run Flask App # Use 'waitress' for production instead of Flask's built-in server # from waitress import serve # serve(app, host='0.0.0.0', port=7860) # For development: app.run(debug=False, host='0.0.0.0', port=7860) # Set debug=False for production/waitress